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/.env.example b/.env.example new file mode 100644 index 0000000..49b936d --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# APP ID +NEXT_PUBLIC_APP_ID= +# APP API key +NEXT_PUBLIC_APP_KEY= +# API url prefix +NEXT_PUBLIC_API_URL= diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..631cb2b --- /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" + } +} diff --git a/.gitignore b/.gitignore index 5ef6a52..9c0404b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,7 @@ # dependencies /node_modules /.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions +.pnp.js # testing /coverage @@ -22,6 +17,7 @@ # misc .DS_Store +.vscode *.pem # debug @@ -30,8 +26,9 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) -.env* +# local env files +.env*.local +.env # vercel .vercel @@ -39,3 +36,16 @@ yarn-error.log* # 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 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2a80d06 --- /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" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b30daf1 --- /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": "explicit" + }, + "[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/Dockerfile b/Dockerfile new file mode 100644 index 0000000..de34e1d --- /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 0000000..c729e97 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Dify + +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 e215bc4..14fb2a5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,46 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Conversation 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 +Create a file named `.env.local` in the current directory and copy the contents from `.env.example`. Setting the following content: +``` +# APP ID: This is the unique identifier for your app. You can find it in the app's detail page URL. +# For example, in the URL `https://cloud.dify.ai/app/xxx/workflow`, the value `xxx` is your APP ID. +NEXT_PUBLIC_APP_ID= + +# APP API Key: This is the key used to authenticate your app's API requests. +# You can generate it on the app's "API Access" page by clicking the "API Key" button in the top-right corner. +NEXT_PUBLIC_APP_KEY= + +# APP URL: This is the API's base URL. If you're using the Dify cloud service, set it to: https://api.dify.ai/v1. +NEXT_PUBLIC_API_URL= +``` + +Config more in `config/index.ts` file: +```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 +``` -First, run the development server: +Then, run the development server: ```bash npm run dev @@ -10,16 +48,19 @@ npm run dev yarn dev # or pnpm dev -# or -bun 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. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - ## Learn More To learn more about Next.js, take a look at the following resources: @@ -27,10 +68,13 @@ 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! +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel +> ⚠️ If you are using [Vercel Hobby](https://vercel.com/pricing), your message will be truncated due to the limitation of 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/app/building-your-application/deploying) for more details. +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..849be1f --- /dev/null +++ b/app/api/chat-messages/route.ts @@ -0,0 +1,16 @@ +import { type NextRequest } from 'next/server' +import { client, getInfo } from '@/app/api/utils/common' + +export async function POST(request: NextRequest) { + const body = await request.json() + const { + inputs, + query, + files, + conversation_id: conversationId, + response_mode: responseMode, + } = body + const { user } = getInfo(request) + const res = await client.createChatMessage(inputs, query, user, responseMode, conversationId, files) + return new Response(res.data as any) +} diff --git a/app/api/conversations/[conversationId]/name/route.ts b/app/api/conversations/[conversationId]/name/route.ts new file mode 100644 index 0000000..d9dd327 --- /dev/null +++ b/app/api/conversations/[conversationId]/name/route.ts @@ -0,0 +1,19 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { client, getInfo } from '@/app/api/utils/common' + +export async function POST(request: NextRequest, { params }: { + params: { conversationId: string } +}) { + const body = await request.json() + const { + auto_generate, + name, + } = body + const { conversationId } = params + const { user } = getInfo(request) + + // auto generate name + const { data } = await client.renameConversation(conversationId, name, user, auto_generate) + return NextResponse.json(data) +} diff --git a/app/api/conversations/route.ts b/app/api/conversations/route.ts new file mode 100644 index 0000000..642da1b --- /dev/null +++ b/app/api/conversations/route.ts @@ -0,0 +1,19 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { client, getInfo, setSession } from '@/app/api/utils/common' + +export async function GET(request: NextRequest) { + const { sessionId, user } = getInfo(request) + try { + const { data }: any = await client.getConversations(user) + return NextResponse.json(data, { + headers: setSession(sessionId), + }) + } + catch (error: any) { + return NextResponse.json({ + data: [], + error: error.message, + }) + } +} diff --git a/app/api/file-upload/route.ts b/app/api/file-upload/route.ts new file mode 100644 index 0000000..fa09906 --- /dev/null +++ b/app/api/file-upload/route.ts @@ -0,0 +1,15 @@ +import { type NextRequest } from 'next/server' +import { client, getInfo } from '@/app/api/utils/common' + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData() + const { user } = getInfo(request) + formData.append('user', user) + const res = await client.fileUpload(formData) + return new Response(res.data.id as any) + } + catch (e: any) { + return new Response(e.message) + } +} diff --git a/app/api/messages/[messageId]/feedbacks/route.ts b/app/api/messages/[messageId]/feedbacks/route.ts new file mode 100644 index 0000000..c701e15 --- /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 { client, getInfo } 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 0000000..0edafe7 --- /dev/null +++ b/app/api/messages/route.ts @@ -0,0 +1,13 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { client, getInfo, setSession } 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), + }) +} diff --git a/app/api/parameters/route.ts b/app/api/parameters/route.ts new file mode 100644 index 0000000..f133da8 --- /dev/null +++ b/app/api/parameters/route.ts @@ -0,0 +1,16 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { client, getInfo, setSession } from '@/app/api/utils/common' + +export async function GET(request: NextRequest) { + const { sessionId, user } = getInfo(request) + try { + const { data } = await client.getApplicationParameters(user) + return NextResponse.json(data as object, { + headers: setSession(sessionId), + }) + } + catch (error) { + return NextResponse.json([]) + } +} diff --git a/app/api/utils/common.ts b/app/api/utils/common.ts new file mode 100644 index 0000000..109ee4b --- /dev/null +++ b/app/api/utils/common.ts @@ -0,0 +1,21 @@ +import { type NextRequest } from 'next/server' +import { ChatClient } from 'dify-client' +import { v4 } from 'uuid' +import { API_KEY, API_URL, APP_ID } from '@/config' + +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 || undefined) diff --git a/app/components/app-unavailable.tsx b/app/components/app-unavailable.tsx new file mode 100644 index 0000000..495c8ce --- /dev/null +++ b/app/components/app-unavailable.tsx @@ -0,0 +1,30 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' + +type IAppUnavailableProps = { + isUnknownReason: boolean + errMessage?: string +} + +const AppUnavailable: FC = ({ + isUnknownReason, + errMessage, +}) => { + const { t } = useTranslation() + let message = errMessage + if (!errMessage) + message = (isUnknownReason ? t('app.common.appUnkonwError') : t('app.common.appUnavailable')) as string + + return ( +
+

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

+
{message}
+
+ ) +} +export default React.memo(AppUnavailable) diff --git a/src/components/base/action-button/index.css b/app/components/base/action-button/index.css similarity index 60% rename from src/components/base/action-button/index.css rename to app/components/base/action-button/index.css index 6e7355b..2cabe7a 100644 --- a/src/components/base/action-button/index.css +++ b/app/components/base/action-button/index.css @@ -1,44 +1,45 @@ -@tailwind utilities; +@tailwind components; @layer components { .action-btn { - @apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover; + @apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover } .action-btn-hover { - @apply bg-state-base-hover; + @apply bg-state-base-hover } .action-btn-disabled { - @apply cursor-not-allowed; + @apply cursor-not-allowed } .action-btn-xl { - @apply p-2 w-9 h-9 rounded-lg; + @apply p-2 w-9 h-9 rounded-lg } .action-btn-l { - @apply p-1.5 w-8 h-8 rounded-lg; + @apply p-1.5 w-8 h-8 rounded-lg } /* m is for the regular button */ .action-btn-m { - @apply p-0.5 w-6 h-6 rounded-lg; + @apply p-0.5 w-6 h-6 rounded-lg } .action-btn-xs { - @apply p-0 w-4 h-4 rounded; + @apply p-0 w-4 h-4 rounded } .action-btn.action-btn-active { - @apply text-text-accent bg-state-accent-active hover:bg-state-accent-active-alt; + @apply text-text-accent bg-state-accent-active hover:bg-state-accent-active-alt } .action-btn.action-btn-disabled { - @apply text-text-disabled; + @apply text-text-disabled } .action-btn.action-btn-destructive { - @apply text-text-destructive bg-state-destructive-hover; + @apply text-text-destructive bg-state-destructive-hover } -} + +} \ No newline at end of file diff --git a/app/components/base/action-button/index.tsx b/app/components/base/action-button/index.tsx new file mode 100644 index 0000000..c90d1a8 --- /dev/null +++ b/app/components/base/action-button/index.tsx @@ -0,0 +1,73 @@ +import type { CSSProperties } from 'react' +import React from 'react' +import { type VariantProps, cva } from 'class-variance-authority' +import classNames from '@/utils/classnames' + +enum ActionButtonState { + Destructive = 'destructive', + Active = 'active', + Disabled = 'disabled', + Default = '', + Hover = 'hover', +} + +const actionButtonVariants = cva( + 'action-btn', + { + variants: { + size: { + xs: 'action-btn-xs', + m: 'action-btn-m', + l: 'action-btn-l', + xl: 'action-btn-xl', + }, + }, + defaultVariants: { + size: 'm', + }, + }, +) + +export type ActionButtonProps = { + size?: 'xs' | 's' | 'm' | 'l' | 'xl' + state?: ActionButtonState + styleCss?: CSSProperties +} & React.ButtonHTMLAttributes & VariantProps + +function getActionButtonState(state: ActionButtonState) { + switch (state) { + case ActionButtonState.Destructive: + return 'action-btn-destructive' + case ActionButtonState.Active: + return 'action-btn-active' + case ActionButtonState.Disabled: + return 'action-btn-disabled' + case ActionButtonState.Hover: + return 'action-btn-hover' + default: + return '' + } +} + +const ActionButton = React.forwardRef( + ({ className, size, state = ActionButtonState.Default, styleCss, children, ...props }, ref) => { + return ( + + ) + }, +) +ActionButton.displayName = 'ActionButton' + +export default ActionButton +export { ActionButton, ActionButtonState, actionButtonVariants } diff --git a/app/components/base/app-icon/index.tsx b/app/components/base/app-icon/index.tsx new file mode 100644 index 0000000..48e1608 --- /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?: 'xs' | '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..f73ba60 --- /dev/null +++ b/app/components/base/app-icon/style.module.css @@ -0,0 +1,23 @@ +.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.xs { + @apply w-3 h-3 text-base; +} + +.appIcon.tiny { + @apply w-6 h-6 text-base; +} + +.appIcon.rounded { + @apply rounded-full; +} \ No newline at end of file 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..ee015fb --- /dev/null +++ b/app/components/base/auto-height-textarea/index.tsx @@ -0,0 +1,74 @@ +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, + ) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + 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 ')} +
+ - {files.length > 0 && ( -
- -
- )} -
-
- - - - {effectiveVisionSettings.enabled && ( - <> - - - )} -
-
- - -
-
- - - - ); -}; -export default Chat; diff --git a/src/components/layouts/chat/loading-anim/index.tsx b/src/components/layouts/chat/loading-anim/index.tsx deleted file mode 100644 index 2b885fc..0000000 --- a/src/components/layouts/chat/loading-anim/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -'use client'; -import type { FC } from 'react'; -import React from 'react'; -import s from './style.module.css'; - -export type ILoaidingAnimProps = { - type: 'text' | 'avatar'; -}; - -const LoaidingAnim: FC = ({ type }) => { - return
; -}; -export default React.memo(LoaidingAnim); diff --git a/src/components/layouts/chat/loading-anim/style.module.css b/src/components/layouts/chat/loading-anim/style.module.css deleted file mode 100644 index 1c1bb45..0000000 --- a/src/components/layouts/chat/loading-anim/style.module.css +++ /dev/null @@ -1,82 +0,0 @@ -.dot-flashing { - position: relative; - animation: 1s infinite linear alternate; - animation-delay: 0.5s; -} - -.dot-flashing::before, -.dot-flashing::after { - content: ''; - display: inline-block; - position: absolute; - top: 0; - animation: 1s infinite linear alternate; -} - -.dot-flashing::before { - animation-delay: 0s; -} - -.dot-flashing::after { - animation-delay: 1s; -} - -@keyframes dot-flashing { - 0% { - background-color: #667085; - } - - 50%, - 100% { - background-color: rgba(102, 112, 133, 0.3); - } -} - -@keyframes dot-flashing-avatar { - 0% { - background-color: #155eef; - } - - 50%, - 100% { - background-color: rgba(21, 94, 239, 0.3); - } -} - -.text, -.text::before, -.text::after { - width: 4px; - height: 4px; - border-radius: 50%; - background-color: #667085; - color: #667085; - animation-name: dot-flashing; -} - -.text::before { - left: -7px; -} - -.text::after { - left: 7px; -} - -.avatar, -.avatar::before, -.avatar::after { - width: 2px; - height: 2px; - border-radius: 50%; - background-color: #155eef; - color: #155eef; - animation-name: dot-flashing-avatar; -} - -.avatar::before { - left: -5px; -} - -.avatar::after { - left: 5px; -} diff --git a/src/components/layouts/chat/question/index.tsx b/src/components/layouts/chat/question/index.tsx deleted file mode 100644 index 3f0cb61..0000000 --- a/src/components/layouts/chat/question/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client'; -import type { FC } from 'react'; -import React from 'react'; -import type { IChatItem } from '../type'; -import style from './style.module.scss'; - -import { Markdown } from '@/components/base/markdown'; -import ImageGallery from '@/components/base/image-gallery'; - -type IQuestionProps = Pick & { - imgSrcs?: string[]; -}; - -const Question: FC = ({ id, content, useCurrentUserAvatar, imgSrcs }) => { - const userName = ''; - return ( -
-
-
-
- {imgSrcs && imgSrcs.length > 0 && } - -
-
-
- {useCurrentUserAvatar ? ( -
- {userName?.[0].toLocaleUpperCase()} -
- ) : ( -
- )} -
- ); -}; - -export default React.memo(Question); diff --git a/src/components/layouts/chat/question/style.module.scss b/src/components/layouts/chat/question/style.module.scss deleted file mode 100644 index 5919f54..0000000 --- a/src/components/layouts/chat/question/style.module.scss +++ /dev/null @@ -1,37 +0,0 @@ -.question-box { - display: flex; - justify-content: flex-end; - align-items: flex-start; - margin: 40px 0; - .question { - position: relative; - font-size: 14px; - line-height: 1.4; - color: black; - &::before { - content: ''; - position: absolute; - top: 0; - width: 8px; - height: 12px; - right: 0; - background: url(../icons/question.svg) no-repeat; - } - .question-child { - margin-right: 8px; - padding: 12px 16px; - background-color: #e1effe; - border-top-left-radius: 16px; - border-bottom-right-radius: 16px; - border-bottom-left-radius: 16px; - } - } - .questionIcon { - background: url(../icons/default-avatar.jpg); - background-size: contain; - border-radius: 50%; - width: 40px; - height: 40px; - flex-shrink: 0; - } -} diff --git a/src/components/layouts/chat/thought/index.tsx b/src/components/layouts/chat/thought/index.tsx deleted file mode 100644 index 7cf74f5..0000000 --- a/src/components/layouts/chat/thought/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; -import type { FC } from 'react'; -import React from 'react'; -import type { ThoughtItem, ToolInfoInThought } from '../type'; -import Tool from './tool'; -import type { Emoji } from '@/types/tools'; - -export type IThoughtProps = { - thought: ThoughtItem; - allToolIcons: Record; - isFinished: boolean; -}; - -function getValue(value: string, isValueArray: boolean, index: number) { - if (isValueArray) { - try { - return JSON.parse(value)[index]; - } catch (e) {} - } - return value; -} - -const Thought: FC = ({ thought, allToolIcons, isFinished }) => { - const [toolNames, isValueArray]: [string[], boolean] = (() => { - try { - if (Array.isArray(JSON.parse(thought.tool))) return [JSON.parse(thought.tool), true]; - } catch (e) {} - return [[thought.tool], false]; - })(); - - const toolThoughtList = toolNames.map((toolName, index) => { - return { - name: toolName, - input: getValue(thought.tool_input, isValueArray, index), - output: getValue(thought.observation, isValueArray, index), - isFinished - }; - }); - - return ( -
- {toolThoughtList.map((item: ToolInfoInThought, index) => ( - - ))} -
- ); -}; -export default React.memo(Thought); diff --git a/src/components/layouts/chat/thought/panel.tsx b/src/components/layouts/chat/thought/panel.tsx deleted file mode 100644 index c04dfa9..0000000 --- a/src/components/layouts/chat/thought/panel.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client'; -import type { FC } from 'react'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - isRequest: boolean; - toolName: string; - content: string; -}; - -const Panel: FC = ({ isRequest, toolName, content }) => { - const { t } = useTranslation(); - - return ( -
-
- {t(`tools.thought.${isRequest ? 'requestTitle' : 'responseTitle'}`)} {toolName} -
-
{content}
-
- ); -}; -export default React.memo(Panel); diff --git a/src/components/layouts/chat/thought/style.module.css b/src/components/layouts/chat/thought/style.module.css deleted file mode 100644 index eb281de..0000000 --- a/src/components/layouts/chat/thought/style.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.wrap { - background-color: rgba(255, 255, 255, 0.92); -} - -.wrapHoverEffect:hover { - box-shadow: - 0px 1px 2px 0px rgba(16, 24, 40, 0.06), - 0px 1px 3px 0px rgba(16, 24, 40, 0.1); -} diff --git a/src/components/layouts/chat/thought/tool.tsx b/src/components/layouts/chat/thought/tool.tsx deleted file mode 100644 index b3d28dd..0000000 --- a/src/components/layouts/chat/thought/tool.tsx +++ /dev/null @@ -1,82 +0,0 @@ -'use client'; -import type { FC } from 'react'; -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import cn from 'classnames'; -import type { ToolInfoInThought } from '../type'; -import Panel from './panel'; -import Loading02 from '@/components/base/icons/line/loading-02'; -import ChevronDown from '@/components/base/icons/line/arrows/chevron-down'; -import CheckCircle from '@/components/base/icons/solid/general/check-circle'; -import DataSetIcon from '@/components/base/icons/public/data-set'; -import type { Emoji } from '@/types/tools'; -import AppIcon from '@/components/base/app-icon'; - -type Props = { - payload: ToolInfoInThought; - allToolIcons?: Record; -}; - -const getIcon = (toolName: string, allToolIcons: Record) => { - if (toolName.startsWith('dataset-')) return ; - const icon = allToolIcons[toolName]; - if (!icon) return null; - return typeof icon === 'string' ? ( -
- ) : ( - - ); -}; - -const Tool: FC = ({ payload, allToolIcons = {} }) => { - const { t } = useTranslation(); - const { name, input, isFinished, output } = payload; - const toolName = name.startsWith('dataset-') ? t('dataset.knowledge') : name; - const [isShowDetail, setIsShowDetail] = useState(false); - const icon = getIcon(toolName, allToolIcons) as any; - return ( -
-
-
setIsShowDetail(!isShowDetail)} - > - {!isFinished && } - {isFinished && !isShowDetail && } - {isFinished && isShowDetail && icon} - - {t(`tools.thought.${isFinished ? 'used' : 'using'}`)} - - - {toolName} - - -
- {isShowDetail && ( -
- - {output && } -
- )} -
-
- ); -}; -export default React.memo(Tool); diff --git a/src/components/layouts/chat/type.ts b/src/components/layouts/chat/type.ts deleted file mode 100644 index 515e7e4..0000000 --- a/src/components/layouts/chat/type.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { VisionFile } from '@/types/app'; - -export type LogAnnotation = { - content: string; - account: { - id: string; - name: string; - email: string; - }; - created_at: number; -}; - -export type Annotation = { - id: string; - authorName: string; - logAnnotation?: LogAnnotation; - created_at?: number; -}; - -export const MessageRatings = ['like', 'dislike', null] as const; -export type MessageRating = (typeof MessageRatings)[number]; - -export type MessageMore = { - time: string; - tokens: number; - latency: number | string; -}; - -export type Feedbacktype = { - rating: MessageRating; - content?: string | null; -}; - -export type FeedbackFunc = (messageId: string, feedback: Feedbacktype) => Promise; -export type SubmitAnnotationFunc = (messageId: string, content: string) => Promise; - -export type DisplayScene = 'web' | 'console'; - -export type ToolInfoInThought = { - name: string; - input: string; - output: string; - isFinished: boolean; -}; - -export type ThoughtItem = { - id: string; - tool: string; // plugin or dataset. May has multi. - thought: string; - tool_input: string; - message_id: string; - observation: string; - position: number; - files?: string[]; - message_files?: VisionFile[]; -}; - -export type CitationItem = { - content: string; - data_source_type: string; - dataset_name: string; - dataset_id: string; - document_id: string; - document_name: string; - hit_count: number; - index_node_hash: string; - segment_id: string; - segment_position: number; - score: number; - word_count: number; -}; - -export type IChatItem = { - id: string; - content: string; - citation?: CitationItem[]; - /** - * Specific message type - */ - isAnswer: boolean; - /** - * The user feedback result of this message - */ - feedback?: Feedbacktype; - /** - * The admin feedback result of this message - */ - adminFeedback?: Feedbacktype; - /** - * Whether to hide the feedback area - */ - feedbackDisabled?: boolean; - /** - * More information about this message - */ - more?: MessageMore; - annotation?: Annotation; - useCurrentUserAvatar?: boolean; - isOpeningStatement?: boolean; - suggestedQuestions?: string[]; - log?: { role: string; text: string }[]; - agent_thoughts?: ThoughtItem[]; - message_files?: VisionFile[]; -}; - -export type MessageEnd = { - id: string; - metadata: { - retriever_resources?: CitationItem[]; - annotation_reply: { - id: string; - account: { - id: string; - name: string; - }; - }; - }; -}; - -export type MessageReplace = { - id: string; - task_id: string; - answer: string; - conversation_id: string; -}; - -export type AnnotationReply = { - id: string; - task_id: string; - answer: string; - conversation_id: string; - annotation_id: string; - annotation_author_name: string; -}; diff --git a/src/components/layouts/footer/footer.module.scss b/src/components/layouts/footer/footer.module.scss deleted file mode 100644 index 51e6d75..0000000 --- a/src/components/layouts/footer/footer.module.scss +++ /dev/null @@ -1,57 +0,0 @@ -@use '@/styles/mixins.scss' as *; -@use '@/styles/variables.scss' as *; -@use '@/styles/responsive.scss' as *; - -.footer { - background: $background-dark; - padding: 26px 167px; - .footer-container { - display: flex; - justify-content: space-between; - align-items: center; - gap: 20px; - border-top: 1px solid rgba(109, 117, 143, 0.5); - padding-top: 24px; - .copyright-text { - font-size: 16px; - line-height: 1.375; - color: $text-muted; - margin: 0; - } - } -} - -.social-media-links { - display: flex; - gap: 16px; - li { - background-color: $primary-color; - border-radius: 4px; - width: 24px; - height: 24px; - a { - display: block; - width: inherit; - height: inherit; - position: relative; - } - img { - width: 12px; - height: 12px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - } -} -// responsive -@include small { - .footer { - padding: 26px 30px; - .footer-container { - flex-direction: column; - gap: 24px; - } - } -} diff --git a/src/components/layouts/footer/footer.tsx b/src/components/layouts/footer/footer.tsx deleted file mode 100644 index 374b576..0000000 --- a/src/components/layouts/footer/footer.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import FooterStyles from './footer.module.scss'; -import { FC } from 'react'; -import Image from 'next/image'; -import Link from 'next/link'; - -const Footer: FC = () => { - return ( - <> -
-
-

Copyright © 2025 Gamma | All Rights Reserved

-
    -
  • - - icons-facebook - -
  • -
  • - - icons-twitter - -
  • -
  • - - icons-instagram - -
  • -
  • - - icons-linkedIn - -
  • -
  • - - icons-youtube - -
  • -
-
-
- - ); -}; - -export default Footer; diff --git a/src/components/layouts/header/header.module.scss b/src/components/layouts/header/header.module.scss deleted file mode 100644 index bc28f0c..0000000 --- a/src/components/layouts/header/header.module.scss +++ /dev/null @@ -1,95 +0,0 @@ -@use '@/styles/mixins.scss' as *; -@use '@/styles/variables.scss' as *; -@use '@/styles/responsive.scss' as *; - -.header { - background: $background-dark; - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 30px; - .logo-brand { - display: flex; - gap: 20px; - align-items: center; - img { - flex-shrink: 0; - } - } - .brand-text { - display: flex; - flex-direction: column; - gap: 10px; - .brand-title { - font-size: 30px; - font-weight: 700; - color: $text-primary; - margin: 0; - line-height: 1.17; - } - .brand-subtitle { - font-weight: 400; - font-size: 24px; - line-height: 1.17; - color: $text-primary; - margin: 0; - } - } - .auth-nav { - display: flex; - align-items: center; - gap: 30px; - flex-shrink: 0; - .btn-login, - .btn-register { - line-height: 1.33; - min-height: 53px; - min-width: 106px; - } - a { - display: flex; - align-items: center; - justify-content: center; - } - .btn-login { - @include button(18px, 700, 10px 20px, 10px, $primary-color); - } - .btn-register { - @include button(18px, 700, 10px 20px, 10px, transparent, 1px solid $text-primary); - } - } -} -// responsive -@include medium { - .header { - flex-direction: column; - align-items: flex-start; - gap: 20px; - .brand-text { - .brand-title { - font-size: 24px; - } - .brand-subtitle { - font-size: 18px; - } - } - } -} -@include small { - .header { - .logo-brand { - flex-direction: column; - align-items: flex-start; - gap: 10px; - } - .auth-nav { - flex-direction: column; - width: 100%; - gap: 10px; - a, - button { - width: 100%; - } - } - } -} diff --git a/src/components/layouts/header/header.tsx b/src/components/layouts/header/header.tsx deleted file mode 100644 index 9a754a1..0000000 --- a/src/components/layouts/header/header.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import { FC } from 'react'; -import Image from 'next/image'; -import HeaderStyles from './header.module.scss'; -import Link from 'next/link'; -import { useAuthStore } from '@/stores/useAuthStore'; -import { signout } from '@/app/login/actions'; - -const Header: FC = () => { - const { isLogin, setLogin } = useAuthStore(); - - const handleLogout = async () => { - const rs = await signout(); - if (rs) { - setLogin(false); - } - }; - - return ( - <> -
- - logo -
-

SỞ KHOA HỌC VÀ CÔNG NGHỆ HÀ NỘI

-

- Trang AI Hỏi đáp – Tra cứu chính sách KH&CN -

-
- - -
- - ); -}; - -export default Header; diff --git a/src/components/layouts/history-chat/history-chat.module.scss b/src/components/layouts/history-chat/history-chat.module.scss deleted file mode 100644 index c081d6f..0000000 --- a/src/components/layouts/history-chat/history-chat.module.scss +++ /dev/null @@ -1,97 +0,0 @@ -@use '@/styles/mixins.scss' as *; -@use '@/styles/variables.scss' as *; -@use '@/styles/responsive.scss' as *; - -.history-chat { - display: flex; - flex-direction: column; - align-items: center; - gap: 32px; - padding: 80px 52px; - .history-chat-header { - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - gap: 24px; - .history-chat-title { - font-weight: 700; - font-size: 24px; - line-height: 1; - color: $text-secondary; - margin: 0; - } - .history-chat-subtitle { - font-weight: 400; - font-size: 16px; - line-height: 1.125; - color: $text-secondary; - margin: 0; - max-width: 420px; - } - } -} -.tabs { - display: inline-flex; - border: 1px solid $primary-color; - border-radius: 6px; - padding: 4px; - gap: 6px; - li { - @include button(16px, 400, 10px 8px, 4px, transparent); - color: $text-muted; - transition: all 0.2s; - &.active { - background-color: $primary-color !important; - color: $text-primary !important; - box-shadow: 0px 1px 4px 0px rgba(78, 159, 255, 0.2); - } - } -} -.accordion { - width: 100%; - max-width: 584px; - display: flex; - flex-direction: column; - gap: 16px; - &-item { - background-color: $background-light; - border-radius: 6px; - padding: 20px 20px 20px 24px; - box-shadow: 0px 0.5px 2px 0px rgba(25, 33, 61, 0.1); - overflow: hidden; - } - &-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 20px; - width: 100%; - text-align: left; - color: $text-secondary; - padding: 6px 1px; - &:hover { - cursor: pointer; - } - } - &-content { - padding-top: 11px; - p { - font-weight: 400; - font-size: 16px; - line-height: 1.125; - color: $text-light-muted; - margin: 0; - } - } -} -// responsive { -@include small { - .history-chat { - padding: 40px 20px; - } - .tabs { - flex-wrap: wrap; - justify-content: center; - } -} diff --git a/src/components/layouts/history-chat/history-chat.tsx b/src/components/layouts/history-chat/history-chat.tsx deleted file mode 100644 index 330d3b1..0000000 --- a/src/components/layouts/history-chat/history-chat.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client'; - -import HistoryChatStyles from './history-chat.module.scss'; -import { FC, useState, useEffect } from 'react'; -import Image from 'next/image'; - -const tabs = ['Mới nhất', 'Đã lưu', 'Thông tư', 'Luật', 'Chính sách']; - -interface HistoryChatItem { - agent_thoughts: any; - answer: string; - conversation_id: string; - created_at: number; - error: null | any; - feedback: null | any; - id: string; - inputs: any; - message_files: any; - parent_message_id: string; - query: string; - retriever_resources: any; - status: 'normal'; -} - -interface HistoryChatProps { - data: { - data: HistoryChatItem[]; - has_more: boolean; - limit: number; - }; -} - -const HistoryChat: FC = ({ data }) => { - const [activeTab, setActiveTab] = useState(0); - const [expandedItems, setExpandedItems] = useState([]); - - const handleTabClick = (index: number) => { - setActiveTab(index); - }; - - const handleToggleContent = (index: number) => { - setExpandedItems((prev) => { - if (prev.includes(index)) { - return prev.filter((item) => item !== index); - } else { - return [...prev, index]; - } - }); - }; - - return ( - <> -
-
-

Lịch sử trò chuyện

-

- Bạn có thể tìm lại các cuộc trò truyện cũ, luật, thông tư nhiều người quan tâm dưới đây nhé. -

-
-
    - {tabs.map((tab, index) => ( -
  • handleTabClick(index)} - style={{ cursor: 'pointer' }} - > - {tab} -
  • - ))} -
-
- {data?.data?.map((item, index) => { - const isExpanded = expandedItems.includes(index); - return ( -
- - {isExpanded && ( -
-

{item.answer}

-
- )} -
- ); - })} -
-
- - ); -}; - -export default HistoryChat; diff --git a/src/components/layouts/main/main.module.scss b/src/components/layouts/main/main.module.scss deleted file mode 100644 index 1c2c687..0000000 --- a/src/components/layouts/main/main.module.scss +++ /dev/null @@ -1,7 +0,0 @@ -@use '@/styles/mixins.scss' as *; -@use '@/styles/variables.scss' as *; -@use '@/styles/responsive.scss' as *; - -.main { - background: $background-dark; -} diff --git a/src/components/layouts/main/main.tsx b/src/components/layouts/main/main.tsx deleted file mode 100644 index 74c9512..0000000 --- a/src/components/layouts/main/main.tsx +++ /dev/null @@ -1,706 +0,0 @@ -'use client'; -import type { FC } from 'react'; -import React, { useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import produce, { setAutoFreeze } from 'immer'; -import { useBoolean, useGetState } from 'ahooks'; -import useConversation from '@/hooks/use-conversation'; -import Toast from '@/components/base/toast'; -import { - fetchAppParams, - fetchChatList, - fetchConversations, - generationConversationName, - sendChatMessage, - updateFeedback -} from '@/service'; -import type { ChatItem, ConversationItem, Feedbacktype, PromptConfig, VisionFile, VisionSettings } from '@/types/app'; -import { Resolution, TransferMethod, WorkflowRunningStatus } from '@/types/app'; -import { setLocaleOnClient } from '@/i18n/client'; -import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'; -import Loading from '@/components/base/loading'; -import { replaceVarWithValues, userInputsFormToPromptVariables } from '@/utils/prompt'; -import AppUnavailable from '@/components/app-unavailable'; -import { API_KEY, APP_ID, APP_INFO, isShowPrompt, promptTemplate } from '@/config'; -import type { Annotation as AnnotationType } from '@/types/log'; -import { addFileInfos, sortAgentSorts } from '@/utils/tools'; -import { useAuthStore } from '@/stores/useAuthStore'; - -export type IMainProps = { - params: any; -}; - -import MainStyles from './main.module.scss'; -import HistoryChat from '@/components/layouts/history-chat/history-chat'; -import Chat from '@/components/layouts/chat/chat'; - -const Main: FC = () => { - const { hideHistoryChat, setHideHistoryCHat } = useAuthStore(); - const { t } = useTranslation(); - const media = useBreakpoints(); - const isMobile = media === MediaType.mobile; - const hasSetAppConfig = APP_ID && API_KEY; - - /* - * app info - */ - const [historyChatList, setHistoryChatList] = useState(null); - const [appUnavailable, setAppUnavailable] = useState(false); - const [isUnknownReason, setIsUnknownReason] = useState(false); - const [promptConfig, setPromptConfig] = useState(null); - const [inited, setInited] = useState(false); - // in mobile, show sidebar by click button - const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false); - const [visionConfig, setVisionConfig] = useState({ - enabled: true, - number_limits: 2, - detail: Resolution.low, - transfer_methods: [TransferMethod.local_file] - }); - useEffect(() => { - if (APP_INFO?.title) document.title = `${APP_INFO.title}`; - }, [APP_INFO?.title]); - - // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576 - useEffect(() => { - setAutoFreeze(false); - return () => { - setAutoFreeze(true); - }; - }, []); - - /* - * conversation info - */ - const { - conversationList, - setConversationList, - currConversationId, - getCurrConversationId, - setCurrConversationId, - getConversationIdFromStorage, - isNewConversation, - currConversationInfo, - currInputs, - newConversationInputs, - resetNewConversationInputs, - setCurrInputs, - setNewConversationInfo, - setExistConversationInfo - } = useConversation(); - - const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = - useGetState(false); - const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false); - const handleStartChat = (inputs: Record) => { - createNewChat(); - setConversationIdChangeBecauseOfNew(true); - setCurrInputs(inputs); - setChatStarted(); - // parse variables in introduction - setChatList(generateNewChatListWithOpenStatement('', inputs)); - }; - const hasSetInputs = (() => { - if (!isNewConversation) return true; - - return isChatStarted; - })(); - - const conversationName = currConversationInfo?.name || (t('app.chat.newChatDefaultName') as string); - const conversationIntroduction = currConversationInfo?.introduction || ''; - const suggestedQuestions = currConversationInfo?.suggested_questions || []; - - const handleConversationSwitch = () => { - if (!inited) return; - - // update inputs of current conversation - let notSyncToStateIntroduction = ''; - let notSyncToStateInputs: Record | undefined | null = {}; - if (!isNewConversation) { - const item = conversationList.find((item) => item.id === currConversationId); - notSyncToStateInputs = item?.inputs || {}; - setCurrInputs(notSyncToStateInputs as any); - notSyncToStateIntroduction = item?.introduction || ''; - setExistConversationInfo({ - name: item?.name || '', - introduction: notSyncToStateIntroduction, - suggested_questions: suggestedQuestions - }); - } else { - notSyncToStateInputs = newConversationInputs; - setCurrInputs(notSyncToStateInputs); - } - - // update chat list of current conversation - if (!isNewConversation && !conversationIdChangeBecauseOfNew && !isResponding) { - fetchChatList(currConversationId).then((res: any) => { - const { data } = res; - const newChatList: ChatItem[] = generateNewChatListWithOpenStatement( - notSyncToStateIntroduction, - notSyncToStateInputs - ); - - data.forEach((item: any) => { - newChatList.push({ - id: `question-${item.id}`, - content: item.query, - isAnswer: false, - message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [] - }); - newChatList.push({ - id: item.id, - content: item.answer, - agent_thoughts: addFileInfos( - item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, - item.message_files - ), - feedback: item.feedback, - isAnswer: true, - message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [] - }); - }); - setChatList(newChatList); - }); - } - - if (isNewConversation && isChatStarted) setChatList(generateNewChatListWithOpenStatement()); - }; - useEffect(handleConversationSwitch, [currConversationId, inited]); - - const handleConversationIdChange = (id: string) => { - if (id === '-1') { - createNewChat(); - setConversationIdChangeBecauseOfNew(true); - } else { - setConversationIdChangeBecauseOfNew(false); - } - // trigger handleConversationSwitch - setCurrConversationId(id, APP_ID); - hideSidebar(); - }; - - /* - * chat info. chat is under conversation. - */ - const [chatList, setChatList, getChatList] = useGetState([]); - const chatListDomRef = useRef(null); - // get history chat - const getHistoryChat = async (currConversationId: string) => { - try { - const data = await fetchChatList(currConversationId); - setHistoryChatList(data); - // return data; - } catch (error) { - console.error(error); - return []; - } - }; - useEffect(() => { - currConversationId !== '-1' && getHistoryChat(currConversationId); - }, [currConversationId]); - // - useEffect(() => { - // scroll to bottom - if (chatListDomRef.current) chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight; - }, [chatList, currConversationId]); - // user can not edit inputs if user had send message - const canEditInputs = !chatList.some((item) => item.isAnswer === false) && isNewConversation; - const createNewChat = () => { - // if new chat is already exist, do not create new chat - if (conversationList.some((item) => item.id === '-1')) return; - - setConversationList( - produce(conversationList, (draft) => { - draft.unshift({ - id: '-1', - name: t('app.chat.newChatDefaultName'), - inputs: newConversationInputs, - introduction: conversationIntroduction, - suggested_questions: suggestedQuestions - }); - }) - ); - }; - - // sometime introduction is not applied to state - const generateNewChatListWithOpenStatement = (introduction?: string, inputs?: Record | null) => { - let calculatedIntroduction = introduction || conversationIntroduction || ''; - const calculatedPromptVariables = inputs || currInputs || null; - if (calculatedIntroduction && calculatedPromptVariables) - calculatedIntroduction = replaceVarWithValues( - calculatedIntroduction, - promptConfig?.prompt_variables || [], - calculatedPromptVariables - ); - - const openStatement = { - id: `${Date.now()}`, - content: calculatedIntroduction, - isAnswer: true, - feedbackDisabled: true, - isOpeningStatement: isShowPrompt, - suggestedQuestions: suggestedQuestions - }; - if (calculatedIntroduction) return [openStatement]; - - return []; - }; - - // init - useEffect(() => { - if (!hasSetAppConfig) { - setAppUnavailable(true); - return; - } - (async () => { - try { - const [conversationData, appParams] = await Promise.all([fetchConversations(), fetchAppParams()]); - - // handle current conversation id - const { data: conversations, error } = conversationData as { - data: ConversationItem[]; - error: string; - }; - if (error) { - Toast.notify({ type: 'error', message: error }); - throw new Error(error); - return; - } - const _conversationId = getConversationIdFromStorage(APP_ID); - const currentConversation = conversations.find((item) => item.id === _conversationId); - const isNotNewConversation = !!currentConversation; - - // fetch new conversation info - const { - user_input_form, - opening_statement: introduction, - file_upload, - system_parameters, - suggested_questions = [] - }: any = appParams; - setLocaleOnClient(APP_INFO.default_language, true); - setNewConversationInfo({ - name: t('app.chat.newChatDefaultName'), - introduction, - suggested_questions - }); - if (isNotNewConversation) { - setExistConversationInfo({ - name: currentConversation.name || t('app.chat.newChatDefaultName'), - introduction, - suggested_questions - }); - } - const prompt_variables = userInputsFormToPromptVariables(user_input_form); - setPromptConfig({ - prompt_template: promptTemplate, - prompt_variables - } as PromptConfig); - setVisionConfig({ - ...file_upload?.image, - image_file_size_limit: system_parameters?.system_parameters || 0 - }); - setConversationList(conversations as ConversationItem[]); - - if (isNotNewConversation) setCurrConversationId(_conversationId, APP_ID, false); - - setInited(true); - } catch (e: any) { - if (e.status === 404) { - setAppUnavailable(true); - } else { - setIsUnknownReason(true); - setAppUnavailable(true); - } - } - })(); - }, []); - - const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false); - const [abortController, setAbortController] = useState(null); - const { notify } = Toast; - const logError = (message: string) => { - notify({ type: 'error', message }); - }; - - const checkCanSend = () => { - if (currConversationId !== '-1') return true; - - if (!currInputs || !promptConfig?.prompt_variables) return true; - - const inputLens = Object.values(currInputs).length; - const promptVariablesLens = promptConfig.prompt_variables.length; - - const emptyInput = inputLens < promptVariablesLens || Object.values(currInputs).find((v) => !v); - if (emptyInput) { - logError(t('app.errorMessage.valueOfVarRequired')); - return false; - } - return true; - }; - - const [controlFocus, setControlFocus] = useState(0); - const [openingSuggestedQuestions, setOpeningSuggestedQuestions] = useState([]); - const [messageTaskId, setMessageTaskId] = useState(''); - const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false); - const [isRespondingConIsCurrCon, setIsRespondingConCurrCon, getIsRespondingConIsCurrCon] = useGetState(true); - const [userQuery, setUserQuery] = useState(''); - - const updateCurrentQA = ({ - responseItem, - questionId, - placeholderAnswerId, - questionItem - }: { - responseItem: ChatItem; - questionId: string; - placeholderAnswerId: string; - questionItem: ChatItem; - }) => { - // closesure new list is outdated. - const newListWithAnswer = produce( - getChatList().filter((item) => item.id !== responseItem.id && item.id !== placeholderAnswerId), - (draft) => { - if (!draft.find((item) => item.id === questionId)) draft.push({ ...questionItem }); - - draft.push({ ...responseItem }); - } - ); - setChatList(newListWithAnswer); - }; - - const transformToServerFile = (fileItem: any) => { - return { - type: 'image', - transfer_method: fileItem.transferMethod, - url: fileItem.url, - upload_file_id: fileItem.id - }; - }; - - const handleSend = async (message: string, files?: VisionFile[]) => { - if (isResponding) { - notify({ type: 'info', message: t('app.errorMessage.waitForResponse') }); - return; - } - const toServerInputs: Record = {}; - if (currInputs) { - Object.keys(currInputs).forEach((key) => { - const value = currInputs[key]; - if (value.supportFileType) toServerInputs[key] = transformToServerFile(value); - else if (value[0]?.supportFileType) - toServerInputs[key] = value.map((item: any) => transformToServerFile(item)); - else toServerInputs[key] = value; - }); - } - - const data: Record = { - inputs: toServerInputs, - query: message, - conversation_id: isNewConversation ? null : currConversationId - }; - - if (files && files?.length > 0) { - data.files = files.map((item) => { - if (item.transfer_method === TransferMethod.local_file) { - return { - ...item, - url: '' - }; - } - return item; - }); - } - - // question - const questionId = `question-${Date.now()}`; - const questionItem = { - id: questionId, - content: message, - isAnswer: false, - message_files: files - }; - - const placeholderAnswerId = `answer-placeholder-${Date.now()}`; - const placeholderAnswerItem = { - id: placeholderAnswerId, - content: '', - isAnswer: true - }; - - const newList = [...getChatList(), questionItem, placeholderAnswerItem]; - setChatList(newList); - - let isAgentMode = false; - - // answer - const responseItem: ChatItem = { - id: `${Date.now()}`, - content: '', - agent_thoughts: [], - message_files: [], - isAnswer: true - }; - let hasSetResponseId = false; - - const prevTempNewConversationId = getCurrConversationId() || '-1'; - let tempNewConversationId = ''; - - setRespondingTrue(); - sendChatMessage(data, { - getAbortController: (abortController) => { - setAbortController(abortController); - }, - onData: ( - message: string, - isFirstMessage: boolean, - { conversationId: newConversationId, messageId, taskId }: any - ) => { - if (!isAgentMode) { - responseItem.content = responseItem.content + message; - } else { - const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]; - if (lastThought) lastThought.thought = lastThought.thought + message; // need immer setAutoFreeze - } - if (messageId && !hasSetResponseId) { - responseItem.id = messageId; - hasSetResponseId = true; - } - - if (isFirstMessage && newConversationId) tempNewConversationId = newConversationId; - - setMessageTaskId(taskId); - // has switched to other conversation - if (prevTempNewConversationId !== getCurrConversationId()) { - setIsRespondingConCurrCon(false); - return; - } - updateCurrentQA({ - responseItem, - questionId, - placeholderAnswerId, - questionItem - }); - }, - async onCompleted(hasError?: boolean) { - if (hasError) return; - - if (getConversationIdChangeBecauseOfNew()) { - const { data: allConversations }: any = await fetchConversations(); - const newItem: any = await generationConversationName(allConversations[0].id); - - const newAllConversations = produce(allConversations, (draft: any) => { - draft[0].name = newItem.name; - }); - setConversationList(newAllConversations as any); - } - setConversationIdChangeBecauseOfNew(false); - resetNewConversationInputs(); - setChatNotStarted(); - setCurrConversationId(tempNewConversationId, APP_ID, true); - setRespondingFalse(); - }, - onFile(file) { - const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]; - if (lastThought) lastThought.message_files = [...(lastThought as any).message_files, { ...file }]; - - updateCurrentQA({ - responseItem, - questionId, - placeholderAnswerId, - questionItem - }); - }, - onThought(thought) { - isAgentMode = true; - const response = responseItem as any; - if (thought.message_id && !hasSetResponseId) { - response.id = thought.message_id; - hasSetResponseId = true; - } - // responseItem.id = thought.message_id; - if (response.agent_thoughts.length === 0) { - response.agent_thoughts.push(thought); - } else { - const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1]; - // thought changed but still the same thought, so update. - if (lastThought.id === thought.id) { - thought.thought = lastThought.thought; - thought.message_files = lastThought.message_files; - responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought; - } else { - responseItem.agent_thoughts!.push(thought); - } - } - // has switched to other conversation - if (prevTempNewConversationId !== getCurrConversationId()) { - setIsRespondingConCurrCon(false); - return false; - } - - updateCurrentQA({ - responseItem, - questionId, - placeholderAnswerId, - questionItem - }); - }, - onMessageEnd: (messageEnd) => { - if (messageEnd.metadata?.annotation_reply) { - responseItem.id = messageEnd.id; - responseItem.annotation = { - id: messageEnd.metadata.annotation_reply.id, - authorName: messageEnd.metadata.annotation_reply.account.name - } as AnnotationType; - const newListWithAnswer = produce( - getChatList().filter((item) => item.id !== responseItem.id && item.id !== placeholderAnswerId), - (draft) => { - if (!draft.find((item) => item.id === questionId)) draft.push({ ...questionItem }); - - draft.push({ - ...responseItem - }); - } - ); - setChatList(newListWithAnswer); - return; - } - // not support show citation - // responseItem.citation = messageEnd.retriever_resources - const newListWithAnswer = produce( - getChatList().filter((item) => item.id !== responseItem.id && item.id !== placeholderAnswerId), - (draft) => { - if (!draft.find((item) => item.id === questionId)) draft.push({ ...questionItem }); - - draft.push({ ...responseItem }); - } - ); - setChatList(newListWithAnswer); - }, - onMessageReplace: (messageReplace) => { - setChatList( - produce(getChatList(), (draft) => { - const current = draft.find((item) => item.id === messageReplace.id); - - if (current) current.content = messageReplace.answer; - }) - ); - }, - onError() { - setRespondingFalse(); - // role back placeholder answer - setChatList( - produce(getChatList(), (draft) => { - draft.splice( - draft.findIndex((item) => item.id === placeholderAnswerId), - 1 - ); - }) - ); - }, - onWorkflowStarted: ({ workflow_run_id, task_id }) => { - // taskIdRef.current = task_id - responseItem.workflow_run_id = workflow_run_id; - responseItem.workflowProcess = { - status: WorkflowRunningStatus.Running, - tracing: [] - }; - setChatList( - produce(getChatList(), (draft) => { - const currentIndex = draft.findIndex((item) => item.id === responseItem.id); - draft[currentIndex] = { - ...draft[currentIndex], - ...responseItem - }; - }) - ); - }, - onWorkflowFinished: ({ data }) => { - responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus; - setChatList( - produce(getChatList(), (draft) => { - const currentIndex = draft.findIndex((item) => item.id === responseItem.id); - draft[currentIndex] = { - ...draft[currentIndex], - ...responseItem - }; - }) - ); - }, - onNodeStarted: ({ data }) => { - responseItem.workflowProcess!.tracing!.push(data as any); - setChatList( - produce(getChatList(), (draft) => { - const currentIndex = draft.findIndex((item) => item.id === responseItem.id); - draft[currentIndex] = { - ...draft[currentIndex], - ...responseItem - }; - }) - ); - }, - onNodeFinished: ({ data }) => { - const currentIndex = responseItem.workflowProcess!.tracing!.findIndex( - (item) => item.node_id === data.node_id - ); - responseItem.workflowProcess!.tracing[currentIndex] = data as any; - setChatList( - produce(getChatList(), (draft) => { - const currentIndex = draft.findIndex((item) => item.id === responseItem.id); - draft[currentIndex] = { - ...draft[currentIndex], - ...responseItem - }; - }) - ); - } - }); - getHistoryChat(currConversationId); - setHideHistoryCHat(true); - }; - - const handleFeedback = async (messageId: string, feedback: Feedbacktype) => { - await updateFeedback({ - url: `/messages/${messageId}/feedbacks`, - body: { rating: feedback.rating } - }); - const newChatList = chatList.map((item) => { - if (item.id === messageId) { - return { - ...item, - feedback - }; - } - return item; - }); - setChatList(newChatList); - notify({ type: 'success', message: t('common.api.success') }); - }; - - if (appUnavailable) - return ( - - ); - - if (!APP_ID || !APP_INFO || !promptConfig) return ; - return ( - <> -
- - {!hideHistoryChat && } -
- - ); -}; - -export default Main; diff --git a/src/components/sidebar/card.module.css b/src/components/sidebar/card.module.css deleted file mode 100644 index f7e1aa2..0000000 --- a/src/components/sidebar/card.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.card:hover { - background: linear-gradient(0deg, rgba(235, 245, 255, 0.4), rgba(235, 245, 255, 0.4)), #ffffff; -} diff --git a/src/components/sidebar/card.tsx b/src/components/sidebar/card.tsx deleted file mode 100644 index b3202a9..0000000 --- a/src/components/sidebar/card.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import s from './card.module.css'; - -type PropType = { - children: React.ReactNode; - text?: string; -}; -function Card({ children, text }: PropType) { - const { t } = useTranslation(); - return ( -
-
{text ?? t('app.chat.powerBy')}
- {children} -
- ); -} - -export default Card; diff --git a/src/components/sidebar/index.tsx b/src/components/sidebar/index.tsx deleted file mode 100644 index ad3252b..0000000 --- a/src/components/sidebar/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import type { FC } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ChatBubbleOvalLeftEllipsisIcon, PencilSquareIcon } from '@heroicons/react/24/outline'; -import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid'; -import Button from '@/components/base/button'; -// import Card from './card' -import type { ConversationItem } from '@/types/app'; - -function classNames(...classes: any[]) { - return classes.filter(Boolean).join(' '); -} - -const MAX_CONVERSATION_LENTH = 20; - -export type ISidebarProps = { - copyRight: string; - currentId: string; - onCurrentIdChange: (id: string) => void; - list: ConversationItem[]; -}; - -const Sidebar: FC = ({ copyRight, currentId, onCurrentIdChange, list }) => { - const { t } = useTranslation(); - return ( -
- {list.length < MAX_CONVERSATION_LENTH && ( -
- -
- )} - - - {/* -
LangGenius
-
*/} -
-
- © {copyRight} {new Date().getFullYear()} -
-
-
- ); -}; - -export default React.memo(Sidebar); diff --git a/src/components/value-panel/index.tsx b/src/components/value-panel/index.tsx deleted file mode 100644 index 2a4a7e3..0000000 --- a/src/components/value-panel/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; -import type { FC, ReactNode } from 'react'; -import React from 'react'; -import cn from 'classnames'; -import { useTranslation } from 'react-i18next'; -import s from './style.module.css'; -import { StarIcon } from '@/components//welcome/massive-component'; -import Button from '@/components/base/button'; - -export type ITemplateVarPanelProps = { - className?: string; - header: ReactNode; - children?: ReactNode | null; - isFold: boolean; -}; - -const TemplateVarPanel: FC = ({ className, header, children, isFold }) => { - return ( -
- {/* header */} -
{header}
- {/* body */} - {!isFold && children &&
{children}
} -
- ); -}; - -export const PanelTitle: FC<{ title: string; className?: string }> = ({ title, className }) => { - return ( -
- - {title} -
- ); -}; - -export const VarOpBtnGroup: FC<{ - className?: string; - onConfirm: () => void; - onCancel: () => void; -}> = ({ className, onConfirm, onCancel }) => { - const { t } = useTranslation(); - - return ( -
- - -
- ); -}; - -export default React.memo(TemplateVarPanel); diff --git a/src/components/value-panel/style.module.css b/src/components/value-panel/style.module.css deleted file mode 100644 index f0b0ffc..0000000 --- a/src/components/value-panel/style.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.boxShodow { - box-shadow: - 0px 12px 16px -4px rgba(16, 24, 40, 0.08), - 0px 4px 6px -2px rgba(16, 24, 40, 0.03); -} diff --git a/src/components/welcome/index.tsx b/src/components/welcome/index.tsx deleted file mode 100644 index 634f51e..0000000 --- a/src/components/welcome/index.tsx +++ /dev/null @@ -1,370 +0,0 @@ -'use client'; -import type { FC } from 'react'; -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import TemplateVarPanel, { PanelTitle, VarOpBtnGroup } from '../value-panel'; -import FileUploaderInAttachmentWrapper from '../base/file-uploader-in-attachment'; -import s from './style.module.css'; -import { AppInfoComp, ChatBtn, EditBtn, FootLogo, PromptTemplate } from './massive-component'; -import type { AppInfo, PromptConfig } from '@/types/app'; -import Toast from '@/components/base/toast'; -import Select from '@/components/base/select'; -import { DEFAULT_VALUE_MAX_LEN } from '@/config'; - -// regex to match the {{}} and replace it with a span -const regex = /\{\{([^}]+)\}\}/g; - -export type IWelcomeProps = { - conversationName: string; - hasSetInputs: boolean; - isPublicVersion: boolean; - siteInfo: AppInfo; - promptConfig: PromptConfig; - onStartChat: (inputs: Record) => void; - canEditInputs: boolean; - savedInputs: Record; - onInputsChange: (inputs: Record) => void; -}; - -const Welcome: FC = ({ - conversationName, - hasSetInputs, - isPublicVersion, - siteInfo, - promptConfig, - onStartChat, - canEditInputs, - savedInputs, - onInputsChange -}) => { - console.log(promptConfig); - const { t } = useTranslation(); - const hasVar = promptConfig.prompt_variables.length > 0; - const [isFold, setIsFold] = useState(true); - const [inputs, setInputs] = useState>( - (() => { - if (hasSetInputs) return savedInputs; - - const res: Record = {}; - if (promptConfig) { - promptConfig.prompt_variables.forEach((item) => { - res[item.key] = ''; - }); - } - return res; - })() - ); - useEffect(() => { - if (!savedInputs) { - const res: Record = {}; - if (promptConfig) { - promptConfig.prompt_variables.forEach((item) => { - res[item.key] = ''; - }); - } - setInputs(res); - } else { - setInputs(savedInputs); - } - }, [savedInputs]); - - const highLightPromoptTemplate = (() => { - if (!promptConfig) return ''; - const res = promptConfig.prompt_template.replace(regex, (match, p1) => { - return `${inputs?.[p1] ? inputs?.[p1] : match}`; - }); - return res; - })(); - - const { notify } = Toast; - const logError = (message: string) => { - notify({ type: 'error', message, duration: 3000 }); - }; - - const renderHeader = () => { - return ( -
-
{conversationName}
-
- ); - }; - - const renderInputs = () => { - return ( -
- {promptConfig.prompt_variables.map((item) => ( -
- - {item.type === 'select' && ( - { - setInputs({ ...inputs, [item.key]: e.target.value }); - }} - className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'} - maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN} - /> - )} - {item.type === 'paragraph' && ( -