From d4f786c99086462597b1098c731466ef13ee074d Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Mon, 10 Nov 2025 21:42:44 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=BA=86translator=EF=BC=8C?= =?UTF-8?q?=E5=86=99=E4=BA=86=E7=82=B9=E6=95=B0=E6=8D=AE=E5=BA=93=E3=80=81?= =?UTF-8?q?=E5=90=8E=E7=AB=AFapi=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 7 + .gitignore | 1 + next.config.ts | 12 +- package-lock.json | 230 ++++++++++- package.json | 1 + public/changelog.txt | 2 +- .../images/github-mark/github-mark-white.png | Bin 0 -> 4837 bytes .../images/github-mark/github-mark-white.svg | 1 + public/images/github-mark/github-mark.png | Bin 0 -> 6393 bytes public/images/github-mark/github-mark.svg | 1 + public/messages/en-US/navbar.json | 4 +- public/messages/zh-CN/navbar.json | 4 +- src/app/alphabet/page.tsx | 4 +- src/app/api/auth/[...nextauth]/route.ts | 52 +-- src/app/api/folder/[id]/route.ts | 18 + src/app/api/folders/route.ts | 35 ++ src/app/api/users/[...slug]/route.ts | 22 -- src/app/api/users/route.ts | 7 - src/app/api/v1/ipa/route.ts | 10 + src/app/api/v1/locale/route.ts | 10 + src/app/api/v1/translate/route.ts | 10 + src/app/layout.tsx | 19 +- src/app/login/page.tsx | 51 ++- src/app/memorize/Choose.tsx | 6 +- src/app/memorize/Edit.tsx | 8 +- src/app/memorize/Main.tsx | 5 +- src/app/memorize/Start.tsx | 6 +- src/app/memorize/page.tsx | 2 +- src/app/page.tsx | 4 +- src/app/profile/page.tsx | 55 +++ src/app/srt-player/page.tsx | 4 +- src/app/text-speaker/SaveList.tsx | 18 +- src/app/text-speaker/page.tsx | 28 +- src/app/translator/page.tsx | 368 ++++++++---------- src/app/word-board/page.tsx | 4 +- src/components/Center.tsx | 7 + src/components/Input.tsx | 3 + src/components/Navbar.tsx | 28 +- src/components/NavbarCenterWrapper.tsx | 18 - src/components/NavbarWrapper.tsx | 14 - src/components/auth-components.tsx | 34 ++ src/components/buttons/DarkButton.tsx | 3 + src/components/buttons/LightButton.tsx | 3 + src/components/buttons/PlainButton.tsx | 3 + src/config/images.ts | 1 + src/interfaces.ts | 32 +- src/lib/SessionWrapper.tsx | 11 + src/lib/actions.ts | 11 + src/lib/ai.ts | 63 +++ src/lib/db.ts | 116 +++++- src/lib/localStorageOperators.ts | 17 + src/lib/tts.ts | 16 + src/utils.ts | 80 ++-- 53 files changed, 1037 insertions(+), 432 deletions(-) create mode 100644 .env.example create mode 100644 public/images/github-mark/github-mark-white.png create mode 100644 public/images/github-mark/github-mark-white.svg create mode 100644 public/images/github-mark/github-mark.png create mode 100644 public/images/github-mark/github-mark.svg create mode 100644 src/app/api/folder/[id]/route.ts create mode 100644 src/app/api/folders/route.ts delete mode 100644 src/app/api/users/[...slug]/route.ts delete mode 100644 src/app/api/users/route.ts create mode 100644 src/app/api/v1/ipa/route.ts create mode 100644 src/app/api/v1/locale/route.ts create mode 100644 src/app/api/v1/translate/route.ts create mode 100644 src/app/profile/page.tsx create mode 100644 src/components/Center.tsx delete mode 100644 src/components/NavbarCenterWrapper.tsx delete mode 100644 src/components/NavbarWrapper.tsx create mode 100644 src/components/auth-components.tsx create mode 100644 src/lib/SessionWrapper.tsx create mode 100644 src/lib/actions.ts create mode 100644 src/lib/ai.ts create mode 100644 src/lib/localStorageOperators.ts create mode 100644 src/lib/tts.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c571d09 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +ZHIPU_API_KEY= +AUTH_SECRET= + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +NEXTAUTH_URL= diff --git a/.gitignore b/.gitignore index 0ca3c82..af52675 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ yarn-error.log* next-env.d.ts .env +!.env.example build.sh diff --git a/next.config.ts b/next.config.ts index ca2fd69..a4e3c95 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,7 +3,17 @@ import createNextIntlPlugin from "next-intl/plugin"; const nextConfig: NextConfig = { /* config options here */ output: "standalone", - allowedDevOrigins: ["192.168.3.65", "192.168.3.66"], + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + port: "", + pathname: "/u/**", + }, + ], + }, + // allowedDevOrigins: ["192.168.3.65", "192.168.3.66"], }; const withNextIntl = createNextIntlPlugin(); diff --git a/package-lock.json b/package-lock.json index 6abab2b..cc89742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "pg": "^8.16.3", "react": "19.1.0", "react-dom": "19.1.0", + "unstorage": "^1.17.2", "zod": "^3.25.76" }, "devDependencies": { @@ -5229,8 +5230,6 @@ "resolved": "https://mirrors.cloud.tencent.com/npm/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", - "optional": true, - "peer": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -6133,6 +6132,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chrome-launcher": { "version": "0.15.2", "resolved": "https://mirrors.cloud.tencent.com/npm/chrome-launcher/-/chrome-launcher-0.15.2.tgz", @@ -6479,6 +6493,12 @@ "node": ">= 0.6" } }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "license": "MIT" + }, "node_modules/core-js-compat": { "version": "3.46.0", "resolved": "https://mirrors.cloud.tencent.com/npm/core-js-compat/-/core-js-compat-3.46.0.tgz", @@ -6518,6 +6538,15 @@ "node": ">= 8" } }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://mirrors.cloud.tencent.com/npm/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -6710,6 +6739,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://mirrors.cloud.tencent.com/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6730,6 +6765,12 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://mirrors.cloud.tencent.com/npm/destroy/-/destroy-1.2.0.tgz", @@ -8263,6 +8304,23 @@ "dev": true, "license": "MIT" }, + "node_modules/h3": { + "version": "1.15.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/h3/-/h3-1.15.4.tgz", + "integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.5", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.2", + "radix3": "^1.1.2", + "ufo": "^1.6.1", + "uncrypto": "^0.1.3" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://mirrors.cloud.tencent.com/npm/has-bigints/-/has-bigints-1.1.0.tgz", @@ -8586,6 +8644,15 @@ "loose-envify": "^1.0.0" } }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://mirrors.cloud.tencent.com/npm/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -11722,6 +11789,12 @@ } } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://mirrors.cloud.tencent.com/npm/node-forge/-/node-forge-1.3.1.tgz", @@ -11741,6 +11814,12 @@ "optional": true, "peer": true }, + "node_modules/node-mock-http": { + "version": "1.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/node-mock-http/-/node-mock-http-1.0.3.tgz", + "integrity": "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://mirrors.cloud.tencent.com/npm/node-releases/-/node-releases-2.0.27.tgz", @@ -11754,8 +11833,6 @@ "resolved": "https://mirrors.cloud.tencent.com/npm/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11937,6 +12014,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, "node_modules/oidc-token-hash": { "version": "5.2.0", "resolved": "https://mirrors.cloud.tencent.com/npm/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", @@ -12464,7 +12552,6 @@ "version": "2.3.1", "resolved": "https://mirrors.cloud.tencent.com/npm/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -12815,6 +12902,12 @@ ], "license": "MIT" }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://mirrors.cloud.tencent.com/npm/range-parser/-/range-parser-1.2.1.tgz", @@ -12928,6 +13021,19 @@ "node": ">=0.10.0" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://mirrors.cloud.tencent.com/npm/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -14790,6 +14896,12 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://mirrors.cloud.tencent.com/npm/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -14809,6 +14921,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici": { "version": "6.22.0", "resolved": "https://mirrors.cloud.tencent.com/npm/undici/-/undici-6.22.0.tgz", @@ -14945,6 +15063,108 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/unstorage": { + "version": "1.17.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/unstorage/-/unstorage-1.17.2.tgz", + "integrity": "sha512-cKEsD6iBWJgOMJ6vW1ID/SYuqNf8oN4yqRk8OYqaVQ3nnkJXOT1PSpaMh2QfzLs78UN5kSNRD2c/mgjT8tX7+w==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.0", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://mirrors.cloud.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", diff --git a/package.json b/package.json index 1a6d411..9d85291 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "pg": "^8.16.3", "react": "19.1.0", "react-dom": "19.1.0", + "unstorage": "^1.17.2", "zod": "^3.25.76" }, "devDependencies": { diff --git a/public/changelog.txt b/public/changelog.txt index 0fdf6f2..c617b1a 100644 --- a/public/changelog.txt +++ b/public/changelog.txt @@ -1,3 +1,4 @@ +2025.11.10 重构了translator,将其改为并发请求多个数据,速度大大提升 2025.10.31 添加国际化支持 2025.10.30 添加背单词功能 2025.10.12 添加朗读器本地保存功能 @@ -8,4 +9,3 @@ 2025.10.05 新增IPA生成与文本朗读功能 2025.09.25 优化了主界面UI 2025.09.19 更新了单词板,单词不再会重叠 - diff --git a/public/images/github-mark/github-mark-white.png b/public/images/github-mark/github-mark-white.png new file mode 100644 index 0000000000000000000000000000000000000000..50b81752278d084ba9d449fff25f4051df162b0f GIT binary patch literal 4837 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 literal 0 HcmV?d00001 diff --git a/public/images/github-mark/github-mark-white.svg b/public/images/github-mark/github-mark-white.svg new file mode 100644 index 0000000..d5e6491 --- /dev/null +++ b/public/images/github-mark/github-mark-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/github-mark/github-mark.png b/public/images/github-mark/github-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..6cb3b705d018006a2bd4200ea94c9d5fb98b6f76 GIT binary patch literal 6393 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 Hu0mjfZ$b4` literal 0 HcmV?d00001 diff --git a/public/images/github-mark/github-mark.svg b/public/images/github-mark/github-mark.svg new file mode 100644 index 0000000..37fa923 --- /dev/null +++ b/public/images/github-mark/github-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/messages/en-US/navbar.json b/public/messages/en-US/navbar.json index 0600dda..d0fa739 100644 --- a/public/messages/en-US/navbar.json +++ b/public/messages/en-US/navbar.json @@ -1,5 +1,7 @@ { "title": "LL", "about": "About", - "sourceCode": "GitHub" + "sourceCode": "GitHub", + "login": "Login", + "profile": "Profile" } diff --git a/public/messages/zh-CN/navbar.json b/public/messages/zh-CN/navbar.json index f261619..b9514e9 100644 --- a/public/messages/zh-CN/navbar.json +++ b/public/messages/zh-CN/navbar.json @@ -1,5 +1,7 @@ { "title": "学语言", "about": "关于", - "sourceCode": "源码" + "sourceCode": "源码", + "login": "登录", + "profile": "个人资料" } diff --git a/src/app/alphabet/page.tsx b/src/app/alphabet/page.tsx index 25dee3a..b39439a 100644 --- a/src/app/alphabet/page.tsx +++ b/src/app/alphabet/page.tsx @@ -4,7 +4,7 @@ import LightButton from "@/components/buttons/LightButton"; import { Letter, SupportedAlphabets } from "@/interfaces"; import { useEffect, useState } from "react"; import MemoryCard from "./MemoryCard"; -import { Navbar } from "@/components/Navbar"; + import { useTranslations } from "next-intl"; export default function Alphabet() { @@ -58,7 +58,6 @@ export default function Alphabet() { if (!chosenAlphabet) return ( <> -
{t("chooseCharacters")}
@@ -87,7 +86,6 @@ export default function Alphabet() { if (loadingState === "success" && alphabetData[chosenAlphabet]) { return ( <> - ) { return ( - - - {children} - - + + + + + + {children} + + + + ); } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 964274a..f924a31 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,25 +1,42 @@ "use client"; import LightButton from "@/components/buttons/LightButton"; -import ACard from "@/components/cards/ACard"; -import Input from "@/components/Input"; -import NavbarCenterWrapper from "@/components/NavbarCenterWrapper"; -import { useRef } from "react"; +import { Center } from "@/components/Center"; +import IMAGES from "@/config/images"; +import { signIn, useSession } from "next-auth/react"; +import Image from "next/image"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect } from "react"; -export default function Login() { - const usernameRef = useRef(null); - const passwordRef = useRef(null); +export default function LoginPage() { + const session = useSession(); + const router = useRouter(); + const searchParams = useSearchParams(); + + useEffect(() => { + if (session.status === "authenticated") { + router.push(searchParams.get("redirect") || "/"); + } + }, [session.status, router, searchParams]); return ( - - -

Login

-
- - - Submit -
-
-
+
+ {session.status === "loading" ? ( +
Loading...
+ ) : ( + signIn("github")} + > + GitHub Logo + GitHub Login + + )} +
); } diff --git a/src/app/memorize/Choose.tsx b/src/app/memorize/Choose.tsx index aeba7a1..3fd2e20 100644 --- a/src/app/memorize/Choose.tsx +++ b/src/app/memorize/Choose.tsx @@ -4,7 +4,7 @@ import BCard from "@/components/cards/BCard"; import { LOCALES } from "@/config/locales"; import { Dispatch, SetStateAction, useState } from "react"; import { WordData } from "@/interfaces"; -import NavbarCenterWrapper from "@/components/NavbarCenterWrapper"; + import { useTranslations } from "next-intl"; interface Props { @@ -39,7 +39,7 @@ export default function Choose({ }; return ( - +
{LOCALES.map((locale, index) => ( @@ -62,6 +62,6 @@ export default function Choose({
- +
); } diff --git a/src/app/memorize/Edit.tsx b/src/app/memorize/Edit.tsx index 3785932..5ea30d5 100644 --- a/src/app/memorize/Edit.tsx +++ b/src/app/memorize/Edit.tsx @@ -5,7 +5,7 @@ import { ChangeEvent, Dispatch, SetStateAction, useRef, useState } from "react"; import DarkButton from "@/components/buttons/DarkButton"; import { WordData } from "@/interfaces"; import Choose from "./Choose"; -import NavbarCenterWrapper from "@/components/NavbarCenterWrapper"; + import { useTranslations } from "next-intl"; interface Props { @@ -51,7 +51,7 @@ export default function Edit({ setPage, wordData, setWordData }: Props) { setWordData(newWordData); if (textareaRef.current) textareaRef.current.value = convertFromWordData(newWordData); - if(localStorage) { + if (localStorage) { localStorage.setItem("wordData", JSON.stringify(newWordData)); } }; @@ -60,7 +60,7 @@ export default function Edit({ setPage, wordData, setWordData }: Props) { }; if (editPage === "edit") return ( - +
- {sourceIPA} + {ipaTexts[0]}
{ - if (sourceText.length !== 0) - await navigator.clipboard.writeText(sourceText); - }} src={IMAGES.copy_all} alt="copy" + onClick={async () => { + await navigator.clipboard.writeText( + taref.current?.value || "", + ); + }} > { + const t = taref.current?.value; + if (!t) return; + tts(t, tlso.get().find((v) => v.text1 === t)?.locale1 || ""); + }} >
{t("detectLanguage")} setIPAEnabled(!ipaEnabled)} + selected={genIpa} + onClick={() => setGenIpa((prev) => !prev)} > {t("generateIPA")}
-
-
-
{targetText}
+ + {/* Card Component - Right Side */} +
+ {/* ICard2 Component */} +
+
{tresult}
- {targetIPA} + {ipaTexts[1]}
{ - if (targetText.length !== 0) - await navigator.clipboard.writeText(targetText); - }} src={IMAGES.copy_all} alt="copy" + onClick={async () => { + await navigator.clipboard.writeText(tresult); + }} > { + tts( + tresult, + tlso.get().find((v) => v.text2 === tresult)?.locale2 || "", + ); + }} >
{t("translateInto")} { - setTargetLang("Chinese"); - }} - selected={targetLang === "Chinese"} + selected={lang === "chinese"} + onClick={() => setLang("chinese")} > {t("chinese")} { - setTargetLang("English"); - }} - selected={targetLang === "English"} + selected={lang === "english"} + onClick={() => setLang("english")} > {t("english")} { - setTargetLang("Italian"); - }} - selected={targetLang === "Italian"} + selected={lang === "italian"} + onClick={() => setLang("italian")} > {t("italian")} { + const newLang = prompt("Enter language"); + if (newLang) { + setLang(newLang); + } + }} > - {t("other") + (tl.includes(targetLang) ? "" : ": " + targetLang)} + {t("other")}
-
+ {/* TranslateButton Component */} +
diff --git a/src/app/word-board/page.tsx b/src/app/word-board/page.tsx index 5a1d318..5319f5e 100644 --- a/src/app/word-board/page.tsx +++ b/src/app/word-board/page.tsx @@ -10,9 +10,8 @@ import { TEXT_SIZE, } from "@/config/word-board-config"; import { inspect } from "@/utils"; -import { Navbar } from "@/components/Navbar"; -export default function WordBoard() { +export default function WordBoardPage() { const inputRef = useRef(null); const inputFileRef = useRef(null); const initialWords = [ @@ -147,7 +146,6 @@ export default function WordBoard() { // } return ( <> -
{ + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/Input.tsx b/src/components/Input.tsx index ccd6afe..3bda581 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -3,6 +3,7 @@ interface Props { placeholder?: string; type?: string; className?: string; + name?: string; } export default function Input({ @@ -10,6 +11,7 @@ export default function Input({ placeholder = "", type = "text", className = "", + name = "", }: Props) { return ( ); } diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index b315bdf..9e96dfb 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -7,11 +7,18 @@ import IconClick from "./IconClick"; import IMAGES from "@/config/images"; import { useState } from "react"; import LightButton from "./buttons/LightButton"; +import { useSession } from "next-auth/react"; -function MyLink({ href, label }: { href: string; label: string }) { +function MyLink({ + href, + children, +}: { + href: string; + children?: React.ReactNode; +}) { return ( - {label} + {children} ); } @@ -25,6 +32,7 @@ export function Navbar() { document.cookie = `locale=${locale}`; window.location.reload(); }; + const session = useSession(); return (
@@ -64,11 +72,17 @@ export function Navbar() { onClick={handleLanguageClick} >
- - + {session?.status === "authenticated" ? ( +
+ {t("profile")} +
+ ) : ( + {t("login")} + )} + {t("about")} + + {t("sourceCode")} +
); diff --git a/src/components/NavbarCenterWrapper.tsx b/src/components/NavbarCenterWrapper.tsx deleted file mode 100644 index d8ba4d6..0000000 --- a/src/components/NavbarCenterWrapper.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import NavbarWrapper from "./NavbarWrapper"; - -interface Props { - children: React.ReactNode; - className?: string; -} - -export default function NavbarCenterWrapper({ children, className }: Props) { - return ( - -
- {children} -
-
- ); -} diff --git a/src/components/NavbarWrapper.tsx b/src/components/NavbarWrapper.tsx deleted file mode 100644 index 0c79d1e..0000000 --- a/src/components/NavbarWrapper.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Navbar } from "./Navbar"; - -interface Props { - children: React.ReactNode; -} - -export default function NavbarWrapper({ children }: Props) { - return ( -
- - {children} -
- ); -} diff --git a/src/components/auth-components.tsx b/src/components/auth-components.tsx new file mode 100644 index 0000000..c2716fe --- /dev/null +++ b/src/components/auth-components.tsx @@ -0,0 +1,34 @@ +import { signIn, signOut } from "next-auth/react"; +import LightButton from "./buttons/LightButton"; + +export function SignIn({ + provider, + ...props +}: { provider?: string } & React.ComponentPropsWithRef) { + return ( +
{ + "use server" + await signIn(provider) + }} + > + Sign In +
+ ) +} + +export function SignOut(props: React.ComponentPropsWithRef) { + return ( +
{ + "use server" + await signOut() + }} + className="w-full" + > + + Sign Out + +
+ ) +} diff --git a/src/components/buttons/DarkButton.tsx b/src/components/buttons/DarkButton.tsx index 91f583c..fa261a7 100644 --- a/src/components/buttons/DarkButton.tsx +++ b/src/components/buttons/DarkButton.tsx @@ -5,16 +5,19 @@ export default function DarkButton({ className, selected, children, + type = "button", }: { onClick?: () => void; className?: string; selected?: boolean; children?: React.ReactNode; + type?: string; }) { return ( {children} diff --git a/src/components/buttons/LightButton.tsx b/src/components/buttons/LightButton.tsx index 1ee68fa..6da1322 100644 --- a/src/components/buttons/LightButton.tsx +++ b/src/components/buttons/LightButton.tsx @@ -5,16 +5,19 @@ export default function LightButton({ className, selected, children, + type = "button" }: { onClick?: () => void; className?: string; selected?: boolean; children?: React.ReactNode; + type?: string; }) { return ( {children} diff --git a/src/components/buttons/PlainButton.tsx b/src/components/buttons/PlainButton.tsx index 33697e1..5b82208 100644 --- a/src/components/buttons/PlainButton.tsx +++ b/src/components/buttons/PlainButton.tsx @@ -2,15 +2,18 @@ export default function PlainButton({ onClick, className, children, + type = "button", }: { onClick?: () => void; className?: string; children?: React.ReactNode; + type?: "button" | "submit" | "reset" | undefined; }) { return ( diff --git a/src/config/images.ts b/src/config/images.ts index 4c3b91c..81f9d3b 100644 --- a/src/config/images.ts +++ b/src/config/images.ts @@ -17,6 +17,7 @@ const IMAGES = { speed: "/images/speed_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg", language_black: "/images/language_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg", language_white: "/images/language_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg", + github_mark: "/images/github-mark/github-mark.svg", }; export default IMAGES; diff --git a/src/interfaces.ts b/src/interfaces.ts index eb16490..1347b4a 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -24,17 +24,33 @@ export const TextSpeakerItemSchema = z.object({ export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema); export const WordDataSchema = z.object({ - locales: z.tuple([z.string(), z.string()]) + locales: z + .tuple([z.string(), z.string()]) .refine(([first, second]) => first !== second, { - message: "Locales must be different" + message: "Locales must be different", }), - wordPairs: z.array(z.tuple([z.string(), z.string()])) + wordPairs: z + .array(z.tuple([z.string(), z.string()])) .min(1, "At least one word pair is required") - .refine((pairs) => { - return pairs.every(([first, second]) => first.trim() !== '' && second.trim() !== ''); - }, { - message: "Word pairs cannot contain empty strings" - }) + .refine( + (pairs) => { + return pairs.every( + ([first, second]) => first.trim() !== "" && second.trim() !== "", + ); + }, + { + message: "Word pairs cannot contain empty strings", + }, + ), }); +export const TranslationHistorySchema = z.object({ + text1: z.string(), + text2: z.string(), + locale1: z.string(), + locale2: z.string(), +}); + +export const TranslationHistoryArraySchema = z.array(TranslationHistorySchema); + export type WordData = z.infer; diff --git a/src/lib/SessionWrapper.tsx b/src/lib/SessionWrapper.tsx new file mode 100644 index 0000000..a141eba --- /dev/null +++ b/src/lib/SessionWrapper.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; + +export default function SessionWrapper({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/src/lib/actions.ts b/src/lib/actions.ts new file mode 100644 index 0000000..f3ba1c1 --- /dev/null +++ b/src/lib/actions.ts @@ -0,0 +1,11 @@ +"use server"; + +import { UserController } from "./db"; + +export async function loginAction(formData: FormData) { + const username = formData.get("username")?.toString(); + const password = formData.get("password")?.toString(); + + + if (username && password) await UserController.createUser(username, password); +} diff --git a/src/lib/ai.ts b/src/lib/ai.ts new file mode 100644 index 0000000..eaf342b --- /dev/null +++ b/src/lib/ai.ts @@ -0,0 +1,63 @@ +import { format } from "util"; + +async function callZhipuAPI( + messages: { role: string; content: string }[], + model = "glm-4.6", +) { + const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"; + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: "Bearer " + process.env.ZHIPU_API_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: model, + messages: messages, + temperature: 0.2, + thinking: { + type: "disabled", + }, + }), + }); + + if (!response.ok) { + throw new Error(`API 调用失败: ${response.status}`); + } + + return await response.json(); +} + +export async function getLLMAnswer(prompt: string) { + return ( + await callZhipuAPI([ + { + role: "user", + content: prompt, + }, + ]) + ).choices[0].message.content.trim() as string; +} + +export async function simpleGetLLMAnswer( + prompt: string, + searchParams: URLSearchParams, + args: string[], +) { + if (args.some((arg) => typeof searchParams.get(arg) !== "string")) { + return Response.json({ + status: "error", + message: "Missing required parameters", + }); + } + return Response.json({ + status: "success", + message: await getLLMAnswer( + format( + prompt, + ...args.map((v) => searchParams.get(v)), + ), + ), + }); +} diff --git a/src/lib/db.ts b/src/lib/db.ts index 0b46399..ea2902f 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -24,14 +24,126 @@ export class UserController { } static async getUserByUsername(username: string) { try { - const user = await pool.query("SELECT * FROM users WHERE username = $1", [username]); + const user = await pool.query("SELECT * FROM users WHERE username = $1", [ + username, + ]); return user.rows[0]; } catch (e) { console.log(e); } } + static async deleteUserById(id: number) { + try { + await pool.query("DELETE FROM users WHERE id = $1", [id]); + } catch (e) { + console.log(e); + } + } } export class FolderController { - + static async getFolderById(id: number) { + try { + const folder = await pool.query("SELECT * FROM folders WHERE id = $1", [ + id, + ]); + return folder.rows[0]; + } catch (e) { + console.log(e); + } + } + static async deleteFolderById(id: number) { + try { + await pool.query("DELETE FROM folders WHERE id = $1", [id]); + } catch (e) { + console.log(e); + } + } + static async getFoldersByOwner(owner: string) { + try { + const folders = await pool.query( + "SELECT * FROM folders WHERE owner = $1", + [owner], + ); + return folders.rows; + } catch (e) { + console.log(e); + } + } + static async createFolder(name: string, owner: string) { + try { + return ( + await pool.query("INSERT INTO folders (name, owner) VALUES ($1, $2)", [ + name, + owner, + ]) + ).rows[0]; + } catch (e) { + console.log(e); + } + } +} + +export class WordPairController { + static async createWordPair( + locale1: string, + locale2: string, + text1: string, + text2: string, + folderId: number, + ) { + try { + await pool.query( + "INSERT INTO word_pairs (locale1, locale2, text1, text2, folder_id) VALUES ($1, $2, $3, $4, $5)", + [locale1, locale2, text1, text2, folderId], + ); + } catch (e) { + console.log(e); + } + } + static async getWordPairById(id: number) { + try { + const wordPair = await pool.query( + "SELECT * FROM word_pairs WHERE id = $1", + [id], + ); + return wordPair.rows[0]; + } catch (e) { + console.log(e); + } + } + static async deleteWordPairById(id: number) { + try { + await pool.query("DELETE FROM word_pairs WHERE id = $1", [id]); + } catch (e) { + console.log(e); + } + } + static async updateWordPairById( + id: number, + locale1: string, + locale2: string, + text1: string, + text2: string, + ) { + try { + await pool.query( + "UPDATE word_pairs SET locale1 = $1, locale2 = $2, text1 = $3, text2 = $4 WHERE id = $5", + [locale1, locale2, text1, text2, id], + ); + } catch (e) { + console.log(e); + } + } + static async getWordPairsByFolderId(folderId: number) { + try { + const wordPairs = await pool.query( + "SELECT * FROM word_pairs WHERE folder_id = $1", + [folderId], + ); + return wordPairs.rows; + } catch (e) { + console.log(e); + } + } } diff --git a/src/lib/localStorageOperators.ts b/src/lib/localStorageOperators.ts new file mode 100644 index 0000000..b75163f --- /dev/null +++ b/src/lib/localStorageOperators.ts @@ -0,0 +1,17 @@ +import { TranslationHistoryArraySchema, TranslationHistorySchema } from "@/interfaces"; +import { getLocalStorageOperator } from "@/utils"; +import z from "zod"; + +const MAX_HISTORY_LENGTH = 50; + +export const tlso = getLocalStorageOperator( + "translator", + TranslationHistoryArraySchema, +); +export const tlsoPush = (item: z.infer) => { + tlso.set( + [...tlso.get(), item as z.infer].slice( + -MAX_HISTORY_LENGTH, + ), + ); +}; \ No newline at end of file diff --git a/src/lib/tts.ts b/src/lib/tts.ts new file mode 100644 index 0000000..9101093 --- /dev/null +++ b/src/lib/tts.ts @@ -0,0 +1,16 @@ +import { ProsodyOptions } from "edge-tts-universal"; +import { EdgeTTS } from "edge-tts-universal/browser"; + +export async function getTTSAudioUrl( + text: string, + short_name: string, + options: ProsodyOptions | undefined = undefined, +) { + const tts = new EdgeTTS(text, short_name, options); + try { + const result = await tts.synthesize(); + return URL.createObjectURL(result.audio); + } catch (e) { + throw e; + } +} diff --git a/src/utils.ts b/src/utils.ts index 97ff5e1..4359588 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,5 @@ import { EdgeTTS, ProsodyOptions } from "edge-tts-universal/browser"; import { env } from "process"; -import { TextSpeakerArraySchema } from "./interfaces"; import z from "zod"; import { NextResponse } from "next/server"; @@ -60,33 +59,43 @@ export async function getTTSAudioUrl( throw e; } } -export const getTextSpeakerData = () => { - try { - if (!localStorage) return []; - const item = localStorage.getItem("text-speaker"); - if (!item) return []; - - const rawData = JSON.parse(item); - const result = TextSpeakerArraySchema.safeParse(rawData); - - if (result.success) { - return result.data; - } else { - console.error("Invalid data structure in localStorage:", result.error); - return []; - } - } catch (e) { - console.error("Failed to parse text-speaker data:", e); - return []; - } -}; -export const setTextSpeakerData = ( - data: z.infer, +export const getLocalStorageOperator = ( + key: string, + schema: T, ) => { - if (!localStorage) return; - localStorage.setItem("text-speaker", JSON.stringify(data)); + return { + get: (): z.infer => { + try { + if (!localStorage) return []; + const item = localStorage.getItem(key); + + if (!item) return []; + + const rawData = JSON.parse(item) as z.infer; + const result = schema.safeParse(rawData); + + if (result.success) { + return result.data; + } else { + console.error( + "Invalid data structure in localStorage:", + result.error, + ); + return []; + } + } catch (e) { + console.error(`Failed to parse ${key} data:`, e); + return []; + } + }, + set: (data: z.infer) => { + if (!localStorage) return; + localStorage.setItem(key, JSON.stringify(data)); + }, + }; }; + export function handleAPIError(error: unknown, message: string) { console.error(message, error); return NextResponse.json( @@ -94,3 +103,24 @@ export function handleAPIError(error: unknown, message: string) { { status: 500 }, ); } + + +export const letsFetch = ( + url: string, + onSuccess: (message: string) => void, + onError: (message: string) => void, + onFinally: () => void, +) => { + return fetch(url) + .then((response) => response.json()) + .then((data) => { + if (data.status === "success") { + onSuccess(data.message); + } else if (data.status === "error") { + onError(data.message); + } else { + onError("Unknown error"); + } + }) + .finally(onFinally); +}; \ No newline at end of file