Compare commits
3 Commits
fa6301538b
...
0af99b6b70
| Author | SHA1 | Date | |
|---|---|---|---|
| 0af99b6b70 | |||
| eaf97b8279 | |||
| 76749549ff |
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TSharedEntry } from "@/shared";
|
import { TSharedEntry } from "@/shared/dictionary-type";
|
||||||
|
|
||||||
interface DictionaryEntryProps {
|
interface DictionaryEntryProps {
|
||||||
entry: TSharedEntry;
|
entry: TSharedEntry;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -93,4 +93,4 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FolderSelector;
|
export { FolderSelector };
|
||||||
|
|||||||
@@ -208,4 +208,4 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Memorize;
|
export { Memorize };
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -46,4 +46,4 @@ const VideoElement = forwardRef<HTMLVideoElement, VideoElementProps>(
|
|||||||
|
|
||||||
VideoElement.displayName = "VideoElement";
|
VideoElement.displayName = "VideoElement";
|
||||||
|
|
||||||
export default VideoElement;
|
export { VideoElement };
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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[]>(
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}`}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
|
||||||
189
src/lib/bigmodel/translator/orchestrator.ts
Normal file
189
src/lib/bigmodel/translator/orchestrator.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/lib/bigmodel/translator/types.ts
Normal file
13
src/lib/bigmodel/translator/types.ts
Normal 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;
|
||||||
|
}
|
||||||
44
src/lib/browser/localStorageOperators.ts
Normal file
44
src/lib/browser/localStorageOperators.ts
Normal 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 };
|
||||||
|
}
|
||||||
@@ -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
19
src/lib/interfaces.ts
Normal 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
25
src/lib/logger.ts
Normal 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
6
src/lib/theme/colors.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const COLORS = {
|
||||||
|
primary: "#35786f",
|
||||||
|
secondary: "#2d5f58",
|
||||||
|
ghost: "#f3f4f6",
|
||||||
|
icon: "#6b7280",
|
||||||
|
} as const;
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './auth-action';
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TSharedItem } from "@/shared";
|
import { TSharedItem } from "@/shared/dictionary-type";
|
||||||
|
|
||||||
export type RepoInputCreateDictionaryLookUp = {
|
export type RepoInputCreateDictionaryLookUp = {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TSharedItem } from "@/shared";
|
import { TSharedItem } from "@/shared/dictionary-type";
|
||||||
|
|
||||||
export type ServiceInputLookUp = {
|
export type ServiceInputLookUp = {
|
||||||
text: string,
|
text: string,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./dictionary-action";
|
|
||||||
export * from "./dictionary-action-dto";
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './folder-aciton';
|
|
||||||
export * from './folder-action-dto';
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './translator-action';
|
|
||||||
export * from './translator-action-dto';
|
|
||||||
@@ -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 决定
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,3 +12,6 @@ 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;
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './dictionary-type';
|
|
||||||
8
src/shared/translator-type.ts
Normal file
8
src/shared/translator-type.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export type TSharedTranslationResult = {
|
||||||
|
sourceText: string;
|
||||||
|
translatedText: string;
|
||||||
|
sourceLanguage: string;
|
||||||
|
targetLanguage: string;
|
||||||
|
sourceIpa: string;
|
||||||
|
targetIpa: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user