diff --git a/api/app.ts b/api/app.ts index 01f9ec1..658f1c3 100644 --- a/api/app.ts +++ b/api/app.ts @@ -1,12 +1,15 @@ import Koa, { Context } from 'koa'; -import koaBody from 'koa-body'; +import body from 'koa-body'; +import logger from 'koa-logger'; const app = new Koa(); + app.use(async (ctx: Context, next) => { console.info(`access url: ${ctx.url}`); await next(); }); -app.use(koaBody()); +app.use(body()); +app.use(logger()); export default app; diff --git a/api/lambda/hello.ts b/api/lambda/hello.ts deleted file mode 100644 index 391e306..0000000 --- a/api/lambda/hello.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default async () => ({ - message: 'Hello Modern.js', -}); - -export const post = async () => ({ - message: 'Hello Modern.js', -}); diff --git a/api/lambda/translate.ts b/api/lambda/translate.ts new file mode 100644 index 0000000..6eef766 --- /dev/null +++ b/api/lambda/translate.ts @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +/* eslint-disable node/prefer-global/text-decoder */ +import type { RequestOption } from '@modern-js/runtime/server'; + +export async function post(context: RequestOption) { + const { text } = context.data; + + const response = await fetch('https://api.coze.cn/v3/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.COZE_API_KEY}`, + }, + body: JSON.stringify({ + bot_id: '7400050699922767910', + user_id: 'anonymous', + auto_save_history: false, + stream: true, + additional_messages: [ + { + role: 'user', + content: text, + content_type: 'text', + }, + ], + }), + }); + const reader = response.body?.getReader()!; + let result = ''; + + while (true) { + const { value, done } = await reader.read(); + const textDecoder = new TextDecoder(); + const chunk = textDecoder.decode(value); + const lines = chunk.split('\n'); + const event = lines[0]; + + if (event === 'event:conversation.message.completed' || done) { + break; + } + + if (event === 'event:conversation.message.delta') { + const json = JSON.parse(lines[1].slice(5)); + + result += json.content; + } + } + + return { + result: result.trim(), + }; +} diff --git a/package.json b/package.json index d3c289d..d23778e 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,12 @@ "@douyinfe/semi-ui": "^2.64.0", "@modern-js/plugin-bff": "^2.58.1", "@modern-js/plugin-koa": "^2.58.1", - "@modern-js/plugin-polyfill": "2.58.1", - "@modern-js/plugin-tailwindcss": "2.58.1", + "@modern-js/plugin-polyfill": "^2.58.1", + "@modern-js/plugin-tailwindcss": "^2.58.1", "@modern-js/runtime": "^2.58.1", "koa": "^2.15.3", "koa-body": "^6.0.1", + "koa-logger": "^3.2.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -44,6 +45,7 @@ "@modern-js/tsconfig": "^2.58.1", "@types/jest": "^29.5.12", "@types/koa": "^2.15.0", + "@types/koa-logger": "^3.1.5", "@types/node": "^22.4.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bb0fab..0e7b830 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,10 +18,10 @@ importers: specifier: ^2.58.1 version: 2.58.1(koa@2.15.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.4))(tsconfig-paths@4.2.0)(zod@3.23.8) '@modern-js/plugin-polyfill': - specifier: 2.58.1 + specifier: ^2.58.1 version: 2.58.1 '@modern-js/plugin-tailwindcss': - specifier: 2.58.1 + specifier: ^2.58.1 version: 2.58.1(@modern-js/runtime@2.58.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.5.4))) '@modern-js/runtime': specifier: ^2.58.1 @@ -32,6 +32,9 @@ importers: koa-body: specifier: ^6.0.1 version: 6.0.1 + koa-logger: + specifier: ^3.2.1 + version: 3.2.1 react: specifier: ^18.3.1 version: 18.3.1 @@ -60,6 +63,9 @@ importers: '@types/koa': specifier: ^2.15.0 version: 2.15.0 + '@types/koa-logger': + specifier: ^3.1.5 + version: 3.1.5 '@types/node': specifier: ^22.4.0 version: 22.4.0 @@ -1834,6 +1840,9 @@ packages: '@types/koa-compose@3.2.8': resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==} + '@types/koa-logger@3.1.5': + resolution: {integrity: sha512-N4f9GRdokJ/gLiCSvd3GGar/D74HJWzuvSJiruayCsz2e7gGkG6DQaque+kM3xo6LjyCRVUUt9HHJCSMjsXrIA==} + '@types/koa@2.15.0': resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} @@ -3666,6 +3675,9 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + humanize-number@0.0.2: + resolution: {integrity: sha512-un3ZAcNQGI7RzaWGZzQDH47HETM4Wrj6z6E4TId8Yeq9w5ZKUVB1nrT2jwFheTUjEmqcgTjXDc959jum+ai1kQ==} + husky@8.0.3: resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} engines: {node: '>=14'} @@ -4062,6 +4074,10 @@ packages: resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} engines: {node: '>= 10'} + koa-logger@3.2.1: + resolution: {integrity: sha512-MjlznhLLKy9+kG8nAXKJLM0/ClsQp/Or2vI3a5rbSQmgl8IJBQO0KI5FA70BvW+hqjtxjp49SpH2E7okS6NmHg==} + engines: {node: '>= 7.6.0'} + koa-router@10.1.1: resolution: {integrity: sha512-z/OzxVjf5NyuNO3t9nJpx7e1oR3FSBAauiwXtMQu4ppcnuNZzTaQ4p21P8A6r2Es8uJJM339oc4oVW+qX7SqnQ==} engines: {node: '>= 8.0.0'} @@ -4683,6 +4699,9 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + passthrough-counter@1.0.0: + resolution: {integrity: sha512-Wy8PXTLqPAN0oEgBrlnsXPMww3SYJ44tQ8aVrGAI4h4JZYCS0oYqsPqtPR8OhJpv6qFbpbB7XAn0liKV7EXubA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -8781,6 +8800,10 @@ snapshots: dependencies: '@types/koa': 2.15.0 + '@types/koa-logger@3.1.5': + dependencies: + '@types/koa': 2.15.0 + '@types/koa@2.15.0': dependencies: '@types/accepts': 1.3.7 @@ -11034,6 +11057,8 @@ snapshots: human-signals@5.0.0: {} + humanize-number@0.0.2: {} + husky@8.0.3: {} husky@9.1.4: {} @@ -11418,6 +11443,13 @@ snapshots: co: 4.6.0 koa-compose: 4.1.0 + koa-logger@3.2.1: + dependencies: + bytes: 3.1.2 + chalk: 2.4.2 + humanize-number: 0.0.2 + passthrough-counter: 1.0.0 + koa-router@10.1.1: dependencies: debug: 4.3.6(supports-color@5.5.0) @@ -12329,6 +12361,8 @@ snapshots: no-case: 3.0.4 tslib: 2.6.3 + passthrough-counter@1.0.0: {} + path-browserify@1.0.1: {} path-exists@3.0.0: {} diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 0000000..015bead --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,24 @@ +import { useState, useCallback } from 'react'; + +export function useLoading Promise>( + handler: F, +): [F, boolean, Error | undefined] { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const newHandler = useCallback( + (...args: any) => { + setLoading(true); + + return handler(...args) + .catch(e => { + setError(e); + }) + .finally(() => { + setLoading(false); + }); + }, + [handler], + ); + + return [newHandler as F, loading, error]; +} diff --git a/src/routes/page.module.scss b/src/routes/page.module.scss index 656f761..93139a5 100644 --- a/src/routes/page.module.scss +++ b/src/routes/page.module.scss @@ -1,5 +1,9 @@ .container { padding: 24px 60px; + + :global(.semi-input-textarea-readonly) { + color: var(--semi-color-text-0); + } } .title { diff --git a/src/routes/page.tsx b/src/routes/page.tsx index f6aa1ea..1886679 100644 --- a/src/routes/page.tsx +++ b/src/routes/page.tsx @@ -1,5 +1,8 @@ import { useState } from 'react'; -import { Button, TextArea } from '@douyinfe/semi-ui'; +import { Button, TextArea, Toast } from '@douyinfe/semi-ui'; + +import { post as translate } from '@api/translate'; +import { useLoading } from '../hooks'; import styles from './page.module.scss'; @@ -7,6 +10,27 @@ function IndexPage(): JSX.Element { const [text, setText] = useState(''); const [result, setResult] = useState(''); + const [handleTranslate, loading] = useLoading(async () => { + const { result } = await translate({ + data: { + text, + }, + }); + + setResult(result); + }); + + const handleCopy = async () => { + await navigator.clipboard.writeText(result); + + Toast.success('Copied'); + }; + + const handleReset = () => { + setText(''); + setResult(''); + }; + return (
English
@@ -25,9 +49,18 @@ function IndexPage(): JSX.Element { onChange={setResult} />
- - - + + +
);