diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..e1d3f0b9928c1e0ea8a02adbfd47f7e320980c2b --- /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 0000000000000000000000000000000000000000..9d8ea2cc8d8b7d2f535ef1ae9552feed7a9f6cdf --- /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 0000000000000000000000000000000000000000..2c013281550e9055fc1ce1eda42ba23762b81293 --- /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 0000000000000000000000000000000000000000..9e36291e0769b335aed1c378e7d813fd1d2059b1 --- /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 0000000000000000000000000000000000000000..e35fdc6dc18a28f4f4bdaedad9b0d74d93db3487 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,32 @@ +{ + "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" + }, + "i18n-ally.localesPaths": [ + "i18n", + "i18n/lang", + "app/api/messages" + ] +} \ No newline at end of file diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 0000000000000000000000000000000000000000..5ee0eadbc17bb6ce0f2ee5544af8c29ce855a665 --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,67 @@ +# 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). + +## Config App +Config app in `config/index.ts`.Please config: +- APP_ID +- API_KEY + +More config: +```js +export const APP_INFO: AppInfo = { + "title": 'Chat APP', + "description": '', + "copyright": '', + "privacy_policy": '', + "default_language": 'zh-Hans' +} + +export const isShowPrompt = true +export const promptTemplate = '' +``` + +## 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. + +## Using Docker + +``` +docker build . -t /webapp-conversation:latest +# now you can access it in port 3000 +docker run -p 3000:3000 /webapp-conversation:latest +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +## 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/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..de34e1d832806862e09af49fee67d8a9ec7542b9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM --platform=linux/amd64 node:19-bullseye-slim + +WORKDIR /app + +COPY . . + +RUN yarn install +RUN yarn build + +EXPOSE 3000 + +CMD ["yarn","start"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..4c3b5202ad9d60be921fe8b60e4b7a52e394dd4b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 DorkAI Networking + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3894460248b60e2e23ba072deb87ed5de8fd1127..8300f3673c81da328054ca210ec9c4139f1ce369 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ --- title: ChatUIPro -emoji: 📉 -colorFrom: blue -colorTo: green +emoji: 🐨 +colorFrom: indigo +colorTo: blue sdk: docker pinned: false +license: openrail --- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/app/api/chat-messages/route.ts b/app/api/chat-messages/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..a046f95157428fadc424ac20d0ec6c8ec071aecc --- /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 0000000000000000000000000000000000000000..9a73a3d28e131eb0da9b9d007b1da67d838540dd --- /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/[messageId]/feedbacks/route.ts b/app/api/messages/[messageId]/feedbacks/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..f41a136779d8ae9d5a06aebbdd1db175b53079ff --- /dev/null +++ b/app/api/messages/[messageId]/feedbacks/route.ts @@ -0,0 +1,16 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { getInfo, client } from '@/app/api/utils/common' + +export async function POST(request: NextRequest, { params }: { + params: { messageId: string } +}) { + const body = await request.json() + const { + rating + } = body + const { messageId } = params + const { user } = getInfo(request); + const { data } = await client.messageFeedback(messageId, rating, user) + return NextResponse.json(data) +} diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..57a027cdcb1449fb1aaffcaab8b31d1650cb00ab --- /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 0000000000000000000000000000000000000000..1c1d917a30c08139a47dbe5a0fac9b80f680ecb0 --- /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/utils/common.ts b/app/api/utils/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..a26c885b5a72cad3c90f76d55a214f26edcc0a93 --- /dev/null +++ b/app/api/utils/common.ts @@ -0,0 +1,21 @@ +import { type NextRequest } from 'next/server' +import { APP_ID, API_KEY, API_URL } from '@/config' +import { ChatClient } from 'dify-client' +import { v4 } from 'uuid' + +const userPrefix = `user_${APP_ID}:`; + +export const getInfo = (request: NextRequest) => { + const sessionId = request.cookies.get('session_id')?.value || 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, API_URL ? API_URL : undefined) diff --git a/app/api/utils/stream.ts b/app/api/utils/stream.ts new file mode 100644 index 0000000000000000000000000000000000000000..2da1359e4c629b4d50aa8b757db1ef858ee236b8 --- /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 0000000000000000000000000000000000000000..ce4d7c7524af88495cf2c7315bed8df39e2954b1 --- /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 0000000000000000000000000000000000000000..b70991b92bceb4ed17622c614b222934ec929eb8 --- /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 0000000000000000000000000000000000000000..43098fd9557206d563fab9cd5889397ede9484fd --- /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 0000000000000000000000000000000000000000..0fe7ef80be87ef60fd37e416821989108190a4f3 --- /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 ')} +
+