Compare commits

...

3 Commits

Author SHA1 Message Date
0af99b6b70 修改语言图标 2026-02-03 17:04:41 +08:00
eaf97b8279 ... 2026-02-02 23:57:01 +08:00
76749549ff ... 2026-02-02 23:32:39 +08:00
76 changed files with 687 additions and 205 deletions

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Letter, SupportedAlphabets } from "@/lib/interfaces"; import { Letter, SupportedAlphabets } from "@/lib/interfaces";
import { IconClick } from "@/components/ui/buttons"; import { IconClick } from "@/components/ui/buttons";
import IMAGES from "@/config/images"; import { IMAGES } from "@/config/images";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
interface AlphabetCardProps { interface AlphabetCardProps {
@@ -13,7 +13,7 @@ interface AlphabetCardProps {
onBack: () => void; onBack: () => void;
} }
export default function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardProps) { export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardProps) {
const t = useTranslations("alphabet"); const t = useTranslations("alphabet");
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [showIPA, setShowIPA] = useState(true); const [showIPA, setShowIPA] = useState(true);

View File

@@ -1,6 +1,6 @@
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import { IconClick } from "@/components/ui/buttons"; import { IconClick } from "@/components/ui/buttons";
import IMAGES from "@/config/images"; import { IMAGES } from "@/config/images";
import { Letter, SupportedAlphabets } from "@/lib/interfaces"; import { Letter, SupportedAlphabets } from "@/lib/interfaces";
import { import {
Dispatch, Dispatch,
@@ -12,7 +12,7 @@ import {
} from "react"; } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
export default function MemoryCard({ export function MemoryCard({
alphabet, alphabet,
setChosenAlphabet, setChosenAlphabet,
}: { }: {

View File

@@ -3,9 +3,9 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Letter, SupportedAlphabets } from "@/lib/interfaces"; import { Letter, SupportedAlphabets } from "@/lib/interfaces";
import Container from "@/components/ui/Container"; import { Container } from "@/components/ui/Container";
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import AlphabetCard from "./AlphabetCard"; import { AlphabetCard } from "./AlphabetCard";
export default function Alphabet() { export default function Alphabet() {
const t = useTranslations("alphabet"); const t = useTranslations("alphabet");

View File

@@ -1,4 +1,4 @@
import { TSharedEntry } from "@/shared"; import { TSharedEntry } from "@/shared/dictionary-type";
interface DictionaryEntryProps { interface DictionaryEntryProps {
entry: TSharedEntry; entry: TSharedEntry;

View File

@@ -4,9 +4,9 @@ import { authClient } from "@/lib/auth-client";
import { DictionaryEntry } from "./DictionaryEntry"; import { DictionaryEntry } from "./DictionaryEntry";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { performDictionaryLookup } from "./utils"; import { performDictionaryLookup } from "./utils";
import { TSharedItem } from "@/shared"; import { TSharedItem } from "@/shared/dictionary-type";
import { TSharedFolder } from "@/shared/folder-type"; import { TSharedFolder } from "@/shared/folder-type";
import { actionCreatePair } from "@/modules/folder"; import { actionCreatePair } from "@/modules/folder/folder-aciton";
interface SearchResultProps { interface SearchResultProps {
searchResult: TSharedItem; searchResult: TSharedItem;

View File

@@ -1,15 +1,15 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Container from "@/components/ui/Container"; import { Container } from "@/components/ui/Container";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { SearchForm } from "./SearchForm"; import { SearchForm } from "./SearchForm";
import { SearchResult } from "./SearchResult"; import { SearchResult } from "./SearchResult";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { POPULAR_LANGUAGES } from "./constants"; import { POPULAR_LANGUAGES } from "./constants";
import { performDictionaryLookup } from "./utils"; import { performDictionaryLookup } from "./utils";
import { TSharedItem } from "@/shared"; import { TSharedItem } from "@/shared/dictionary-type";
import { actionGetFoldersByUserId } from "@/modules/folder"; import { actionGetFoldersByUserId } from "@/modules/folder/folder-aciton";
import { TSharedFolder } from "@/shared/folder-type"; import { TSharedFolder } from "@/shared/folder-type";
import { toast } from "sonner"; import { toast } from "sonner";

View File

@@ -1,7 +1,7 @@
import { toast } from "sonner"; import { toast } from "sonner";
import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action"; import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action";
import { ActionInputLookUpDictionary, ActionOutputLookUpDictionary } from "@/modules/dictionary"; import { ActionInputLookUpDictionary, ActionOutputLookUpDictionary } from "@/modules/dictionary/dictionary-action-dto";
import { TSharedItem } from "@/shared"; import { TSharedItem } from "@/shared/dictionary-type";
export async function performDictionaryLookup( export async function performDictionaryLookup(
options: ActionInputLookUpDictionary, options: ActionInputLookUpDictionary,

View File

@@ -93,4 +93,4 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
); );
}; };
export default FolderSelector; export { FolderSelector };

View File

@@ -208,4 +208,4 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
); );
}; };
export default Memorize; export { Memorize };

View File

@@ -1,11 +1,11 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { isNonNegativeInteger } from "@/utils/random"; import { isNonNegativeInteger } from "@/utils/random";
import FolderSelector from "./FolderSelector"; import { FolderSelector } from "./FolderSelector";
import Memorize from "./Memorize"; import { Memorize } from "./Memorize";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { actionGetFoldersWithTotalPairsByUserId, actionGetPairsByFolderId } from "@/modules/folder"; import { actionGetFoldersWithTotalPairsByUserId, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton";
export default async function MemorizePage({ export default async function MemorizePage({
searchParams, searchParams,

View File

@@ -1,4 +1,4 @@
export default function SubtitleDisplay({ subtitle }: { subtitle: string }) { export function SubtitleDisplay({ subtitle }: { subtitle: string }) {
const words = subtitle.match(/\b[\w']+(?:-[\w']+)*\b/g) || []; const words = subtitle.match(/\b[\w']+(?:-[\w']+)*\b/g) || [];
let i = 0; let i = 0;
return ( return (

View File

@@ -1,5 +1,5 @@
import { useState, useRef, forwardRef, useEffect, useCallback } from "react"; import { useState, useRef, forwardRef, useEffect, useCallback } from "react";
import SubtitleDisplay from "./SubtitleDisplay"; import { SubtitleDisplay } from "./SubtitleDisplay";
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import { getIndex, parseSrt, getNearistIndex } from "../subtitle"; import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -213,4 +213,4 @@ const VideoPanel = forwardRef<HTMLVideoElement, VideoPanelProps>(
VideoPanel.displayName = "VideoPanel"; VideoPanel.displayName = "VideoPanel";
export default VideoPanel; export { VideoPanel };

View File

@@ -7,7 +7,7 @@ interface FileInputComponentProps extends FileInputProps {
children: React.ReactNode; children: React.ReactNode;
} }
export default function FileInput({ accept, onFileSelect, disabled, className, children }: FileInputComponentProps) { export function FileInput({ accept, onFileSelect, disabled, className, children }: FileInputComponentProps) {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {

View File

@@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import { PlayButtonProps } from "../../types/player"; import { PlayButtonProps } from "../../types/player";
export default function PlayButton({ isPlaying, onToggle, disabled, className }: PlayButtonProps) { export function PlayButton({ isPlaying, onToggle, disabled, className }: PlayButtonProps) {
const t = useTranslations("srt_player"); const t = useTranslations("srt_player");
return ( return (

View File

@@ -3,7 +3,7 @@
import React from "react"; import React from "react";
import { SeekBarProps } from "../../types/player"; import { SeekBarProps } from "../../types/player";
export default function SeekBar({ value, max, onChange, disabled, className }: SeekBarProps) { export function SeekBar({ value, max, onChange, disabled, className }: SeekBarProps) {
const handleChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = parseInt(event.target.value); const newValue = parseInt(event.target.value);
onChange(newValue); onChange(newValue);

View File

@@ -5,7 +5,7 @@ import { LightButton } from "@/components/ui/buttons";
import { SpeedControlProps } from "../../types/player"; import { SpeedControlProps } from "../../types/player";
import { getPlaybackRateOptions, getPlaybackRateLabel } from "../../utils/timeUtils"; import { getPlaybackRateOptions, getPlaybackRateLabel } from "../../utils/timeUtils";
export default function SpeedControl({ playbackRate, onPlaybackRateChange, disabled, className }: SpeedControlProps) { export function SpeedControl({ playbackRate, onPlaybackRateChange, disabled, className }: SpeedControlProps) {
const speedOptions = getPlaybackRateOptions(); const speedOptions = getPlaybackRateOptions();
const handleSpeedChange = React.useCallback(() => { const handleSpeedChange = React.useCallback(() => {

View File

@@ -3,7 +3,7 @@
import React from "react"; import React from "react";
import { SubtitleTextProps } from "../../types/subtitle"; import { SubtitleTextProps } from "../../types/subtitle";
export default function SubtitleText({ text, onWordClick, style, className }: SubtitleTextProps) { export function SubtitleText({ text, onWordClick, style, className }: SubtitleTextProps) {
const handleWordClick = React.useCallback((word: string) => { const handleWordClick = React.useCallback((word: string) => {
onWordClick?.(word); onWordClick?.(word);
}, [onWordClick]); }, [onWordClick]);

View File

@@ -46,4 +46,4 @@ const VideoElement = forwardRef<HTMLVideoElement, VideoElementProps>(
VideoElement.displayName = "VideoElement"; VideoElement.displayName = "VideoElement";
export default VideoElement; export { VideoElement };

View File

@@ -5,10 +5,10 @@ import { useTranslations } from "next-intl";
import { ChevronLeft, ChevronRight, RotateCcw, Pause } from "lucide-react"; import { ChevronLeft, ChevronRight, RotateCcw, Pause } from "lucide-react";
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import { ControlBarProps } from "../../types/controls"; import { ControlBarProps } from "../../types/controls";
import PlayButton from "../atoms/PlayButton"; import { PlayButton } from "../atoms/PlayButton";
import SpeedControl from "../atoms/SpeedControl"; import { SpeedControl } from "../atoms/SpeedControl";
export default function ControlBar({ export function ControlBar({
isPlaying, isPlaying,
onPlayPause, onPlayPause,
onPrevious, onPrevious,

View File

@@ -2,9 +2,9 @@
import React from "react"; import React from "react";
import { SubtitleDisplayProps } from "../../types/subtitle"; import { SubtitleDisplayProps } from "../../types/subtitle";
import SubtitleText from "../atoms/SubtitleText"; import { SubtitleText } from "../atoms/SubtitleText";
export default function SubtitleArea({ subtitle, onWordClick, settings, className }: SubtitleDisplayProps) { export function SubtitleArea({ subtitle, onWordClick, settings, className }: SubtitleDisplayProps) {
const handleWordClick = React.useCallback((word: string) => { const handleWordClick = React.useCallback((word: string) => {
// 打开有道词典页面查询单词 // 打开有道词典页面查询单词
window.open( window.open(

View File

@@ -8,7 +8,7 @@ import { LightButton } from "@/components/ui/buttons";
import { FileUploadProps } from "../../types/controls"; import { FileUploadProps } from "../../types/controls";
import { useFileUpload } from "../../hooks/useFileUpload"; import { useFileUpload } from "../../hooks/useFileUpload";
export default function UploadZone({ onVideoUpload, onSubtitleUpload, className }: FileUploadProps) { export function UploadZone({ onVideoUpload, onSubtitleUpload, className }: FileUploadProps) {
const t = useTranslations("srt_player"); const t = useTranslations("srt_player");
const { uploadVideo, uploadSubtitle } = useFileUpload(); const { uploadVideo, uploadSubtitle } = useFileUpload();

View File

@@ -2,7 +2,7 @@
import React, { forwardRef } from "react"; import React, { forwardRef } from "react";
import { VideoElementProps } from "../../types/player"; import { VideoElementProps } from "../../types/player";
import VideoElement from "../atoms/VideoElement"; import { VideoElement } from "../atoms/VideoElement";
interface VideoPlayerComponentProps extends VideoElementProps { interface VideoPlayerComponentProps extends VideoElementProps {
children?: React.ReactNode; children?: React.ReactNode;
@@ -38,4 +38,4 @@ const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerComponentProps>(
VideoPlayer.displayName = "VideoPlayer"; VideoPlayer.displayName = "VideoPlayer";
export default VideoPlayer; export { VideoPlayer };

View File

@@ -9,11 +9,11 @@ import { useSubtitleSync } from "./hooks/useSubtitleSync";
import { useKeyboardShortcuts, createSrtPlayerShortcuts } from "./hooks/useKeyboardShortcuts"; import { useKeyboardShortcuts, createSrtPlayerShortcuts } from "./hooks/useKeyboardShortcuts";
import { useFileUpload } from "./hooks/useFileUpload"; import { useFileUpload } from "./hooks/useFileUpload";
import { loadSubtitle } from "./utils/subtitleParser"; import { loadSubtitle } from "./utils/subtitleParser";
import VideoPlayer from "./components/compounds/VideoPlayer"; import { VideoPlayer } from "./components/compounds/VideoPlayer";
import SubtitleArea from "./components/compounds/SubtitleArea"; import { SubtitleArea } from "./components/compounds/SubtitleArea";
import ControlBar from "./components/compounds/ControlBar"; import { ControlBar } from "./components/compounds/ControlBar";
import UploadZone from "./components/compounds/UploadZone"; import { UploadZone } from "./components/compounds/UploadZone";
import SeekBar from "./components/atoms/SeekBar"; import { SeekBar } from "./components/atoms/SeekBar";
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
export default function SrtPlayerPage() { export default function SrtPlayerPage() {

View File

@@ -7,7 +7,7 @@ import {
TextSpeakerItemSchema, TextSpeakerItemSchema,
} from "@/lib/interfaces"; } from "@/lib/interfaces";
import { IconClick } from "@/components/ui/buttons"; import { IconClick } from "@/components/ui/buttons";
import IMAGES from "@/config/images"; import { IMAGES } from "@/config/images";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators"; import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
@@ -50,7 +50,7 @@ interface SaveListProps {
show?: boolean; show?: boolean;
handleUse: (item: z.infer<typeof TextSpeakerItemSchema>) => void; handleUse: (item: z.infer<typeof TextSpeakerItemSchema>) => void;
} }
export default function SaveList({ show = false, handleUse }: SaveListProps) { export function SaveList({ show = false, handleUse }: SaveListProps) {
const t = useTranslations("text_speaker"); const t = useTranslations("text_speaker");
const { get: getFromLocalStorage, set: setIntoLocalStorage } = const { get: getFromLocalStorage, set: setIntoLocalStorage } =
getLocalStorageOperator<typeof TextSpeakerArraySchema>( getLocalStorageOperator<typeof TextSpeakerArraySchema>(

View File

@@ -2,7 +2,7 @@
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import { IconClick } from "@/components/ui/buttons"; import { IconClick } from "@/components/ui/buttons";
import IMAGES from "@/config/images"; import { IMAGES } from "@/config/images";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { import {
TextSpeakerArraySchema, TextSpeakerArraySchema,
@@ -10,13 +10,13 @@ import {
} from "@/lib/interfaces"; } from "@/lib/interfaces";
import { ChangeEvent, useEffect, useRef, useState } from "react"; import { ChangeEvent, useEffect, useRef, useState } from "react";
import z from "zod"; import z from "zod";
import SaveList from "./SaveList"; import { SaveList } from "./SaveList";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators"; import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
import { genIPA, genLanguage } from "@/modules/translator/translator-action"; import { genIPA, genLanguage } from "@/modules/translator/translator-action";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
import PageLayout from "@/components/ui/PageLayout"; import { PageLayout } from "@/components/ui/PageLayout";
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
export default function TextSpeakerPage() { export default function TextSpeakerPage() {

View File

@@ -2,21 +2,21 @@
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import { IconClick } from "@/components/ui/buttons"; import { IconClick } from "@/components/ui/buttons";
import IMAGES from "@/config/images"; import { IMAGES } from "@/config/images";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { translateText } from "@/modules/translator"; import { actionTranslateText } from "@/modules/translator/translator-action";
import { toast } from "sonner"; import { toast } from "sonner";
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
import { TranslateTextOutput } from "@/modules/translator"; import { TSharedTranslationResult } from "@/shared/translator-type";
export default function TranslatorPage() { export default function TranslatorPage() {
const t = useTranslations("translator"); const t = useTranslations("translator");
const taref = useRef<HTMLTextAreaElement>(null); const taref = useRef<HTMLTextAreaElement>(null);
const [targetLanguage, setTargetLanguage] = useState<string>("Chinese"); const [targetLanguage, setTargetLanguage] = useState<string>("Chinese");
const [translationResult, setTranslationResult] = useState<TranslateTextOutput | null>(null); const [translationResult, setTranslationResult] = useState<TSharedTranslationResult | null>(null);
const [needIpa, setNeedIpa] = useState(true); const [needIpa, setNeedIpa] = useState(true);
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [lastTranslation, setLastTranslation] = useState<{ const [lastTranslation, setLastTranslation] = useState<{
@@ -70,18 +70,22 @@ export default function TranslatorPage() {
lastTranslation?.targetLanguage === targetLanguage; lastTranslation?.targetLanguage === targetLanguage;
try { try {
const result = await translateText({ const result = await actionTranslateText({
sourceText, sourceText,
targetLanguage, targetLanguage,
forceRetranslate, forceRetranslate,
needIpa, needIpa,
}); });
setTranslationResult(result); if (result.success && result.data) {
setLastTranslation({ setTranslationResult(result.data);
sourceText, setLastTranslation({
targetLanguage, sourceText,
}); targetLanguage,
});
} else {
toast.error(result.message || "翻译失败,请重试");
}
} catch (error) { } catch (error) {
toast.error("翻译失败,请重试"); toast.error("翻译失败,请重试");
console.error("翻译错误:", error); console.error("翻译错误:", error);

View File

@@ -2,17 +2,17 @@
import { useState, useActionState, startTransition } from "react"; import { useState, useActionState, startTransition } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Container from "@/components/ui/Container"; import { Container } from "@/components/ui/Container";
import Input from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { signInAction, signUpAction, SignUpState } from "@/modules/auth"; import { signInAction, signUpAction, SignUpState } from "@/modules/auth/auth-action";
interface AuthFormProps { interface AuthFormProps {
redirectTo?: string; redirectTo?: string;
} }
export default function AuthForm({ redirectTo }: AuthFormProps) { export function AuthForm({ redirectTo }: AuthFormProps) {
const t = useTranslations("auth"); const t = useTranslations("auth");
const [mode, setMode] = useState<'signin' | 'signup'>('signin'); const [mode, setMode] = useState<'signin' | 'signup'>('signin');
const [clearSignIn, setClearSignIn] = useState(false); const [clearSignIn, setClearSignIn] = useState(false);

View File

@@ -1,7 +1,7 @@
import { auth } from "@/auth"; import { auth } from "@/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import AuthForm from "./AuthForm"; import { AuthForm } from "./AuthForm";
export default async function AuthPage( export default async function AuthPage(
props: { props: {

View File

@@ -11,10 +11,10 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { toast } from "sonner"; import { toast } from "sonner";
import PageLayout from "@/components/ui/PageLayout"; import { PageLayout } from "@/components/ui/PageLayout";
import PageHeader from "@/components/ui/PageHeader"; import { PageHeader } from "@/components/ui/PageHeader";
import CardList from "@/components/ui/CardList"; import { CardList } from "@/components/ui/CardList";
import { actionCreateFolder, actionDeleteFolderById, actionGetFoldersWithTotalPairsByUserId, actionRenameFolderById } from "@/modules/folder"; import { actionCreateFolder, actionDeleteFolderById, actionGetFoldersWithTotalPairsByUserId, actionRenameFolderById } from "@/modules/folder/folder-aciton";
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type"; import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
interface FolderProps { interface FolderProps {
@@ -97,7 +97,7 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
); );
}; };
export default function FoldersClient({ userId }: { userId: string; }) { export function FoldersClient({ userId }: { userId: string; }) {
const t = useTranslations("folders"); const t = useTranslations("folders");
const [folders, setFolders] = useState<TSharedFolderWithTotalPairs[]>( const [folders, setFolders] = useState<TSharedFolderWithTotalPairs[]>(
[], [],

View File

@@ -1,5 +1,5 @@
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import Input from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { LocaleSelector } from "@/components/ui/LocaleSelector"; import { LocaleSelector } from "@/components/ui/LocaleSelector";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
@@ -16,7 +16,7 @@ interface AddTextPairModalProps {
) => void; ) => void;
} }
export default function AddTextPairModal({ export function AddTextPairModal({
isOpen, isOpen,
onClose, onClose,
onAdd, onAdd,

View File

@@ -3,19 +3,19 @@
import { ArrowLeft, Plus } from "lucide-react"; import { ArrowLeft, Plus } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { redirect, useRouter } from "next/navigation"; import { redirect, useRouter } from "next/navigation";
import AddTextPairModal from "./AddTextPairModal"; import { AddTextPairModal } from "./AddTextPairModal";
import TextPairCard from "./TextPairCard"; import { TextPairCard } from "./TextPairCard";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import PageLayout from "@/components/ui/PageLayout"; import { PageLayout } from "@/components/ui/PageLayout";
import { GreenButton } from "@/components/ui/buttons"; import { GreenButton } from "@/components/ui/buttons";
import { IconButton } from "@/components/ui/buttons"; import { IconButton } from "@/components/ui/buttons";
import CardList from "@/components/ui/CardList"; import { CardList } from "@/components/ui/CardList";
import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder"; import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton";
import { TSharedPair } from "@/shared/folder-type"; import { TSharedPair } from "@/shared/folder-type";
import { toast } from "sonner"; import { toast } from "sonner";
export default function InFolder({ folderId }: { folderId: number; }) { export function InFolder({ folderId }: { folderId: number; }) {
const [textPairs, setTextPairs] = useState<TSharedPair[]>([]); const [textPairs, setTextPairs] = useState<TSharedPair[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [openAddModal, setAddModal] = useState(false); const [openAddModal, setAddModal] = useState(false);

View File

@@ -1,9 +1,10 @@
import { Edit, Trash2 } from "lucide-react"; import { Edit, Trash2 } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import UpdateTextPairModal from "./UpdateTextPairModal"; import { UpdateTextPairModal } from "./UpdateTextPairModal";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { TSharedPair } from "@/shared/folder-type"; import { TSharedPair } from "@/shared/folder-type";
import { actionUpdatePairById, ActionInputUpdatePairById } from "@/modules/folder"; import { actionUpdatePairById } from "@/modules/folder/folder-aciton";
import { ActionInputUpdatePairById } from "@/modules/folder/folder-action-dto";
import { toast } from "sonner"; import { toast } from "sonner";
interface TextPairCardProps { interface TextPairCardProps {
@@ -12,7 +13,7 @@ interface TextPairCardProps {
refreshTextPairs: () => void; refreshTextPairs: () => void;
} }
export default function TextPairCard({ export function TextPairCard({
textPair, textPair,
onDel, onDel,
refreshTextPairs, refreshTextPairs,

View File

@@ -1,11 +1,11 @@
import { LightButton } from "@/components/ui/buttons"; import { LightButton } from "@/components/ui/buttons";
import Input from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { LocaleSelector } from "@/components/ui/LocaleSelector"; import { LocaleSelector } from "@/components/ui/LocaleSelector";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { TSharedPair } from "@/shared/folder-type"; import { TSharedPair } from "@/shared/folder-type";
import { ActionInputUpdatePairById } from "@/modules/folder"; import { ActionInputUpdatePairById } from "@/modules/folder/folder-action-dto";
interface UpdateTextPairModalProps { interface UpdateTextPairModalProps {
isOpen: boolean; isOpen: boolean;
@@ -14,7 +14,7 @@ interface UpdateTextPairModalProps {
onUpdate: (id: number, tp: ActionInputUpdatePairById) => void; onUpdate: (id: number, tp: ActionInputUpdatePairById) => void;
} }
export default function UpdateTextPairModal({ export function UpdateTextPairModal({
isOpen, isOpen,
onClose, onClose,
onUpdate, onUpdate,

View File

@@ -1,9 +1,9 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import InFolder from "./InFolder"; import { InFolder } from "./InFolder";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { actionGetUserIdByFolderId } from "@/modules/folder"; import { actionGetUserIdByFolderId } from "@/modules/folder/folder-aciton";
export default async function FoldersPage({ export default async function FoldersPage({
params, params,
}: { }: {

View File

@@ -1,5 +1,5 @@
import { auth } from "@/auth"; import { auth } from "@/auth";
import FoldersClient from "./FoldersClient"; import { FoldersClient } from "./FoldersClient";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { headers } from "next/headers"; import { headers } from "next/headers";

View File

@@ -5,7 +5,7 @@ import { authClient } from "@/lib/auth-client";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export default function LogoutButton() { export function LogoutButton() {
const t = useTranslations("profile"); const t = useTranslations("profile");
const router = useRouter(); const router = useRouter();
return <LightButton onClick={async () => { return <LightButton onClick={async () => {

View File

@@ -1,11 +1,11 @@
import Image from "next/image"; import Image from "next/image";
import PageLayout from "@/components/ui/PageLayout"; import { PageLayout } from "@/components/ui/PageLayout";
import PageHeader from "@/components/ui/PageHeader"; import { PageHeader } from "@/components/ui/PageHeader";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { headers } from "next/headers"; import { headers } from "next/headers";
import LogoutButton from "./LogoutButton"; import { LogoutButton } from "./LogoutButton";
export default async function ProfilePage() { export default async function ProfilePage() {
const t = await getTranslations("profile"); const t = await getTranslations("profile");

View File

@@ -1,7 +1,7 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma"; import { prismaAdapter } from "better-auth/adapters/prisma";
import { nextCookies } from "better-auth/next-js"; import { nextCookies } from "better-auth/next-js";
import prisma from "./lib/db"; import { prisma } from "./lib/db";
export const auth = betterAuth({ export const auth = betterAuth({
database: prismaAdapter(prisma, { database: prismaAdapter(prisma, {

View File

@@ -1,10 +1,10 @@
"use client"; "use client";
import IMAGES from "@/config/images"; import { GhostButton } from "./ui/buttons";
import { IconClick, GhostButton } from "./ui/buttons";
import { useState } from "react"; import { useState } from "react";
import { Languages } from "lucide-react";
export default function LanguageSettings() { export function LanguageSettings() {
const [showLanguageMenu, setShowLanguageMenu] = useState(false); const [showLanguageMenu, setShowLanguageMenu] = useState(false);
const handleLanguageClick = () => { const handleLanguageClick = () => {
setShowLanguageMenu((prev) => !prev); setShowLanguageMenu((prev) => !prev);
@@ -15,13 +15,7 @@ export default function LanguageSettings() {
}; };
return ( return (
<> <>
<IconClick <Languages />
src={IMAGES.language_white}
alt="language"
disableOnHoverBgChange={true}
onClick={handleLanguageClick}
size={40}
></IconClick>
<div className="relative"> <div className="relative">
{showLanguageMenu && ( {showLanguageMenu && (
<div> <div>

View File

@@ -1,7 +1,7 @@
import Image from "next/image"; import Image from "next/image";
import IMAGES from "@/config/images"; import { IMAGES } from "@/config/images";
import { Folder, Home, User } from "lucide-react"; import { Folder, Home, User } from "lucide-react";
import LanguageSettings from "../LanguageSettings"; import { LanguageSettings } from "../LanguageSettings";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";

View File

@@ -34,7 +34,7 @@ export interface ButtonProps {
href?: string; href?: string;
} }
export default function Button({ export function Button({
variant = "secondary", variant = "secondary",
size = "md", size = "md",
selected = false, selected = false,

View File

@@ -21,7 +21,7 @@ interface CardListProps {
className?: string; className?: string;
} }
export default function CardList({ children, className = "" }: CardListProps) { export function CardList({ children, className = "" }: CardListProps) {
return ( return (
<div className={`max-h-96 overflow-y-auto rounded-xl border border-gray-200 overflow-hidden ${className}`}> <div className={`max-h-96 overflow-y-auto rounded-xl border border-gray-200 overflow-hidden ${className}`}>
{children} {children}

View File

@@ -3,7 +3,7 @@ interface ContainerProps {
className?: string; className?: string;
} }
export default function Container({ children, className }: ContainerProps) { export function Container({ children, className }: ContainerProps) {
return ( return (
<div <div
className={`w-full max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl ${className}`} className={`w-full max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl ${className}`}

View File

@@ -7,7 +7,7 @@ interface Props {
defaultValue?: string; defaultValue?: string;
} }
export default function Input({ export function Input({
ref, ref,
placeholder = "", placeholder = "",
type = "text", type = "text",

View File

@@ -15,7 +15,7 @@ interface PageHeaderProps {
subtitle?: string; subtitle?: string;
} }
export default function PageHeader({ title, subtitle }: PageHeaderProps) { export function PageHeader({ title, subtitle }: PageHeaderProps) {
return ( return (
<div className="mb-6"> <div className="mb-6">
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-2"> <h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-2">

View File

@@ -20,7 +20,7 @@ interface PageLayoutProps {
className?: string; className?: string;
} }
export default function PageLayout({ children, className = "" }: PageLayoutProps) { export function PageLayout({ children, className = "" }: PageLayoutProps) {
return ( return (
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8 ${className}`}> <div className={`min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8 ${className}`}>
<div className="w-full max-w-2xl"> <div className="w-full max-w-2xl">

View File

@@ -1,7 +1,7 @@
// 向后兼容的按钮组件包装器 // 向后兼容的按钮组件包装器
// 这些组件将新 Button 组件包装,以保持向后兼容 // 这些组件将新 Button 组件包装,以保持向后兼容
import Button from "../Button"; import { Button } from "../Button";
// LightButton: 次要按钮,支持 selected 状态 // LightButton: 次要按钮,支持 selected 状态
export const LightButton = (props: any) => <Button variant="secondary" {...props} />; export const LightButton = (props: any) => <Button variant="secondary" {...props} />;

View File

@@ -21,4 +21,4 @@ const IMAGES = {
github_mark_white: "/images/github-mark/github-mark-white.svg", github_mark_white: "/images/github-mark/github-mark-white.svg",
}; };
export default IMAGES; export { IMAGES };

View File

@@ -1,6 +1,6 @@
import { useRef } from "react"; import { useRef } from "react";
export default function useFileUpload(callback: (file: File) => void) { export function useFileUpload(callback: (file: File) => void) {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const upload = (type: string = "*") => { const upload = (type: string = "*") => {
const input = inputRef.current; const input = inputRef.current;

View File

@@ -1,18 +0,0 @@
/**
* 词典查询模块 - 多阶段 LLM 调用架构
*
* 将词典查询拆分为 4 个独立的 LLM 调用阶段,每个阶段都有代码层面的数据验证
* 只要有一环失败,直接返回错误
*/
// 导出主编排器
export { executeDictionaryLookup } from "./orchestrator";
// 导出各阶段的独立函数(可选,用于调试或单独使用)
export { analyzeInput } from "./stage1-inputAnalysis";
export { determineSemanticMapping } from "./stage2-semanticMapping";
export { generateStandardForm } from "./stage3-standardForm";
export { generateEntries } from "./stage4-entriesGeneration";
// 导出类型定义
export * from "./types";

View File

@@ -0,0 +1,189 @@
import { getAnswer } from "../zhipu";
import { parseAIGeneratedJSON } from "@/utils/json";
import { LanguageDetectionResult, TranslationLLMResponse } from "./types";
async function detectLanguage(text: string): Promise<LanguageDetectionResult> {
const prompt = `
你是一个语言识别专家。分析用户输入并返回 JSON 结果。
用户输入位于 <text> 标签内:
<text>${text}</text>
你的任务是:
1. 识别输入文本的语言
2. 评估识别置信度
返回 JSON 格式:
{
"sourceLanguage": "检测到的语言名称(如 English、中文、日本語、Français、Deutsch等",
"confidence": "high/medium/low"
}
只返回 JSON不要任何其他文字。
`.trim();
try {
const result = await getAnswer([
{
role: "system",
content: "你是一个语言识别专家,只返回 JSON 格式的分析结果。",
},
{
role: "user",
content: prompt,
},
]).then(parseAIGeneratedJSON<LanguageDetectionResult>);
if (typeof result.sourceLanguage !== "string" || !result.sourceLanguage) {
throw new Error("Invalid source language detected");
}
return result;
} catch (error) {
console.error("Language detection failed:", error);
throw new Error("Failed to detect source language");
}
}
async function performTranslation(
sourceText: string,
sourceLanguage: string,
targetLanguage: string
): Promise<string> {
const prompt = `
你是一个专业翻译。将文本翻译成目标语言。
源文本位于 <source_text> 标签内:
<source_text>${sourceText}</source_text>
源语言:${sourceLanguage}
目标语言:${targetLanguage}
要求:
1. 保持原意准确
2. 符合目标语言的表达习惯
3. 如果是成语、俗语或文化特定表达,在目标语言中寻找对应表达
4. 只返回翻译结果,不要任何解释或说明
请直接返回翻译结果:
`.trim();
try {
const result = await getAnswer([
{
role: "system",
content: "你是一个专业翻译,只返回翻译结果。",
},
{
role: "user",
content: prompt,
},
]);
return result.trim();
} catch (error) {
console.error("Translation failed:", error);
throw new Error("Translation failed");
}
}
async function generateIPA(
text: string,
language: string
): Promise<string> {
const prompt = `
你是一个语音学专家。为文本生成国际音标IPA标注。
文本位于 <text> 标签内:
<text>${text}</text>
语言:${language}
要求:
1. 生成准确的国际音标IPA标注
2. 使用标准的 IPA 符号
3. 只返回 IPA 标注,不要任何其他文字
请直接返回 IPA 标注:
`.trim();
try {
const result = await getAnswer([
{
role: "system",
content: "你是一个语音学专家,只返回 IPA 标注。",
},
{
role: "user",
content: prompt,
},
]);
return result.trim();
} catch (error) {
console.error("IPA generation failed:", error);
return "";
}
}
export async function executeTranslation(
sourceText: string,
targetLanguage: string,
needIpa: boolean
): Promise<TranslationLLMResponse> {
try {
console.log("[翻译] 开始翻译流程...");
console.log("[翻译] 源文本:", sourceText);
console.log("[翻译] 目标语言:", targetLanguage);
console.log("[翻译] 需要 IPA:", needIpa);
// Stage 1: Detect source language
console.log("[阶段1] 检测源语言...");
const detectionResult = await detectLanguage(sourceText);
console.log("[阶段1] 检测结果:", detectionResult);
// Stage 2: Perform translation
console.log("[阶段2] 执行翻译...");
const translatedText = await performTranslation(
sourceText,
detectionResult.sourceLanguage,
targetLanguage
);
console.log("[阶段2] 翻译完成:", translatedText);
// Validate translation result
if (!translatedText) {
throw new Error("Translation result is empty");
}
// Stage 3 (Optional): Generate IPA
let sourceIpa: string | undefined;
let targetIpa: string | undefined;
if (needIpa) {
console.log("[阶段3] 生成 IPA...");
sourceIpa = await generateIPA(sourceText, detectionResult.sourceLanguage);
console.log("[阶段3] 源文本 IPA:", sourceIpa);
targetIpa = await generateIPA(translatedText, targetLanguage);
console.log("[阶段3] 目标文本 IPA:", targetIpa);
}
// Assemble final result
const finalResult: TranslationLLMResponse = {
sourceText,
translatedText,
sourceLanguage: detectionResult.sourceLanguage,
targetLanguage,
sourceIpa,
targetIpa,
};
console.log("[完成] 翻译流程成功");
return finalResult;
} catch (error) {
console.error("[错误] 翻译失败:", error);
const errorMessage = error instanceof Error ? error.message : "未知错误";
throw new Error(errorMessage);
}
}

View File

@@ -0,0 +1,13 @@
export interface LanguageDetectionResult {
sourceLanguage: string;
confidence: "high" | "medium" | "low";
}
export interface TranslationLLMResponse {
sourceText: string;
translatedText: string;
sourceLanguage: string;
targetLanguage: string;
sourceIpa?: string;
targetIpa?: string;
}

View File

@@ -0,0 +1,44 @@
import { z } from "zod";
interface LocalStorageOperator<T> {
get: () => T;
set: (value: T) => void;
}
export function getLocalStorageOperator<T extends z.ZodType>(
key: string,
schema: T
): LocalStorageOperator<z.infer<T>> {
const get = (): z.infer<T> => {
if (typeof window === "undefined") {
return [] as unknown as z.infer<T>;
}
try {
const item = localStorage.getItem(key);
if (item === null) {
return [] as unknown as z.infer<T>;
}
const parsed = JSON.parse(item);
return schema.parse(parsed);
} catch (error) {
console.error(`Error reading from localStorage key "${key}":`, error);
return [] as unknown as z.infer<T>;
}
};
const set = (value: z.infer<T>): void => {
if (typeof window === "undefined") {
return;
}
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error writing to localStorage key "${key}":`, error);
}
};
return { get, set };
}

View File

@@ -7,4 +7,4 @@ const adapter = new PrismaPg({
const prisma = new PrismaClient({ const prisma = new PrismaClient({
adapter: adapter, adapter: adapter,
}); });
export default prisma; export { prisma };

19
src/lib/interfaces.ts Normal file
View File

@@ -0,0 +1,19 @@
import z from "zod";
// Text Speaker types
export const TextSpeakerItemSchema = z.object({
text: z.string(),
language: z.string(),
ipa: z.string().optional(),
});
export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema);
// Alphabet types
export type SupportedAlphabets = "japanese" | "english" | "uyghur" | "esperanto";
export interface Letter {
letter: string;
letter_sound_ipa: string;
roman_letter?: string;
}

25
src/lib/logger.ts Normal file
View File

@@ -0,0 +1,25 @@
class Logger {
error(message: string, error?: unknown): void {
if (error instanceof Error) {
console.error(`[ERROR] ${message}:`, error.message, error.stack);
} else {
console.error(`[ERROR] ${message}:`, error);
}
}
warn(message: string, ...args: unknown[]): void {
console.warn(`[WARN] ${message}`, ...args);
}
info(message: string, ...args: unknown[]): void {
console.info(`[INFO] ${message}`, ...args);
}
debug(message: string, ...args: unknown[]): void {
if (process.env.NODE_ENV === "development") {
console.debug(`[DEBUG] ${message}`, ...args);
}
}
}
export const logger = new Logger();

6
src/lib/theme/colors.ts Normal file
View File

@@ -0,0 +1,6 @@
export const COLORS = {
primary: "#35786f",
secondary: "#2d5f58",
ghost: "#f3f4f6",
icon: "#6b7280",
} as const;

View File

@@ -1 +0,0 @@
export * from './auth-action';

View File

@@ -1,4 +1,4 @@
import { TSharedItem } from "@/shared"; import { TSharedItem } from "@/shared/dictionary-type";
import { LENGTH_MAX_DICTIONARY_TEXT, LENGTH_MAX_LANGUAGE, LENGTH_MIN_DICTIONARY_TEXT, LENGTH_MIN_LANGUAGE } from "@/shared/constant"; import { LENGTH_MAX_DICTIONARY_TEXT, LENGTH_MAX_LANGUAGE, LENGTH_MIN_DICTIONARY_TEXT, LENGTH_MIN_LANGUAGE } from "@/shared/constant";
import { generateValidator } from "@/utils/validate"; import { generateValidator } from "@/utils/validate";
import z from "zod"; import z from "zod";

View File

@@ -1,4 +1,4 @@
import { TSharedItem } from "@/shared"; import { TSharedItem } from "@/shared/dictionary-type";
export type RepoInputCreateDictionaryLookUp = { export type RepoInputCreateDictionaryLookUp = {
userId?: string; userId?: string;

View File

@@ -7,7 +7,7 @@ import {
RepoInputSelectLastLookUpResult, RepoInputSelectLastLookUpResult,
RepoOutputSelectLastLookUpResult, RepoOutputSelectLastLookUpResult,
} from "./dictionary-repository-dto"; } from "./dictionary-repository-dto";
import prisma from "@/lib/db"; import { prisma } from "@/lib/db";
export async function repoSelectLastLookUpResult(dto: RepoInputSelectLastLookUpResult): Promise<RepoOutputSelectLastLookUpResult> { export async function repoSelectLastLookUpResult(dto: RepoInputSelectLastLookUpResult): Promise<RepoOutputSelectLastLookUpResult> {
const result = await prisma.dictionaryLookUp.findFirst({ const result = await prisma.dictionaryLookUp.findFirst({

View File

@@ -1,4 +1,4 @@
import { TSharedItem } from "@/shared"; import { TSharedItem } from "@/shared/dictionary-type";
export type ServiceInputLookUp = { export type ServiceInputLookUp = {
text: string, text: string,

View File

@@ -1,4 +1,4 @@
import { executeDictionaryLookup } from "@/lib/bigmodel/dictionary"; import { executeDictionaryLookup } from "@/lib/bigmodel/dictionary/orchestrator";
import { repoCreateLookUp, repoCreateLookUpWithItemAndEntries, repoSelectLastLookUpResult } from "./dictionary-repository"; import { repoCreateLookUp, repoCreateLookUpWithItemAndEntries, repoSelectLastLookUpResult } from "./dictionary-repository";
import { ServiceInputLookUp } from "./dictionary-service-dto"; import { ServiceInputLookUp } from "./dictionary-service-dto";

View File

@@ -1,2 +0,0 @@
export * from "./dictionary-action";
export * from "./dictionary-action-dto";

View File

@@ -1,4 +1,4 @@
import prisma from "@/lib/db"; import { prisma } from "@/lib/db";
import { RepoInputCreateFolder, RepoInputCreatePair, RepoInputUpdatePair } from "./folder-repository-dto"; import { RepoInputCreateFolder, RepoInputCreatePair, RepoInputUpdatePair } from "./folder-repository-dto";
export async function repoCreatePair(data: RepoInputCreatePair) { export async function repoCreatePair(data: RepoInputCreatePair) {

View File

@@ -1,2 +0,0 @@
export * from './folder-aciton';
export * from './folder-action-dto';

View File

@@ -1,2 +0,0 @@
export * from './translator-action';
export * from './translator-action-dto';

View File

@@ -1,40 +1,27 @@
import { TSharedTranslationResult } from "@/shared/translator-type";
import {
LENGTH_MAX_LANGUAGE,
LENGTH_MIN_LANGUAGE,
LENGTH_MAX_TRANSLATOR_TEXT,
LENGTH_MIN_TRANSLATOR_TEXT,
} from "@/shared/constant";
import { generateValidator } from "@/utils/validate";
import z from "zod";
export interface CreateTranslationHistoryInput { const schemaActionInputTranslateText = z.object({
userId?: string; sourceText: z.string().min(LENGTH_MIN_TRANSLATOR_TEXT).max(LENGTH_MAX_TRANSLATOR_TEXT),
sourceText: string; targetLanguage: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE),
sourceLanguage: string; forceRetranslate: z.boolean().optional().default(false),
targetLanguage: string; needIpa: z.boolean().optional().default(true),
translatedText: string; userId: z.string().optional(),
sourceIpa?: string; });
targetIpa?: string;
}
export interface TranslationHistoryQuery { export type ActionInputTranslateText = z.infer<typeof schemaActionInputTranslateText>;
sourceText: string;
targetLanguage: string;
}
export interface TranslateTextInput { export const validateActionInputTranslateText = generateValidator(schemaActionInputTranslateText);
sourceText: string;
targetLanguage: string;
forceRetranslate?: boolean; // 默认 false
needIpa?: boolean; // 默认 true
userId?: string; // 可选用户 ID
}
export interface TranslateTextOutput { export type ActionOutputTranslateText = {
sourceText: string; message: string;
translatedText: string; success: boolean;
sourceLanguage: string; data?: TSharedTranslationResult;
targetLanguage: string; };
sourceIpa: string; // 如果 needIpa=false返回空字符串
targetIpa: string; // 如果 needIpa=false返回空字符串
}
export interface TranslationLLMResponse {
translatedText: string;
sourceLanguage: string;
targetLanguage: string;
sourceIpa?: string; // 可选,根据 needIpa 决定
targetIpa?: string; // 可选,根据 needIpa 决定
}

View File

@@ -1 +1,103 @@
"use server"; "use server";
import {
ActionInputTranslateText,
ActionOutputTranslateText,
validateActionInputTranslateText,
} from "./translator-action-dto";
import { ValidateError } from "@/lib/errors";
import { serviceTranslateText } from "./translator-service";
import { getAnswer } from "@/lib/bigmodel/zhipu";
export const actionTranslateText = async (
dto: ActionInputTranslateText
): Promise<ActionOutputTranslateText> => {
try {
return {
message: "success",
success: true,
data: await serviceTranslateText(validateActionInputTranslateText(dto)),
};
} catch (e) {
if (e instanceof ValidateError) {
return {
success: false,
message: e.message,
};
}
console.log(e);
return {
success: false,
message: "Unknown error occurred.",
};
}
};
/**
* @deprecated 保留此函数以支持旧代码text-speaker 功能)
*/
export const genIPA = async (text: string) => {
return (
"[" +
(
await getAnswer(
`
<text>${text}</text>
请生成以上文本的严式国际音标
然后直接发给我
不要附带任何说明
不要擅自增减符号
不许用"/"或者"[]"包裹
`.trim(),
)
)
.replaceAll("[", "")
.replaceAll("]", "") +
"]"
);
};
/**
* @deprecated 保留此函数以支持旧代码text-speaker 功能)
*/
export const genLanguage = async (text: string) => {
const language = await getAnswer([
{
role: "system",
content: `
你是一个语言检测工具。请识别文本的语言并返回语言名称。
返回语言的标准英文名称,例如:
- 中文: Chinese
- 英语: English
- 日语: Japanese
- 韩语: Korean
- 法语: French
- 德语: German
- 意大利语: Italian
- 葡萄牙语: Portuguese
- 西班牙语: Spanish
- 俄语: Russian
- 阿拉伯语: Arabic
- 印地语: Hindi
- 泰语: Thai
- 越南语: Vietnamese
- 等等...
如果无法识别语言,返回 "Unknown"
规则:
1. 只返回语言的标准英文名称
2. 首字母大写,其余小写
3. 不要附带任何说明
4. 不要擅自增减符号
`.trim()
},
{
role: "user",
content: `<text>${text}</text>`
}
]);
return language.trim();
};

View File

@@ -0,0 +1,23 @@
export type RepoInputSelectLatestTranslation = {
sourceText: string;
targetLanguage: string;
};
export type RepoOutputSelectLatestTranslation = {
id: number;
translatedText: string;
sourceLanguage: string;
targetLanguage: string;
sourceIpa: string | null;
targetIpa: string | null;
} | null;
export type RepoInputCreateTranslationHistory = {
userId?: string;
sourceText: string;
sourceLanguage: string;
targetLanguage: string;
translatedText: string;
sourceIpa?: string;
targetIpa?: string;
};

View File

@@ -1,31 +1,41 @@
"use server"; import {
RepoInputCreateTranslationHistory,
RepoInputSelectLatestTranslation,
RepoOutputSelectLatestTranslation,
} from "./translator-repository-dto";
import { prisma } from "@/lib/db";
import { CreateTranslationHistoryInput, TranslationHistoryQuery } from "./translator-action-dto"; export async function repoSelectLatestTranslation(
import prisma from "@/lib/db"; dto: RepoInputSelectLatestTranslation
): Promise<RepoOutputSelectLatestTranslation> {
const result = await prisma.translationHistory.findFirst({
where: {
sourceText: dto.sourceText,
targetLanguage: dto.targetLanguage,
},
orderBy: {
createdAt: "desc",
},
});
/** if (!result) {
* 创建翻译历史记录 return null;
*/ }
export async function repoCreateTranslationHistory(data: CreateTranslationHistoryInput) {
return prisma.translationHistory.create({ return {
data: data, id: result.id,
}); translatedText: result.translatedText,
sourceLanguage: result.sourceLanguage,
targetLanguage: result.targetLanguage,
sourceIpa: result.sourceIpa,
targetIpa: result.targetIpa,
};
} }
/** export async function repoCreateTranslationHistory(
* 查询最新的翻译记录 data: RepoInputCreateTranslationHistory
* @param sourceText 源文本 ) {
* @param targetLanguage 目标语言 return await prisma.translationHistory.create({
* @returns 最新的翻译记录,如果不存在则返回 null data: data,
*/ });
export async function repoSelectLatestTranslation(query: TranslationHistoryQuery) {
return prisma.translationHistory.findFirst({
where: {
sourceText: query.sourceText,
targetLanguage: query.targetLanguage,
},
orderBy: {
createdAt: 'desc',
},
});
} }

View File

@@ -0,0 +1,11 @@
import { TSharedTranslationResult } from "@/shared/translator-type";
export type ServiceInputTranslateText = {
sourceText: string;
targetLanguage: string;
forceRetranslate: boolean;
needIpa: boolean;
userId?: string;
};
export type ServiceOutputTranslateText = TSharedTranslationResult;

View File

@@ -0,0 +1,69 @@
import { executeTranslation } from "@/lib/bigmodel/translator/orchestrator";
import { repoCreateTranslationHistory, repoSelectLatestTranslation } from "./translator-repository";
import { ServiceInputTranslateText, ServiceOutputTranslateText } from "./translator-service-dto";
export const serviceTranslateText = async (
dto: ServiceInputTranslateText
): Promise<ServiceOutputTranslateText> => {
const { sourceText, targetLanguage, forceRetranslate, needIpa, userId } = dto;
// Check for existing translation
const lastTranslation = await repoSelectLatestTranslation({
sourceText,
targetLanguage,
});
if (forceRetranslate || !lastTranslation) {
// Call AI for translation
const response = await executeTranslation(
sourceText,
targetLanguage,
needIpa
);
// Save translation history asynchronously (don't block response)
repoCreateTranslationHistory({
userId,
sourceText,
sourceLanguage: response.sourceLanguage,
targetLanguage: response.targetLanguage,
translatedText: response.translatedText,
sourceIpa: needIpa ? response.sourceIpa : undefined,
targetIpa: needIpa ? response.targetIpa : undefined,
}).catch((error) => {
console.error("Failed to save translation data:", error);
});
return {
sourceText: response.sourceText,
translatedText: response.translatedText,
sourceLanguage: response.sourceLanguage,
targetLanguage: response.targetLanguage,
sourceIpa: response.sourceIpa || "",
targetIpa: response.targetIpa || "",
};
} else {
// Return cached translation
// Still save a history record for analytics
repoCreateTranslationHistory({
userId,
sourceText,
sourceLanguage: lastTranslation.sourceLanguage,
targetLanguage: lastTranslation.targetLanguage,
translatedText: lastTranslation.translatedText,
sourceIpa: lastTranslation.sourceIpa || undefined,
targetIpa: lastTranslation.targetIpa || undefined,
}).catch((error) => {
console.error("Failed to save translation data:", error);
});
return {
sourceText,
translatedText: lastTranslation.translatedText,
sourceLanguage: lastTranslation.sourceLanguage,
targetLanguage: lastTranslation.targetLanguage,
sourceIpa: lastTranslation.sourceIpa || "",
targetIpa: lastTranslation.targetIpa || "",
};
}
};

View File

@@ -11,4 +11,7 @@ export const LENGTH_MAX_IPA = 150;
export const LENGTH_MIN_IPA = 1; export const LENGTH_MIN_IPA = 1;
export const LENGTH_MAX_FOLDER_NAME = 20; export const LENGTH_MAX_FOLDER_NAME = 20;
export const LENGTH_MIN_FOLDER_NAME = 1; export const LENGTH_MIN_FOLDER_NAME = 1;
export const LENGTH_MAX_TRANSLATOR_TEXT = 1000;
export const LENGTH_MIN_TRANSLATOR_TEXT = 1;

View File

@@ -1 +0,0 @@
export * from './dictionary-type';

View File

@@ -0,0 +1,8 @@
export type TSharedTranslationResult = {
sourceText: string;
translatedText: string;
sourceLanguage: string;
targetLanguage: string;
sourceIpa: string;
targetIpa: string;
};