From 97d3a6277deccf9051f04a2f14562329e420320b Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 14 Apr 2023 20:35:08 +0800 Subject: [PATCH] feat: init --- .editorconfig | 22 + .eslintrc.json | 28 + .gitignore | 49 + .vscode/launch.json | 28 + .vscode/settings.json | 27 + README.md | 53 + app/api/chat-messages/route.ts | 17 + app/api/conversations/route.ts | 11 + app/api/messages/route.ts | 13 + app/api/parameters/route.ts | 11 + app/api/sdk.js | 102 ++ app/api/site/route.ts | 11 + app/api/utils/common.ts | 20 + app/api/utils/stream.ts | 25 + app/components/app-unavailable.tsx | 31 + app/components/base/app-icon/index.tsx | 36 + app/components/base/app-icon/style.module.css | 15 + .../base/auto-height-textarea/index.tsx | 73 ++ app/components/base/button/index.tsx | 44 + app/components/base/loading/index.tsx | 31 + app/components/base/loading/style.css | 41 + app/components/base/markdown.tsx | 45 + app/components/base/select/index.tsx | 216 ++++ app/components/base/spinner/index.tsx | 24 + app/components/base/toast/index.tsx | 131 +++ app/components/base/toast/style.module.css | 43 + app/components/base/tooltip/index.tsx | 46 + app/components/chat/icons/answer.svg | 3 + app/components/chat/icons/default-avatar.jpg | Bin 0 -> 2183 bytes app/components/chat/icons/edit.svg | 3 + app/components/chat/icons/question.svg | 3 + app/components/chat/icons/robot.svg | 10 + app/components/chat/icons/send-active.svg | 3 + app/components/chat/icons/send.svg | 3 + app/components/chat/icons/typing.svg | 19 + app/components/chat/icons/user.svg | 10 + app/components/chat/index.tsx | 336 ++++++ app/components/chat/style.module.css | 90 ++ app/components/config-scence/index.tsx | 13 + app/components/header.tsx | 48 + app/components/index.tsx | 461 ++++++++ app/components/sidebar/card.module.css | 3 + app/components/sidebar/card.tsx | 19 + app/components/sidebar/index.tsx | 87 ++ app/components/value-panel/index.tsx | 79 ++ app/components/value-panel/style.module.css | 3 + app/components/welcome/index.tsx | 339 ++++++ app/components/welcome/massive-component.tsx | 90 ++ app/components/welcome/style.module.css | 22 + app/layout.tsx | 25 + app/page.tsx | 15 + app/styles/globals.css | 128 ++ app/styles/markdown.scss | 1041 +++++++++++++++++ config/index.ts | 25 + hooks/use-breakpoints.ts | 27 + hooks/use-conversation.ts | 66 ++ i18n/client.ts | 18 + i18n/i18next-config.ts | 38 + i18n/i18next-serverside-config.ts | 26 + i18n/index.ts | 6 + i18n/lang/app.en.ts | 33 + i18n/lang/app.zh.ts | 28 + i18n/lang/common.en.ts | 22 + i18n/lang/common.zh.ts | 22 + i18n/server.ts | 29 + next.config.js | 21 + package.json | 68 ++ postcss.config.js | 6 + public/favicon.ico | Bin 0 -> 15406 bytes service/base.ts | 223 ++++ service/index.ts | 37 + tailwind.config.js | 66 ++ tsconfig.json | 42 + types/app.ts | 75 ++ typography.js | 357 ++++++ utils/prompt.ts | 12 + utils/string.ts | 6 + 77 files changed, 5299 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 app/api/chat-messages/route.ts create mode 100644 app/api/conversations/route.ts create mode 100644 app/api/messages/route.ts create mode 100644 app/api/parameters/route.ts create mode 100644 app/api/sdk.js create mode 100644 app/api/site/route.ts create mode 100644 app/api/utils/common.ts create mode 100644 app/api/utils/stream.ts create mode 100644 app/components/app-unavailable.tsx create mode 100644 app/components/base/app-icon/index.tsx create mode 100644 app/components/base/app-icon/style.module.css create mode 100644 app/components/base/auto-height-textarea/index.tsx create mode 100644 app/components/base/button/index.tsx create mode 100644 app/components/base/loading/index.tsx create mode 100644 app/components/base/loading/style.css create mode 100644 app/components/base/markdown.tsx create mode 100644 app/components/base/select/index.tsx create mode 100644 app/components/base/spinner/index.tsx create mode 100644 app/components/base/toast/index.tsx create mode 100644 app/components/base/toast/style.module.css create mode 100644 app/components/base/tooltip/index.tsx create mode 100644 app/components/chat/icons/answer.svg create mode 100644 app/components/chat/icons/default-avatar.jpg create mode 100644 app/components/chat/icons/edit.svg create mode 100644 app/components/chat/icons/question.svg create mode 100644 app/components/chat/icons/robot.svg create mode 100644 app/components/chat/icons/send-active.svg create mode 100644 app/components/chat/icons/send.svg create mode 100644 app/components/chat/icons/typing.svg create mode 100644 app/components/chat/icons/user.svg create mode 100644 app/components/chat/index.tsx create mode 100644 app/components/chat/style.module.css create mode 100644 app/components/config-scence/index.tsx create mode 100644 app/components/header.tsx create mode 100644 app/components/index.tsx create mode 100644 app/components/sidebar/card.module.css create mode 100644 app/components/sidebar/card.tsx create mode 100644 app/components/sidebar/index.tsx create mode 100644 app/components/value-panel/index.tsx create mode 100644 app/components/value-panel/style.module.css create mode 100644 app/components/welcome/index.tsx create mode 100644 app/components/welcome/massive-component.tsx create mode 100644 app/components/welcome/style.module.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/styles/globals.css create mode 100644 app/styles/markdown.scss create mode 100644 config/index.ts create mode 100644 hooks/use-breakpoints.ts create mode 100644 hooks/use-conversation.ts create mode 100644 i18n/client.ts create mode 100644 i18n/i18next-config.ts create mode 100644 i18n/i18next-serverside-config.ts create mode 100644 i18n/index.ts create mode 100644 i18n/lang/app.en.ts create mode 100644 i18n/lang/app.zh.ts create mode 100644 i18n/lang/common.en.ts create mode 100644 i18n/lang/common.zh.ts create mode 100644 i18n/server.ts create mode 100644 next.config.js create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/favicon.ico create mode 100644 service/base.ts create mode 100644 service/index.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 types/app.ts create mode 100644 typography.js create mode 100644 utils/prompt.ts create mode 100644 utils/string.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e1d3f0b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,tsx}] +charset = utf-8 +indent_style = space +indent_size = 2 + + +# Matches the exact files either package.json or .travis.yml +[{package.json,.travis.yml}] +indent_style = space +indent_size = 2 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..9d8ea2c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "extends": [ + "@antfu", + "plugin:react-hooks/recommended" + ], + "rules": { + "@typescript-eslint/consistent-type-definitions": [ + "error", + "type" + ], + "no-console": "off", + "indent": "off", + "@typescript-eslint/indent": [ + "error", + 2, + { + "SwitchCase": 1, + "flatTernaryExpressions": false, + "ignoredNodes": [ + "PropertyDefinition[decorators]", + "TSUnionType", + "FunctionExpression[params]:has(Identifier[decorators])" + ] + } + ], + "react-hooks/exhaustive-deps": "warn" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c01328 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# npm +package-lock.json + +# yarn +.pnp.cjs +.pnp.loader.mjs +.yarn/ +yarn.lock +.yarnrc.yml + +# pmpm +pnpm-lock.yaml \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9e36291 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug full stack", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "serverReadyAction": { + "pattern": "started server on .+, url: (https?://.+)", + "uriFormat": "%s", + "action": "debugWithChrome" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..edbfba9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "typescript.tsdk": ".yarn/cache/typescript-patch-72dc6f164f-ab417a2f39.zip/node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "prettier.enable": false, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "[python]": { + "editor.formatOnType": true + }, + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6e6d1e --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Conversion Web App Template +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Set App Info +Set app info in `config/index.ts`. Includes: +- APP_ID +- API_KEY +- APP_INFO + +## Getting Started +First, install dependencies: +```bash +npm install +# or +yarn +# or +pnpm install +``` + +Then, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/app/api/chat-messages/route.ts b/app/api/chat-messages/route.ts new file mode 100644 index 0000000..a046f95 --- /dev/null +++ b/app/api/chat-messages/route.ts @@ -0,0 +1,17 @@ +import { type NextRequest } from 'next/server' +import { getInfo, client } from '@/app/api/utils/common' +import { OpenAIStream } from '@/app/api/utils/stream' + +export async function POST(request: NextRequest) { + const body = await request.json() + const { + inputs, + query, + conversation_id: conversationId, + response_mode: responseMode + } = body + const { user } = getInfo(request); + const res = await client.createChatMessage(inputs, query, user, responseMode, conversationId) + const stream = await OpenAIStream(res as any) + return new Response(stream as any) +} \ No newline at end of file diff --git a/app/api/conversations/route.ts b/app/api/conversations/route.ts new file mode 100644 index 0000000..9a73a3d --- /dev/null +++ b/app/api/conversations/route.ts @@ -0,0 +1,11 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { getInfo, setSession, client } from '@/app/api/utils/common' + +export async function GET(request: NextRequest) { + const { sessionId, user } = getInfo(request); + const { data }: any = await client.getConversations(user); + return NextResponse.json(data, { + headers: setSession(sessionId) + }) +} \ No newline at end of file diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts new file mode 100644 index 0000000..57a027c --- /dev/null +++ b/app/api/messages/route.ts @@ -0,0 +1,13 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { getInfo, setSession, client } from '@/app/api/utils/common' + +export async function GET(request: NextRequest) { + const { sessionId, user } = getInfo(request); + const { searchParams } = new URL(request.url); + const conversationId = searchParams.get('conversation_id') + const { data }: any = await client.getConversationMessages(user, conversationId as string); + return NextResponse.json(data, { + headers: setSession(sessionId) + }) +} \ No newline at end of file diff --git a/app/api/parameters/route.ts b/app/api/parameters/route.ts new file mode 100644 index 0000000..1c1d917 --- /dev/null +++ b/app/api/parameters/route.ts @@ -0,0 +1,11 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { getInfo, setSession, client } from '@/app/api/utils/common' + +export async function GET(request: NextRequest) { + const { sessionId, user } = getInfo(request); + const { data } = await client.getApplicationParameters(user); + return NextResponse.json(data as object, { + headers: setSession(sessionId) + }) +} \ No newline at end of file diff --git a/app/api/sdk.js b/app/api/sdk.js new file mode 100644 index 0000000..6e8c51d --- /dev/null +++ b/app/api/sdk.js @@ -0,0 +1,102 @@ +import axios from 'axios' + +export class LangGeniusClient { + constructor(apiKey, baseUrl = 'https://api.langgenius.ai/v1') { + this.apiKey = apiKey + this.baseUrl = baseUrl + } + + async sendRequest(method, endpoint, data = null, params = null, stream = false) { + const headers = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + } + + const url = `${this.baseUrl}${endpoint}` + let response + if (!stream) { + response = await axios({ + method, + url, + data, + params, + headers, + responseType: stream ? 'stream' : 'json', + }) + } else { + response = await fetch(url, { + headers, + method, + body: JSON.stringify(data), + }) + } + + return response + } + + messageFeedback(messageId, rating, user) { + const data = { + rating, + user, + } + return this.sendRequest('POST', `/messages/${messageId}/feedbacks`, data) + } + + getApplicationParameters(user) { + const params = { user } + return this.sendRequest('GET', '/parameters', null, params) + } +} + +export class CompletionClient extends LangGeniusClient { + createCompletionMessage(inputs, query, responseMode, user) { + const data = { + inputs, + query, + responseMode, + user, + } + return this.sendRequest('POST', '/completion-messages', data, null, responseMode === 'streaming') + } +} + +export class ChatClient extends LangGeniusClient { + createChatMessage(inputs, query, user, responseMode = 'blocking', conversationId = null) { + const data = { + inputs, + query, + user, + responseMode, + } + if (conversationId) + data.conversation_id = conversationId + + return this.sendRequest('POST', '/chat-messages', data, null, responseMode === 'streaming') + } + + getConversationMessages(user, conversationId = '', firstId = null, limit = null) { + const params = { user } + + if (conversationId) + params.conversation_id = conversationId + + if (firstId) + params.first_id = firstId + + if (limit) + params.limit = limit + + return this.sendRequest('GET', '/messages', null, params) + } + + getConversations(user, firstId = null, limit = null, pinned = null) { + const params = { user, first_id: firstId, limit, pinned } + return this.sendRequest('GET', '/conversations', null, params) + } + + renameConversation(conversationId, name, user) { + const data = { name, user } + return this.sendRequest('PATCH', `/conversations/${conversationId}`, data) + } +} + diff --git a/app/api/site/route.ts b/app/api/site/route.ts new file mode 100644 index 0000000..86aff37 --- /dev/null +++ b/app/api/site/route.ts @@ -0,0 +1,11 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { getInfo, setSession } from '@/app/api/utils/common' +import { APP_INFO } from '@/config' + +export async function GET(request: NextRequest) { + const { sessionId } = getInfo(request); + return NextResponse.json(APP_INFO, { + headers: setSession(sessionId) + }) +} \ No newline at end of file diff --git a/app/api/utils/common.ts b/app/api/utils/common.ts new file mode 100644 index 0000000..6a3a46a --- /dev/null +++ b/app/api/utils/common.ts @@ -0,0 +1,20 @@ +import { type NextRequest } from 'next/server' +import { APP_ID, API_KEY } from '@/config' +import { ChatClient } from '../sdk' +const userPrefix = `user_${APP_ID}:`; +const uuid = require('uuid') + +export const getInfo = (request: NextRequest) => { + const sessionId = request.cookies.get('session_id')?.value || uuid.v4(); + const user = userPrefix + sessionId; + return { + sessionId, + user + } +} + +export const setSession = (sessionId: string) => { + return { 'Set-Cookie': `session_id=${sessionId}` } +} + +export const client = new ChatClient(API_KEY) \ No newline at end of file diff --git a/app/api/utils/stream.ts b/app/api/utils/stream.ts new file mode 100644 index 0000000..2da1359 --- /dev/null +++ b/app/api/utils/stream.ts @@ -0,0 +1,25 @@ +export async function OpenAIStream(res: { body: any }) { + const reader = res.body.getReader(); + + const stream = new ReadableStream({ + // https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams + // https://github.com/whichlight/chatgpt-api-streaming/blob/master/pages/api/OpenAIStream.ts + start(controller) { + return pump(); + function pump() { + return reader.read().then(({ done, value }: any) => { + // When no more data needs to be consumed, close the stream + if (done) { + controller.close(); + return; + } + // Enqueue the next data chunk into our target stream + controller.enqueue(value); + return pump(); + }); + } + }, + }); + + return stream; +} \ No newline at end of file diff --git a/app/components/app-unavailable.tsx b/app/components/app-unavailable.tsx new file mode 100644 index 0000000..ce4d7c7 --- /dev/null +++ b/app/components/app-unavailable.tsx @@ -0,0 +1,31 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' + +type IAppUnavailableProps = { + isUnknwonReason: boolean + errMessage?: string +} + +const AppUnavailable: FC = ({ + isUnknwonReason, + errMessage, +}) => { + const { t } = useTranslation() + let message = errMessage + if (!errMessage) { + message = (isUnknwonReason ? t('app.common.appUnkonwError') : t('app.common.appUnavailable')) as string + } + + return ( +
+

{(errMessage || isUnknwonReason) ? 500 : 404}

+
{message}
+
+ ) +} +export default React.memo(AppUnavailable) diff --git a/app/components/base/app-icon/index.tsx b/app/components/base/app-icon/index.tsx new file mode 100644 index 0000000..b70991b --- /dev/null +++ b/app/components/base/app-icon/index.tsx @@ -0,0 +1,36 @@ +import type { FC } from 'react' +import classNames from 'classnames' +import style from './style.module.css' + +export type AppIconProps = { + size?: 'tiny' | 'small' | 'medium' | 'large' + rounded?: boolean + icon?: string + background?: string + className?: string +} + +const AppIcon: FC = ({ + size = 'medium', + rounded = false, + background, + className, +}) => { + return ( + + 🤖 + + ) +} + +export default AppIcon diff --git a/app/components/base/app-icon/style.module.css b/app/components/base/app-icon/style.module.css new file mode 100644 index 0000000..43098fd --- /dev/null +++ b/app/components/base/app-icon/style.module.css @@ -0,0 +1,15 @@ +.appIcon { + @apply flex items-center justify-center relative w-9 h-9 text-lg bg-teal-100 rounded-lg grow-0 shrink-0; +} +.appIcon.large { + @apply w-10 h-10; +} +.appIcon.small { + @apply w-8 h-8; +} +.appIcon.tiny { + @apply w-6 h-6 text-base; +} +.appIcon.rounded { + @apply rounded-full; +} diff --git a/app/components/base/auto-height-textarea/index.tsx b/app/components/base/auto-height-textarea/index.tsx new file mode 100644 index 0000000..0fe7ef8 --- /dev/null +++ b/app/components/base/auto-height-textarea/index.tsx @@ -0,0 +1,73 @@ +import { forwardRef, useEffect, useRef } from 'react' +import cn from 'classnames' + +type IProps = { + placeholder?: string + value: string + onChange: (e: React.ChangeEvent) => void + className?: string + minHeight?: number + maxHeight?: number + autoFocus?: boolean + controlFocus?: number + onKeyDown?: (e: React.KeyboardEvent) => void + onKeyUp?: (e: React.KeyboardEvent) => void +} + +const AutoHeightTextarea = forwardRef( + ( + { value, onChange, placeholder, className, minHeight = 36, maxHeight = 96, autoFocus, controlFocus, onKeyDown, onKeyUp }: IProps, + outerRef: any, + ) => { + const ref = outerRef || useRef(null) + + const doFocus = () => { + if (ref.current) { + ref.current.setSelectionRange(value.length, value.length) + ref.current.focus() + return true + } + return false + } + + const focus = () => { + if (!doFocus()) { + let hasFocus = false + const runId = setInterval(() => { + hasFocus = doFocus() + if (hasFocus) + clearInterval(runId) + }, 100) + } + } + + useEffect(() => { + if (autoFocus) + focus() + }, []) + useEffect(() => { + if (controlFocus) + focus() + }, [controlFocus]) + + return ( +
+
+ {!value ? placeholder : value.replace(/\n$/, '\n ')} +
+