...
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-11-16 12:04:09 +08:00
parent 72c6791d93
commit 0e3d41829c
19 changed files with 215 additions and 312 deletions

View File

@@ -1,8 +1,12 @@
// LLM
ZHIPU_API_KEY= ZHIPU_API_KEY=
AUTH_SECRET= ZHIPU_MODEL_NAME=
// Auth
AUTH_SECRET=
GITHUB_CLIENT_ID= GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET= GITHUB_CLIENT_SECRET=
NEXTAUTH_URL= NEXTAUTH_URL=
// Database
DATABASE_URL= DATABASE_URL=

View File

@@ -5,7 +5,7 @@ FROM node:23-alpine AS base
# Install dependencies only when needed # Install dependencies only when needed
FROM base AS deps FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat openssl
WORKDIR /app WORKDIR /app
# Install dependencies based on the preferred package manager # Install dependencies based on the preferred package manager
@@ -29,10 +29,17 @@ COPY . .
# Uncomment the following line in case you want to disable telemetry during the build. # Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
# RUN \
# if [ -f yarn.lock ]; then yarn run build; \
# elif [ -f package-lock.json ]; then npm run build; \
# elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
# else echo "Lockfile not found." && exit 1; \
# fi
RUN pnpx prisma generate
RUN \ RUN \
if [ -f yarn.lock ]; then yarn run build; \ if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \ else echo "Lockfile not found." && exit 1; \
fi fi

View File

@@ -19,6 +19,7 @@
"next-intl": "^4.5.2", "next-intl": "^4.5.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"sonner": "^2.0.7",
"unstorage": "^1.17.2", "unstorage": "^1.17.2",
"zod": "^3.25.76" "zod": "^3.25.76"
}, },

14
pnpm-lock.yaml generated
View File

@@ -35,6 +35,9 @@ importers:
react-dom: react-dom:
specifier: 19.1.0 specifier: 19.1.0
version: 19.1.0(react@19.1.0) version: 19.1.0(react@19.1.0)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
unstorage: unstorage:
specifier: ^1.17.2 specifier: ^1.17.2
version: 1.17.2 version: 1.17.2
@@ -4040,6 +4043,12 @@ packages:
resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -9954,6 +9963,11 @@ snapshots:
slugify@1.6.6: slugify@1.6.6:
optional: true optional: true
sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
source-map-support@0.5.21: source-map-support@0.5.21:

View File

@@ -36,7 +36,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
<Folder /> <Folder />
<div className="flex-1 flex gap-2"> <div className="flex-1 flex gap-2">
<span className="group-hover:text-blue-500"> <span className="group-hover:text-blue-500">
{folder.name} {folder.id}. {folder.name}
</span> </span>
<span>({folder.total_pairs})</span> <span>({folder.total_pairs})</span>
</div> </div>

View File

@@ -3,7 +3,6 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { import {
getFoldersByOwner,
getFoldersWithTotalPairsByOwner, getFoldersWithTotalPairsByOwner,
getOwnerByFolderId, getOwnerByFolderId,
} from "@/lib/services/folderService"; } from "@/lib/services/folderService";

View File

@@ -0,0 +1,78 @@
import LightButton from "@/components/buttons/LightButton";
import Container from "@/components/cards/Container";
import { TranslationHistorySchema } from "@/lib/interfaces";
import { useSession } from "next-auth/react";
import { Dispatch, useEffect, useState } from "react";
import z from "zod";
import { folder } from "../../../../generated/prisma/browser";
import { getFoldersByOwner } from "@/lib/services/folderService";
import { Folder } from "lucide-react";
import { createTextPair } from "@/lib/services/textPairService";
import { toast } from "sonner";
interface AddToFolderProps {
item: z.infer<typeof TranslationHistorySchema>;
setShow: Dispatch<React.SetStateAction<boolean>>;
}
const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
const session = useSession();
const [folders, setFolders] = useState<folder[]>([]);
useEffect(() => {
const username = session.data!.user!.name as string;
getFoldersByOwner(username).then(setFolders);
}, [session.data]);
if (session.status !== "authenticated") {
return (
<div className="fixed left-0 top-0 z-50 w-screen h-screen bg-black/50 flex justify-center items-center">
<Container className="p-6">
<div>You are not authenticated</div>;
</Container>
</div>
);
}
return (
<div className="fixed left-0 top-0 z-50 w-screen h-screen bg-black/50 flex justify-center items-center">
<Container className="p-6">
<h1>Choose a Folder to Add to</h1>
<div className="border border-gray-200 rounded-2xl">
{(folders.length > 0 &&
folders.map((folder) => (
<button
key={folder.id}
className="p-2 flex items-center justify-start hover:bg-gray-50 gap-2 hover:cursor-pointer w-full border-b border-gray-200"
onClick={() => {
createTextPair({
text1: item.text1,
text2: item.text2,
locale1: item.locale1,
locale2: item.locale2,
folders: {
connect: {
id: folder.id,
},
},
})
.then(() => {
toast.success("Text pair added to folder");
setShow(false);
})
.catch(() => {
toast.error("Failed to add text pair to folder");
});
}}
>
<Folder />
{folder.id}. {folder.name}
</button>
))) || <div>No folders found</div>}
</div>
<LightButton onClick={() => setShow(false)}>Close</LightButton>
</Container>
</div>
);
};
export default AddToFolder;

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import LightButton from "@/components/buttons/LightButton"; import LightButton from "@/components/buttons/LightButton";
import Container from "@/components/cards/Container";
import IconClick from "@/components/IconClick"; import IconClick from "@/components/IconClick";
import IMAGES from "@/config/images"; import IMAGES from "@/config/images";
import { VOICES } from "@/config/locales"; import { VOICES } from "@/config/locales";
@@ -9,10 +8,12 @@ import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { TranslationHistorySchema } from "@/lib/interfaces"; import { TranslationHistorySchema } from "@/lib/interfaces";
import { tlsoPush, tlso } from "@/lib/localStorageOperators"; import { tlsoPush, tlso } from "@/lib/localStorageOperators";
import { getTTSAudioUrl } from "@/lib/tts"; import { getTTSAudioUrl } from "@/lib/tts";
import { letsFetch } from "@/lib/utils"; import { letsFetch, shallowEqual } from "@/lib/utils";
import { Plus, Trash } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import z from "zod"; import z from "zod";
import AddToFolder from "./AddToFolder";
export default function TranslatorPage() { export default function TranslatorPage() {
const t = useTranslations("translator"); const t = useTranslations("translator");
@@ -27,6 +28,10 @@ export default function TranslatorPage() {
const [history, setHistory] = useState< const [history, setHistory] = useState<
z.infer<typeof TranslationHistorySchema>[] z.infer<typeof TranslationHistorySchema>[]
>(tlso.get()); >(tlso.get());
const [showAddToFolder, setShowAddToFolder] = useState(false);
const [addToFolderItem, setAddToFolderItem] = useState<z.infer<
typeof TranslationHistorySchema
> | null>(null);
const lastTTS = useRef({ const lastTTS = useRef({
text: "", text: "",
@@ -67,8 +72,7 @@ export default function TranslatorPage() {
const checkUpdateLocalStorage = (item: typeof newItem) => { const checkUpdateLocalStorage = (item: typeof newItem) => {
if (item.text1 && item.text2 && item.locale1 && item.locale2) { if (item.text1 && item.text2 && item.locale1 && item.locale2) {
tlsoPush(item as z.infer<typeof TranslationHistorySchema>); setHistory(tlsoPush(item as z.infer<typeof TranslationHistorySchema>));
setHistory(tlso.get());
} }
}; };
const innerStates = { const innerStates = {
@@ -187,11 +191,11 @@ export default function TranslatorPage() {
<div className="w-full md:w-1/2 flex flex-col-reverse gap-2"> <div className="w-full md:w-1/2 flex flex-col-reverse gap-2">
{/* ICard2 Component */} {/* ICard2 Component */}
<div className="bg-gray-100 rounded-2xl w-full h-64 p-2"> <div className="bg-gray-100 rounded-2xl w-full h-64 p-2">
<div className="h-8/12 w-full">{tresult}</div> <div className="h-2/3 w-full overflow-y-auto">{tresult}</div>
<div className="ipa w-full h-2/12 overflow-auto text-gray-600"> <div className="ipa w-full h-1/6 overflow-y-auto text-gray-600">
{ipaTexts[1]} {ipaTexts[1]}
</div> </div>
<div className="h-2/12 w-full flex justify-end items-center"> <div className="h-1/6 w-full flex justify-end items-center">
<IconClick <IconClick
src={IMAGES.copy_all} src={IMAGES.copy_all}
alt="copy" alt="copy"
@@ -256,16 +260,47 @@ export default function TranslatorPage() {
</button> </button>
</div> </div>
{history.length > 0 && ( {history.length > 0 && (
<Container className="m-6 flex flex-col p-6"> <div className="m-6 flex flex-col items-center">
<h1 className="text-2xl font-light">History</h1> <h1 className="text-2xl font-light">History</h1>
<ul className="list-disc list-inside"> <div className="border border-gray-200 rounded-2xl m-4">
{history.map((item, index) => ( {history.map((item, index) => (
<li key={index}> <div key={index}>
<span className="font-bold">{item.text1}</span> - {item.text2} <div className="border-b border-gray-200 p-2 group hover:bg-gray-50 flex gap-2 flex-row justify-between items-start">
</li> <div className="flex-1 flex flex-col">
<p className="text-sm font-light">{item.text1}</p>
<p className="text-sm font-light">{item.text2}</p>
</div>
<div className="flex gap-2">
<button
onClick={() => {
setShowAddToFolder(true);
setAddToFolderItem(item);
}}
className="hover:bg-gray-200 hover:cursor-pointer rounded-4xl border border-gray-200 w-8 h-8 flex justify-center items-center"
>
<Plus />
</button>
<button
onClick={() => {
setHistory(
tlso.set(
tlso.get().filter((v) => !shallowEqual(v, item)),
) || [],
);
}}
className="hover:bg-gray-200 hover:cursor-pointer rounded-4xl border border-gray-200 w-8 h-8 flex justify-center items-center"
>
<Trash />
</button>
</div>
</div>
</div>
))} ))}
</ul> </div>
</Container> {showAddToFolder && (
<AddToFolder setShow={setShowAddToFolder} item={addToFolderItem!} />
)}
</div>
)} )}
</> </>
); );

View File

@@ -1,66 +0,0 @@
"use client";
import {
BOARD_WIDTH,
TEXT_WIDTH,
BOARD_HEIGHT,
TEXT_SIZE,
} from "@/config/word-board-config";
import { Word } from "@/lib/interfaces";
import { Dispatch, SetStateAction } from "react";
export default function TheBoard({
words,
selectWord,
}: {
words: [
{
word: string;
x: number;
y: number;
},
];
setWords: Dispatch<SetStateAction<Word[]>>;
selectWord: (word: string) => void;
}) {
function DraggableWord({ word }: { word: Word }) {
return (
<span
style={{
left: `${Math.floor(word.x * (BOARD_WIDTH - TEXT_WIDTH * word.word.length))}px`,
top: `${Math.floor(word.y * (BOARD_HEIGHT - TEXT_SIZE))}px`,
fontSize: `${TEXT_SIZE}px`,
}}
className="select-none cursor-pointer absolute code-block border-amber-100 border-1"
// onClick={inspect(word.word)}>{word.word}</span>))
onClick={() => {
selectWord(word.word);
}}
>
{word.word}
</span>
);
}
return (
<div
style={{
width: `${BOARD_WIDTH}px`,
height: `${BOARD_HEIGHT}px`,
}}
className="relative rounded bg-white"
>
{words.map(
(
v: {
word: string;
x: number;
y: number;
},
i: number,
) => {
return <DraggableWord word={v} key={i}></DraggableWord>;
},
)}
</div>
);
}

View File

@@ -1,185 +0,0 @@
"use client";
import TheBoard from "@/app/(features)/word-board/TheBoard";
import LightButton from "../../../components/buttons/LightButton";
import { KeyboardEvent, useRef, useState } from "react";
import { Word } from "@/lib/interfaces";
import {
BOARD_WIDTH,
TEXT_WIDTH,
BOARD_HEIGHT,
TEXT_SIZE,
} from "@/config/word-board-config";
import { inspect } from "@/lib/utils";
export default function WordBoardPage() {
const inputRef = useRef<HTMLInputElement>(null);
const inputFileRef = useRef<HTMLInputElement>(null);
const initialWords = [
// 'apple',
// 'banana',
// 'cannon',
// 'desktop',
// 'kernel',
// 'system',
// 'programming',
// 'owe'
] as Array<string>;
const [words, setWords] = useState(
initialWords.map((v: string) => ({
word: v,
x: Math.random(),
y: Math.random(),
})),
);
const generateNewWord = (word: string) => {
const isOK = (w: Word) => {
if (words.length === 0) return true;
const tf = (ww: Word) =>
({
word: ww.word,
x: Math.floor(ww.x * (BOARD_WIDTH - TEXT_WIDTH * ww.word.length)),
y: Math.floor(ww.y * (BOARD_HEIGHT - TEXT_SIZE)),
}) as Word;
const tfd_words = words.map(tf);
const tfd_w = tf(w);
for (const www of tfd_words) {
const p1 = {
x: (www.x + www.x + TEXT_WIDTH * www.word.length) / 2,
y: (www.y + www.y + TEXT_SIZE) / 2,
};
const p2 = {
x: (tfd_w.x + tfd_w.x + TEXT_WIDTH * tfd_w.word.length) / 2,
y: (tfd_w.y + tfd_w.y + TEXT_SIZE) / 2,
};
if (
Math.abs(p1.x - p2.x) <
(TEXT_WIDTH * (www.word.length + tfd_w.word.length)) / 2 &&
Math.abs(p1.y - p2.y) < TEXT_SIZE
) {
return false;
}
}
return true;
};
let new_word;
let count = 0;
do {
new_word = {
word: word,
x: Math.random(),
y: Math.random(),
};
if (++count > 1000) return null;
} while (!isOK(new_word));
return new_word as Word;
};
const insertWord = () => {
if (!inputRef.current) return;
const word = inputRef.current.value.trim();
if (word === "") return;
const new_word = generateNewWord(word);
if (!new_word) return;
setWords([...words, new_word]);
inputRef.current.value = "";
};
const deleteWord = () => {
if (!inputRef.current) return;
const word = inputRef.current.value.trim();
if (word === "") return;
setWords(words.filter((v) => v.word !== word));
inputRef.current.value = "";
};
const importWords = () => {
inputFileRef.current?.click();
};
const exportWords = () => {
const blob = new Blob([JSON.stringify(words)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${Date.now()}.json`;
a.style.display = "none";
a.click();
URL.revokeObjectURL(url);
};
const handleFileChange = () => {
const files = inputFileRef.current?.files;
if (files && files.length > 0) {
const reader = new FileReader();
reader.onload = () => {
if (reader.result && typeof reader.result === "string")
setWords(JSON.parse(reader.result) as [Word]);
};
reader.readAsText(files[0]);
}
};
const deleteAll = () => {
setWords([] as Array<Word>);
};
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
// e.preventDefault();
if (e.key === "Enter") {
insertWord();
}
};
const selectWord = (word: string) => {
if (!inputRef.current) return;
inputRef.current.value = word;
};
const searchWord = () => {
if (!inputRef.current) return;
const word = inputRef.current.value.trim();
if (word === "") return;
inspect(word)();
inputRef.current.value = "";
};
// const readWordAloud = () => {
// playFromUrl('https://fanyi.baidu.com/gettts?lan=uk&text=disclose&spd=3')
// return;
// if (!inputRef.current) return;
// const word = inputRef.current.value.trim();
// if (word === '') return;
// inspect(word)();
// inputRef.current.value = '';
// }
return (
<>
<div className="flex w-screen h-screen justify-center items-center">
<div
onKeyDown={handleKeyDown}
className="p-5 bg-gray-200 rounded shadow-2xl"
>
<TheBoard
selectWord={selectWord}
words={words as [Word]}
setWords={setWords}
/>
<div className="flex justify-center rounded mt-3 gap-1">
<input
ref={inputRef}
placeholder="word to operate"
type="text"
className="focus:outline-none border-b-2 border-black"
/>
<LightButton onClick={insertWord}></LightButton>
<LightButton onClick={deleteWord}></LightButton>
<LightButton onClick={searchWord}></LightButton>
<LightButton onClick={importWords}></LightButton>
<LightButton onClick={exportWords}></LightButton>
<LightButton onClick={deleteAll}></LightButton>
{/* <Button label="朗读" onClick={readWordAloud}></Button> */}
</div>
<input
type="file"
ref={inputFileRef}
className="hidden"
accept="application/json"
onChange={handleFileChange}
></input>
</div>
</div>
</>
);
}

View File

@@ -1,11 +1,6 @@
"use client"; "use client";
import { import { ChevronRight, Folder, FolderPlus, Trash2 } from "lucide-react";
ChevronRight,
Folder,
FolderPlus,
Trash2,
} from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Center } from "@/components/Center"; import { Center } from "@/components/Center";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -13,11 +8,11 @@ import { folder } from "../../../generated/prisma/browser";
import { import {
createFolder, createFolder,
deleteFolderById, deleteFolderById,
getFoldersByOwner, getFoldersWithTotalPairsByOwner,
} from "@/lib/services/folderService"; } from "@/lib/services/folderService";
interface FolderProps { interface FolderProps {
folder: folder; folder: folder & { total_pairs: number };
deleteCallback: () => void; deleteCallback: () => void;
openCallback: () => void; openCallback: () => void;
} }
@@ -34,14 +29,16 @@ const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h3 className="font-medium text-gray-900">{folder.name}</h3> <h3 className="font-medium text-gray-900">
{folder.id}. {folder.name} ({folder.total_pairs})
</h3>
{/*<p className="text-sm text-gray-500">{} items</p>*/} {/*<p className="text-sm text-gray-500">{} items</p>*/}
</div> </div>
<div className="text-xs text-gray-400">#{folder.id}</div> <div className="text-xs text-gray-400">#{folder.id}</div>
</div> </div>
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-2 opacity-50 group-hover:opacity-100 transition-opacity">
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -58,12 +55,14 @@ const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
}; };
export default function FoldersClient({ username }: { username: string }) { export default function FoldersClient({ username }: { username: string }) {
const [folders, setFolders] = useState<folder[]>([]); const [folders, setFolders] = useState<(folder & { total_pairs: number })[]>(
[],
);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
getFoldersByOwner(username).then((folders) => { getFoldersWithTotalPairsByOwner(username).then((folders) => {
setFolders(folders); setFolders(folders);
}); });
}, [username]); }, [username]);
@@ -71,7 +70,7 @@ export default function FoldersClient({ username }: { username: string }) {
const updateFolders = async () => { const updateFolders = async () => {
setLoading(true); setLoading(true);
try { try {
const updatedFolders = await getFoldersByOwner(username); const updatedFolders = await getFoldersWithTotalPairsByOwner(username);
setFolders(updatedFolders); setFolders(updatedFolders);
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { ArrowLeft, Edit, Plus, Trash2, X } from "lucide-react"; import { ArrowLeft, Plus } from "lucide-react";
import { Center } from "@/components/Center"; import { Center } from "@/components/Center";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { redirect, useRouter } from "next/navigation"; import { redirect, useRouter } from "next/navigation";
@@ -9,12 +9,9 @@ import {
createTextPair, createTextPair,
deleteTextPairById, deleteTextPairById,
getTextPairsByFolderId, getTextPairsByFolderId,
updateTextPairById,
} from "@/lib/services/textPairService"; } from "@/lib/services/textPairService";
import AddTextPairModal from "./AddTextPairModal"; import AddTextPairModal from "./AddTextPairModal";
import TextPairCard from "./TextPairCard"; import TextPairCard from "./TextPairCard";
import UpdateTextPairModal from "./UpdateTextPairModal";
import { text_pairUpdateInput } from "../../../../generated/prisma/models";
import LightButton from "@/components/buttons/LightButton"; import LightButton from "@/components/buttons/LightButton";
export interface TextPair { export interface TextPair {

View File

@@ -4,6 +4,7 @@ import type { Viewport } from "next";
import { NextIntlClientProvider } from "next-intl"; import { NextIntlClientProvider } from "next-intl";
import SessionWrapper from "@/lib/SessionWrapper"; import SessionWrapper from "@/lib/SessionWrapper";
import { Navbar } from "@/components/Navbar"; import { Navbar } from "@/components/Navbar";
import { Toaster } from "sonner";
export const viewport: Viewport = { export const viewport: Viewport = {
width: "device-width", width: "device-width",
@@ -27,6 +28,7 @@ export default async function RootLayout({
<NextIntlClientProvider> <NextIntlClientProvider>
<Navbar></Navbar> <Navbar></Navbar>
{children} {children}
<Toaster />
</NextIntlClientProvider> </NextIntlClientProvider>
</body> </body>
</html> </html>

View File

@@ -3,10 +3,9 @@
import { signOut, useSession } from "next-auth/react"; import { signOut, useSession } from "next-auth/react";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import Image from "next/image"; import Image from "next/image";
import DarkButton from "@/components/buttons/DarkButton";
import { useEffect } from "react"; import { useEffect } from "react";
import ACard from "@/components/cards/ACard";
import { Center } from "@/components/Center"; import { Center } from "@/components/Center";
import Container from "@/components/cards/Container";
import LightButton from "@/components/buttons/LightButton"; import LightButton from "@/components/buttons/LightButton";
export default function MePage() { export default function MePage() {
@@ -22,7 +21,7 @@ export default function MePage() {
return ( return (
<Center> <Center>
<ACard> <Container className="p-6">
<h1>My Profile</h1> <h1>My Profile</h1>
{(session.data?.user?.image as string) && ( {(session.data?.user?.image as string) && (
<Image <Image
@@ -35,9 +34,8 @@ export default function MePage() {
)} )}
<p>{session.data?.user?.name}</p> <p>{session.data?.user?.name}</p>
<p>Email: {session.data?.user?.email}</p> <p>Email: {session.data?.user?.email}</p>
<DarkButton onClick={signOut}>Logout</DarkButton> <LightButton onClick={signOut}>Logout</LightButton>
</Container>
</ACard>
</Center> </Center>
); );
} }

View File

@@ -8,7 +8,7 @@ import IMAGES from "@/config/images";
import { useState } from "react"; import { useState } from "react";
import LightButton from "./buttons/LightButton"; import LightButton from "./buttons/LightButton";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { Folder, Home } from "lucide-react"; import { Folder, Home, LoaderCircle } from "lucide-react";
export function Navbar() { export function Navbar() {
const t = useTranslations("navbar"); const t = useTranslations("navbar");
@@ -73,13 +73,15 @@ export function Navbar() {
<Link href="/folders" className="md:hidden block"> <Link href="/folders" className="md:hidden block">
<Folder /> <Folder />
</Link> </Link>
{session?.status === "authenticated" ? ( {session?.status === "authenticated" && (
<div className="flex gap-2"> <div className="flex gap-2">
<Link href="/profile">{t("profile")}</Link> <Link href="/profile">{t("profile")}</Link>
</div> </div>
) : ( )}
{session?.status === "unauthenticated" && (
<Link href="/login">{t("login")}</Link> <Link href="/login">{t("login")}</Link>
)} )}
{session?.status === "loading" && <LoaderCircle />}
<Link href="/changelog.txt">{t("about")}</Link> <Link href="/changelog.txt">{t("about")}</Link>
<Link <Link
className="hidden md:block" className="hidden md:block"

View File

@@ -2,7 +2,7 @@ import { format } from "util";
async function callZhipuAPI( async function callZhipuAPI(
messages: { role: string; content: string }[], messages: { role: string; content: string }[],
model = "glm-4.6", model = process.env.ZHIPU_MODEL_NAME,
) { ) {
const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"; const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
@@ -54,10 +54,7 @@ export async function simpleGetLLMAnswer(
return Response.json({ return Response.json({
status: "success", status: "success",
message: await getLLMAnswer( message: await getLLMAnswer(
format( format(prompt, ...args.map((v) => searchParams.get(v))),
prompt,
...args.map((v) => searchParams.get(v)),
),
), ),
}); });
} }

View File

@@ -2,7 +2,7 @@ import {
TranslationHistoryArraySchema, TranslationHistoryArraySchema,
TranslationHistorySchema, TranslationHistorySchema,
} from "@/lib/interfaces"; } from "@/lib/interfaces";
import { getLocalStorageOperator } from "@/lib/utils"; import { getLocalStorageOperator, shallowEqual } from "@/lib/utils";
import z from "zod"; import z from "zod";
const MAX_HISTORY_LENGTH = 50; const MAX_HISTORY_LENGTH = 50;
@@ -11,9 +11,11 @@ export const tlso = getLocalStorageOperator<
typeof TranslationHistoryArraySchema typeof TranslationHistoryArraySchema
>("translator", TranslationHistoryArraySchema); >("translator", TranslationHistoryArraySchema);
export const tlsoPush = (item: z.infer<typeof TranslationHistorySchema>) => { export const tlsoPush = (item: z.infer<typeof TranslationHistorySchema>) => {
tlso.set( const oldHistory = tlso.get();
[...tlso.get(), item as z.infer<typeof TranslationHistorySchema>].slice( if (oldHistory.some((v) => shallowEqual(v, item))) return oldHistory;
-MAX_HISTORY_LENGTH,
), const newHistory = [...oldHistory, item].slice(-MAX_HISTORY_LENGTH);
); tlso.set(newHistory);
return newHistory;
}; };

View File

@@ -1,6 +1,5 @@
"use server"; "use server";
import { folder } from "../../../generated/prisma/client";
import { import {
folderCreateInput, folderCreateInput,
folderUpdateInput, folderUpdateInput,

View File

@@ -92,6 +92,7 @@ export const getLocalStorageOperator = <T extends z.ZodTypeAny>(
set: (data: z.infer<T>) => { set: (data: z.infer<T>) => {
if (!localStorage) return; if (!localStorage) return;
localStorage.setItem(key, JSON.stringify(data)); localStorage.setItem(key, JSON.stringify(data));
return data;
}, },
}; };
}; };
@@ -128,3 +129,22 @@ export const letsFetch = (
export function isNonNegativeInteger(str: string): boolean { export function isNonNegativeInteger(str: string): boolean {
return /^\d+$/.test(str); return /^\d+$/.test(str);
} }
export function shallowEqual<T extends object>(obj1: T, obj2: T): boolean {
const keys1 = Object.keys(obj1) as Array<keyof T>;
const keys2 = Object.keys(obj2) as Array<keyof T>;
// 首先检查键的数量是否相同
if (keys1.length !== keys2.length) {
return false;
}
// 然后逐个比较键值对
for (const key of keys1) {
if (obj1[key] !== obj2[key]) {
return false;
}
}
return true;
}