Wangbo 2 månader sedan
incheckning
2adf523749
100 ändrade filer med 7669 tillägg och 0 borttagningar
  1. 24 0
      .dockerignore
  2. 22 0
      .editorconfig
  3. 27 0
      .env.example
  4. 7 0
      .eslintignore
  5. 31 0
      .eslintrc.json
  6. 53 0
      .gitignore
  7. 82 0
      .husky/pre-commit
  8. 19 0
      .storybook/main.ts
  9. 37 0
      .storybook/preview.tsx
  10. 6 0
      .storybook/storybook.css
  11. 6 0
      .vscode/extensions.json
  12. 25 0
      .vscode/settings.example.json
  13. 6 0
      .vscode/settings.json
  14. 72 0
      Dockerfile
  15. 118 0
      README.md
  16. 0 0
      __mocks__/mime.js
  17. 15 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx
  18. 10 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/configuration/page.tsx
  19. 15 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/develop/page.tsx
  20. 161 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx
  21. 11 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx
  22. 140 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx
  23. 101 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx
  24. 24 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx
  25. 87 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx
  26. 180 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx
  27. 6 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts
  28. 41 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx
  29. 179 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx
  30. 291 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx
  31. 97 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx
  32. 45 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx
  33. 28 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx
  34. 16 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts
  35. 6 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css
  36. 12 0
      app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx
  37. 27 0
      app/(commonLayout)/app/(appDetailLayout)/layout.tsx
  38. 422 0
      app/(commonLayout)/apps/AppCard.tsx
  39. 162 0
      app/(commonLayout)/apps/Apps.tsx
  40. 101 0
      app/(commonLayout)/apps/NewAppCard.tsx
  41. 3 0
      app/(commonLayout)/apps/assets/add.svg
  42. 4 0
      app/(commonLayout)/apps/assets/chat-solid.svg
  43. 3 0
      app/(commonLayout)/apps/assets/chat.svg
  44. 4 0
      app/(commonLayout)/apps/assets/completion-solid.svg
  45. 3 0
      app/(commonLayout)/apps/assets/completion.svg
  46. 3 0
      app/(commonLayout)/apps/assets/discord.svg
  47. 17 0
      app/(commonLayout)/apps/assets/github.svg
  48. 3 0
      app/(commonLayout)/apps/assets/link-gray.svg
  49. 3 0
      app/(commonLayout)/apps/assets/link.svg
  50. 3 0
      app/(commonLayout)/apps/assets/right-arrow.svg
  51. 53 0
      app/(commonLayout)/apps/hooks/useAppsQueryState.ts
  52. 25 0
      app/(commonLayout)/apps/page.tsx
  53. 29 0
      app/(commonLayout)/apps/style.module.css
  54. 11 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/api/page.tsx
  55. 16 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/[documentId]/page.tsx
  56. 16 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/[documentId]/settings/page.tsx
  57. 16 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/create/page.tsx
  58. 16 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/page.tsx
  59. 9 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css
  60. 16 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/hitTesting/page.tsx
  61. 262 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx
  62. 20 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx
  63. 18 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/style.module.css
  64. 16 0
      app/(commonLayout)/datasets/(datasetDetailLayout)/layout.tsx
  65. 41 0
      app/(commonLayout)/datasets/ApiServer.tsx
  66. 116 0
      app/(commonLayout)/datasets/Container.tsx
  67. 240 0
      app/(commonLayout)/datasets/DatasetCard.tsx
  68. 19 0
      app/(commonLayout)/datasets/DatasetFooter.tsx
  69. 87 0
      app/(commonLayout)/datasets/Datasets.tsx
  70. 28 0
      app/(commonLayout)/datasets/Doc.tsx
  71. 38 0
      app/(commonLayout)/datasets/NewDatasetCard.tsx
  72. 8 0
      app/(commonLayout)/datasets/connect/page.tsx
  73. 12 0
      app/(commonLayout)/datasets/create/page.tsx
  74. 14 0
      app/(commonLayout)/datasets/layout.tsx
  75. 11 0
      app/(commonLayout)/datasets/page.tsx
  76. 11 0
      app/(commonLayout)/datasets/store.ts
  77. 1319 0
      app/(commonLayout)/datasets/template/template.en.mdx
  78. 1321 0
      app/(commonLayout)/datasets/template/template.zh.mdx
  79. 8 0
      app/(commonLayout)/explore/apps/page.tsx
  80. 16 0
      app/(commonLayout)/explore/installed/[appId]/page.tsx
  81. 16 0
      app/(commonLayout)/explore/layout.tsx
  82. 38 0
      app/(commonLayout)/layout.tsx
  83. 225 0
      app/(commonLayout)/list.module.css
  84. 28 0
      app/(commonLayout)/tools/page.tsx
  85. 11 0
      app/(shareLayout)/chat/[token]/page.tsx
  86. 11 0
      app/(shareLayout)/chatbot/[token]/page.tsx
  87. 10 0
      app/(shareLayout)/completion/[token]/page.tsx
  88. 21 0
      app/(shareLayout)/layout.tsx
  89. 103 0
      app/(shareLayout)/webapp-signin/page.tsx
  90. 11 0
      app/(shareLayout)/workflow/[token]/page.tsx
  91. 9 0
      app/account/account-page/index.module.css
  92. 335 0
      app/account/account-page/index.tsx
  93. 94 0
      app/account/avatar.tsx
  94. 37 0
      app/account/header.tsx
  95. 40 0
      app/account/layout.tsx
  96. 7 0
      app/account/page.tsx
  97. 67 0
      app/activate/activateForm.tsx
  98. 32 0
      app/activate/page.tsx
  99. 4 0
      app/activate/style.module.css
  100. BIN
      app/activate/team-28x28.png

+ 24 - 0
.dockerignore

@@ -0,0 +1,24 @@
+.env
+.env.*
+
+# Logs
+logs
+*.log*
+
+# node
+node_modules
+.husky
+.next
+
+# vscode
+.vscode
+
+# webstorm
+.idea
+*.iml
+*.iws
+*.ipr
+
+
+# Jetbrains
+.idea

+ 22 - 0
.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

+ 27 - 0
.env.example

@@ -0,0 +1,27 @@
+# For production release, change this to PRODUCTION
+NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT
+# The deployment edition, SELF_HOSTED
+NEXT_PUBLIC_EDITION=SELF_HOSTED
+# The base URL of console application, refers to the Console base URL of WEB service if console domain is
+# different from api or web app domain.
+# example: http://cloud.dify.ai/console/api
+NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
+# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
+# console or api domain.
+# example: http://udify.app/api
+NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
+
+# SENTRY
+NEXT_PUBLIC_SENTRY_DSN=
+
+# Disable Next.js Telemetry (https://nextjs.org/telemetry)
+NEXT_TELEMETRY_DISABLED=1
+
+# Disable Upload Image as WebApp icon default is false
+NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON=false
+
+# The timeout for the text generation in millisecond
+NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
+
+# CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
+NEXT_PUBLIC_CSP_WHITELIST=

+ 7 - 0
.eslintignore

@@ -0,0 +1,7 @@
+/**/node_modules/*
+node_modules/
+
+dist/
+build/
+out/
+.next/

+ 31 - 0
.eslintrc.json

@@ -0,0 +1,31 @@
+{
+  "extends": [
+    "next",
+    "@antfu",
+    "plugin:storybook/recommended"
+  ],
+  "rules": {
+    "@typescript-eslint/consistent-type-definitions": [
+      "error",
+      "type"
+    ],
+    "@typescript-eslint/no-var-requires": "off",
+    "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",
+    "react/display-name": "warn"
+  }
+}

+ 53 - 0
.gitignore

@@ -0,0 +1,53 @@
+# 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
+
+/.history
+
+# 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/
+.yarnrc.yml
+
+# pmpm
+pnpm-lock.yaml
+
+.favorites.json
+*storybook.log

+ 82 - 0
.husky/pre-commit

@@ -0,0 +1,82 @@
+#!/usr/bin/env bash
+. "$(dirname -- "$0")/_/husky.sh"
+
+# get the list of modified files
+files=$(git diff --cached --name-only)
+
+# check if api or web directory is modified
+
+api_modified=false
+web_modified=false
+
+for file in $files
+do
+    if [[ $file == "api/"* && $file == *.py ]]; then
+        # set api_modified flag to true
+        api_modified=true
+    elif [[ $file == "web/"* ]]; then
+        # set web_modified flag to true
+        web_modified=true
+    fi
+done
+
+# run linters based on the modified modules
+
+if $api_modified; then
+    echo "Running Ruff linter on api module"
+
+    # python style checks rely on `ruff` in path
+    if ! command -v ruff &> /dev/null; then
+        echo "Installing linting tools (Ruff, dotenv-linter ...) ..."
+        poetry install -C api --only lint
+    fi
+
+    # run Ruff linter auto-fixing
+    ruff check --fix ./api
+
+    # run Ruff linter checks
+    ruff check --preview ./api || status=$?
+
+    status=${status:-0}
+
+
+    if [ $status -ne 0 ]; then
+      echo "Ruff linter on api module error, exit code: $status"
+      echo "Please run 'dev/reformat' to fix the fixable linting errors."
+      exit 1
+    fi
+fi
+
+if $web_modified; then
+    echo "Running ESLint on web module"
+    cd ./web || exit 1
+    npx lint-staged
+
+    echo "Running unit tests check"
+    modified_files=$(git diff --cached --name-only -- utils | grep -v '\.spec\.ts$' || true)
+
+    if [ -n "$modified_files" ]; then
+        for file in $modified_files; do
+            test_file="${file%.*}.spec.ts"
+            echo "Checking for test file: $test_file"
+
+            # check if the test file exists
+            if [ -f "../$test_file" ]; then
+                echo "Detected changes in $file, running corresponding unit tests..."
+                npm run test "../$test_file"
+
+                if [ $? -ne 0 ]; then
+                    echo "Unit tests failed. Please fix the errors before committing."
+                    exit 1
+                fi
+                echo "Unit tests for $file passed."
+            else
+                echo "Warning: $file does not have a corresponding test file."
+            fi
+
+        done
+        echo "All unit tests for modified web/utils files have passed."
+    fi
+
+    cd ../
+fi

+ 19 - 0
.storybook/main.ts

@@ -0,0 +1,19 @@
+import type { StorybookConfig } from '@storybook/nextjs'
+
+const config: StorybookConfig = {
+    // stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+    stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+    addons: [
+        '@storybook/addon-onboarding',
+        '@storybook/addon-links',
+        '@storybook/addon-essentials',
+        '@chromatic-com/storybook',
+        '@storybook/addon-interactions',
+    ],
+    framework: {
+        name: '@storybook/nextjs',
+        options: {},
+    },
+    staticDirs: ['../public'],
+}
+export default config

+ 37 - 0
.storybook/preview.tsx

@@ -0,0 +1,37 @@
+import React from 'react'
+import type { Preview } from '@storybook/react'
+import { withThemeByDataAttribute } from '@storybook/addon-themes';
+import I18nServer from '../app/components/i18n-server'
+
+import '../app/styles/globals.css'
+import '../app/styles/markdown.scss'
+import './storybook.css'
+
+export const decorators = [
+    withThemeByDataAttribute({
+      themes: {
+        light: 'light',
+        dark: 'dark',
+      },
+      defaultTheme: 'light',
+      attributeName: 'data-theme',
+    }),
+    Story => {
+      return <I18nServer>
+        <Story />
+      </I18nServer>
+    }
+  ];
+
+const preview: Preview = {
+  parameters: {
+        controls: {
+            matchers: {
+                color: /(background|color)$/i,
+                date: /Date$/i,
+            },
+        },
+    },
+}
+
+export default preview

+ 6 - 0
.storybook/storybook.css

@@ -0,0 +1,6 @@
+html,
+body {
+  max-width: unset;
+  overflow: auto;
+  user-select: text;
+}

+ 6 - 0
.vscode/extensions.json

@@ -0,0 +1,6 @@
+{
+  "recommendations": [
+    "bradlc.vscode-tailwindcss",
+    "firsttris.vscode-jest-runner"
+  ]
+}

+ 25 - 0
.vscode/settings.example.json

@@ -0,0 +1,25 @@
+{
+  "prettier.enable": false,
+  "editor.formatOnSave": true,
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": "explicit"
+  },
+  "eslint.format.enable": true,
+  "[python]": {
+    "editor.formatOnType": true
+  },
+  "[html]": {
+    "editor.defaultFormatter": "vscode.html-language-features"
+  },
+  "[typescriptreact]": {
+    "editor.defaultFormatter": "vscode.typescript-language-features"
+  },
+  "[javascriptreact]": {
+    "editor.defaultFormatter": "vscode.typescript-language-features"
+  },
+  "[jsonc]": {
+    "editor.defaultFormatter": "vscode.json-language-features"
+  },
+  "typescript.tsdk": "node_modules/typescript/lib",
+  "typescript.enablePromptUseWorkspaceTsdk": true
+}

+ 6 - 0
.vscode/settings.json

@@ -0,0 +1,6 @@
+{
+    "i18n-ally.localesPaths": [
+        "i18n",
+        "public/vs/language"
+    ]
+}

+ 72 - 0
Dockerfile

@@ -0,0 +1,72 @@
+# base image
+FROM node:20.11-alpine3.19 AS base
+LABEL maintainer="takatost@gmail.com"
+
+# if you located in China, you can use aliyun mirror to speed up
+# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
+
+RUN apk add --no-cache tzdata
+
+
+# install packages
+FROM base AS packages
+
+WORKDIR /app/web
+
+COPY package.json .
+COPY yarn.lock .
+
+# if you located in China, you can use taobao registry to speed up
+# RUN yarn install --frozen-lockfile --registry https://registry.npmmirror.com/
+
+RUN yarn install --frozen-lockfile
+
+# build resources
+FROM base AS builder
+WORKDIR /app/web
+COPY --from=packages /app/web/ .
+COPY . .
+
+RUN yarn build
+
+
+# production stage
+FROM base AS production
+
+ENV NODE_ENV=production
+ENV EDITION=SELF_HOSTED
+ENV DEPLOY_ENV=PRODUCTION
+ENV CONSOLE_API_URL=http://127.0.0.1:5001
+ENV APP_API_URL=http://127.0.0.1:5001
+ENV PORT=3000
+ENV NEXT_TELEMETRY_DISABLED=1
+
+# set timezone
+ENV TZ=UTC
+RUN ln -s /usr/share/zoneinfo/${TZ} /etc/localtime \
+    && echo ${TZ} > /etc/timezone
+
+
+WORKDIR /app/web
+COPY --from=builder /app/web/public ./public
+COPY --from=builder /app/web/.next/standalone ./
+COPY --from=builder /app/web/.next/static ./.next/static
+
+COPY docker/pm2.json ./pm2.json
+COPY docker/entrypoint.sh ./entrypoint.sh
+
+
+# global runtime packages
+RUN yarn global add pm2 \
+    && yarn cache clean \
+    && mkdir /.pm2 \
+    && chown -R 1001:0 /.pm2 /app/web \
+    && chmod -R g=u /.pm2 /app/web
+
+
+ARG COMMIT_SHA
+ENV COMMIT_SHA=${COMMIT_SHA}
+
+USER 1001
+EXPOSE 3000
+ENTRYPOINT ["/bin/sh", "./entrypoint.sh"]

+ 118 - 0
README.md

@@ -0,0 +1,118 @@
+# Dify Frontend
+
+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).
+
+## Getting Started
+
+### Run by source code
+
+To start the web frontend service, you will need [Node.js v18.x (LTS)](https://nodejs.org/en) and [NPM version 8.x.x](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/).
+
+First, install the dependencies:
+
+```bash
+npm install
+# or
+yarn install --frozen-lockfile
+```
+
+Then, configure the environment variables. Create a file named `.env.local` in the current directory and copy the contents from `.env.example`. Modify the values of these environment variables according to your requirements:
+
+```bash
+cp .env.example .env.local
+```
+
+```
+# For production release, change this to PRODUCTION
+NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT
+# The deployment edition, SELF_HOSTED
+NEXT_PUBLIC_EDITION=SELF_HOSTED
+# The base URL of console application, refers to the Console base URL of WEB service if console domain is
+# different from api or web app domain.
+# example: http://cloud.dify.ai/console/api
+NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
+# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
+# console or api domain.
+# example: http://udify.app/api
+NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
+
+# SENTRY
+NEXT_PUBLIC_SENTRY_DSN=
+```
+
+Finally, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the file under folder `app`. The page auto-updates as you edit the file.
+
+## Deploy
+
+### Deploy on server
+
+First, build the app for production:
+
+```bash
+npm run build
+```
+
+Then, start the server:
+
+```bash
+npm run start
+```
+
+If you want to customize the host and port:
+
+```bash
+npm run start --port=3001 --host=0.0.0.0
+```
+
+## Storybook
+
+This project uses [Storybook](https://storybook.js.org/) for UI component development.
+
+To start the storybook server, run:
+
+```bash
+yarn storybook
+```
+
+Open [http://localhost:6006](http://localhost:6006) with your browser to see the result.
+
+## Lint Code
+
+If your IDE is VSCode, rename `web/.vscode/settings.example.json` to `web/.vscode/settings.json` for lint code setting.
+
+## Test
+
+We start to use [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for Unit Testing.
+
+You can create a test file with a suffix of `.spec` beside the file that to be tested. For example, if you want to test a file named `util.ts`. The test file name should be `util.spec.ts`.
+
+Run test:
+
+```bash
+npm run test
+```
+
+If you are not familiar with writing tests, here is some code to refer to:
+* [classnames.spec.ts](./utils/classnames.spec.ts)
+* [index.spec.tsx](./app/components/base/button/index.spec.tsx)
+
+
+
+
+## Documentation
+
+Visit <https://docs.dify.ai/getting-started/readme> to view the full documentation.
+
+## Community
+
+The Dify community can be found on [Discord community](https://discord.gg/5AEfbxcd9k), where you can ask questions, voice ideas, and share your projects.

+ 0 - 0
__mocks__/mime.js


+ 15 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx

@@ -0,0 +1,15 @@
+import React from 'react'
+import Main from '@/app/components/app/log-annotation'
+import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
+
+export type IProps = {
+  params: { appId: string }
+}
+
+const Logs = async () => {
+  return (
+    <Main pageType={PageType.annotation} />
+  )
+}
+
+export default Logs

+ 10 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/configuration/page.tsx

@@ -0,0 +1,10 @@
+import React from 'react'
+import Configuration from '@/app/components/app/configuration'
+
+const IConfiguration = async () => {
+  return (
+    <Configuration />
+  )
+}
+
+export default IConfiguration

+ 15 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/develop/page.tsx

@@ -0,0 +1,15 @@
+import React from 'react'
+import { type Locale } from '@/i18n'
+import DevelopMain from '@/app/components/develop'
+
+export type IDevelopProps = {
+  params: { locale: Locale; appId: string }
+}
+
+const Develop = async ({
+  params: { appId },
+}: IDevelopProps) => {
+  return <DevelopMain appId={appId} />
+}
+
+export default Develop

+ 161 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx

@@ -0,0 +1,161 @@
+'use client'
+import type { FC } from 'react'
+import { useUnmount } from 'ahooks'
+import React, { useCallback, useEffect, useState } from 'react'
+import { usePathname, useRouter } from 'next/navigation'
+import {
+  RiDashboard2Fill,
+  RiDashboard2Line,
+  RiFileList3Fill,
+  RiFileList3Line,
+  RiTerminalBoxFill,
+  RiTerminalBoxLine,
+  RiTerminalWindowFill,
+  RiTerminalWindowLine,
+} from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { useShallow } from 'zustand/react/shallow'
+import { useContextSelector } from 'use-context-selector'
+import s from './style.module.css'
+import cn from '@/utils/classnames'
+import { useStore } from '@/app/components/app/store'
+import AppSideBar from '@/app/components/app-sidebar'
+import type { NavIcon } from '@/app/components/app-sidebar/navLink'
+import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
+import AppContext, { useAppContext } from '@/context/app-context'
+import Loading from '@/app/components/base/loading'
+import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+
+export type IAppDetailLayoutProps = {
+  children: React.ReactNode
+  params: { appId: string }
+}
+
+const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
+  const {
+    children,
+    params: { appId }, // get appId in path
+  } = props
+  const { t } = useTranslation()
+  const router = useRouter()
+  const pathname = usePathname()
+  const media = useBreakpoints()
+  const isMobile = media === MediaType.mobile
+  const { isCurrentWorkspaceEditor } = useAppContext()
+  const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({
+    appDetail: state.appDetail,
+    setAppDetail: state.setAppDetail,
+    setAppSiderbarExpand: state.setAppSiderbarExpand,
+  })))
+  const [navigation, setNavigation] = useState<Array<{
+    name: string
+    href: string
+    icon: NavIcon
+    selectedIcon: NavIcon
+  }>>([])
+  const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
+
+  const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
+    const navs = [
+      ...(isCurrentWorkspaceEditor
+        ? [{
+          name: t('common.appMenus.promptEng'),
+          href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
+          icon: RiTerminalWindowLine,
+          selectedIcon: RiTerminalWindowFill,
+        }]
+        : []
+      ),
+      {
+        name: t('common.appMenus.apiAccess'),
+        href: `/app/${appId}/develop`,
+        icon: RiTerminalBoxLine,
+        selectedIcon: RiTerminalBoxFill,
+      },
+      ...(isCurrentWorkspaceEditor
+        ? [{
+          name: mode !== 'workflow'
+            ? t('common.appMenus.logAndAnn')
+            : t('common.appMenus.logs'),
+          href: `/app/${appId}/logs`,
+          icon: RiFileList3Line,
+          selectedIcon: RiFileList3Fill,
+        }]
+        : []
+      ),
+      {
+        name: t('common.appMenus.overview'),
+        href: `/app/${appId}/overview`,
+        icon: RiDashboard2Line,
+        selectedIcon: RiDashboard2Fill,
+      },
+    ]
+    return navs
+  }, [t])
+
+  useEffect(() => {
+    if (appDetail) {
+      document.title = `${(appDetail.name || 'App')} - Dify`
+      const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
+      const mode = isMobile ? 'collapse' : 'expand'
+      setAppSiderbarExpand(isMobile ? mode : localeMode)
+      // TODO: consider screen size and mode
+      // if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
+      //   setAppSiderbarExpand('collapse')
+    }
+  }, [appDetail, isMobile])
+
+  useEffect(() => {
+    setAppDetail()
+    fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
+      // redirection
+      const canIEditApp = isCurrentWorkspaceEditor
+      if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
+        router.replace(`/app/${appId}/overview`)
+        return
+      }
+      if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
+        router.replace(`/app/${appId}/workflow`)
+      }
+      else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
+        router.replace(`/app/${appId}/configuration`)
+      }
+      else {
+        setAppDetail({ ...res, enable_sso: false })
+        setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
+        if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
+          fetchAppSSO({ appId }).then((ssoRes) => {
+            setAppDetail({ ...res, enable_sso: ssoRes.enabled })
+          })
+        }
+      }
+    }).catch((e: any) => {
+      if (e.status === 404)
+        router.replace('/apps')
+    })
+  }, [appId, isCurrentWorkspaceEditor, systemFeatures, getNavigations, pathname, router, setAppDetail])
+
+  useUnmount(() => {
+    setAppDetail()
+  })
+
+  if (!appDetail) {
+    return (
+      <div className='flex h-full items-center justify-center bg-white'>
+        <Loading />
+      </div>
+    )
+  }
+
+  return (
+    <div className={cn(s.app, 'flex', 'overflow-hidden')}>
+      {appDetail && (
+        <AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background} desc={appDetail.mode} navigation={navigation} />
+      )}
+      <div className="bg-white grow overflow-hidden">
+        {children}
+      </div>
+    </div>
+  )
+}
+export default React.memo(AppDetailLayout)

+ 11 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx

@@ -0,0 +1,11 @@
+import React from 'react'
+import Main from '@/app/components/app/log-annotation'
+import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
+
+const Logs = async () => {
+  return (
+    <Main pageType={PageType.log} />
+  )
+}
+
+export default Logs

+ 140 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx

@@ -0,0 +1,140 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { useContext, useContextSelector } from 'use-context-selector'
+import AppCard from '@/app/components/app/overview/appCard'
+import Loading from '@/app/components/base/loading'
+import { ToastContext } from '@/app/components/base/toast'
+import {
+  fetchAppDetail,
+  fetchAppSSO,
+  updateAppSSO,
+  updateAppSiteAccessToken,
+  updateAppSiteConfig,
+  updateAppSiteStatus,
+} from '@/service/apps'
+import type { App, AppSSO } from '@/types/app'
+import type { UpdateAppSiteCodeResponse } from '@/models/app'
+import { asyncRunSafe } from '@/utils'
+import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
+import type { IAppCardProps } from '@/app/components/app/overview/appCard'
+import { useStore as useAppStore } from '@/app/components/app/store'
+import AppContext from '@/context/app-context'
+
+export type ICardViewProps = {
+  appId: string
+}
+
+const CardView: FC<ICardViewProps> = ({ appId }) => {
+  const { t } = useTranslation()
+  const { notify } = useContext(ToastContext)
+  const appDetail = useAppStore(state => state.appDetail)
+  const setAppDetail = useAppStore(state => state.setAppDetail)
+  const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
+
+  const updateAppDetail = async () => {
+    try {
+      const res = await fetchAppDetail({ url: '/apps', id: appId })
+      if (systemFeatures.enable_web_sso_switch_component) {
+        const ssoRes = await fetchAppSSO({ appId })
+        setAppDetail({ ...res, enable_sso: ssoRes.enabled })
+      }
+      else {
+        setAppDetail({ ...res })
+      }
+    }
+    catch (error) { console.error(error) }
+  }
+
+  const handleCallbackResult = (err: Error | null, message?: string) => {
+    const type = err ? 'error' : 'success'
+
+    message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
+
+    if (type === 'success')
+      updateAppDetail()
+
+    notify({
+      type,
+      message: t(`common.actionMsg.${message}`),
+    })
+  }
+
+  const onChangeSiteStatus = async (value: boolean) => {
+    const [err] = await asyncRunSafe<App>(
+      updateAppSiteStatus({
+        url: `/apps/${appId}/site-enable`,
+        body: { enable_site: value },
+      }) as Promise<App>,
+    )
+
+    handleCallbackResult(err)
+  }
+
+  const onChangeApiStatus = async (value: boolean) => {
+    const [err] = await asyncRunSafe<App>(
+      updateAppSiteStatus({
+        url: `/apps/${appId}/api-enable`,
+        body: { enable_api: value },
+      }) as Promise<App>,
+    )
+
+    handleCallbackResult(err)
+  }
+
+  const onSaveSiteConfig: IAppCardProps['onSaveSiteConfig'] = async (params) => {
+    const [err] = await asyncRunSafe<App>(
+      updateAppSiteConfig({
+        url: `/apps/${appId}/site`,
+        body: params,
+      }) as Promise<App>,
+    )
+    if (!err)
+      localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
+
+    if (systemFeatures.enable_web_sso_switch_component) {
+      const [sso_err] = await asyncRunSafe<AppSSO>(
+        updateAppSSO({ id: appId, enabled: Boolean(params.enable_sso) }) as Promise<AppSSO>,
+      )
+      if (sso_err) {
+        handleCallbackResult(sso_err)
+        return
+      }
+    }
+
+    handleCallbackResult(err)
+  }
+
+  const onGenerateCode = async () => {
+    const [err] = await asyncRunSafe<UpdateAppSiteCodeResponse>(
+      updateAppSiteAccessToken({
+        url: `/apps/${appId}/site/access-token-reset`,
+      }) as Promise<UpdateAppSiteCodeResponse>,
+    )
+
+    handleCallbackResult(err, err ? 'generatedUnsuccessfully' : 'generatedSuccessfully')
+  }
+
+  if (!appDetail)
+    return <Loading />
+
+  return (
+    <div className="grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6">
+      <AppCard
+        appInfo={appDetail}
+        cardType="webapp"
+        onChangeStatus={onChangeSiteStatus}
+        onGenerateCode={onGenerateCode}
+        onSaveSiteConfig={onSaveSiteConfig}
+      />
+      <AppCard
+        cardType="api"
+        appInfo={appDetail}
+        onChangeStatus={onChangeApiStatus}
+      />
+    </div>
+  )
+}
+
+export default CardView

+ 101 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx

@@ -0,0 +1,101 @@
+'use client'
+import React, { useState } from 'react'
+import dayjs from 'dayjs'
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import { useTranslation } from 'react-i18next'
+import type { PeriodParams } from '@/app/components/app/overview/appChart'
+import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/appChart'
+import type { Item } from '@/app/components/base/select'
+import { SimpleSelect } from '@/app/components/base/select'
+import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter'
+import { useStore as useAppStore } from '@/app/components/app/store'
+
+dayjs.extend(quarterOfYear)
+
+const today = dayjs()
+
+const queryDateFormat = 'YYYY-MM-DD HH:mm'
+
+export type IChartViewProps = {
+  appId: string
+}
+
+export default function ChartView({ appId }: IChartViewProps) {
+  const { t } = useTranslation()
+  const appDetail = useAppStore(state => state.appDetail)
+  const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
+  const isWorkflow = appDetail?.mode === 'workflow'
+  const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } })
+
+  const onSelect = (item: Item) => {
+    if (item.value === 'all') {
+      setPeriod({ name: item.name, query: undefined })
+    }
+    else if (item.value === 0) {
+      const startOfToday = today.startOf('day').format(queryDateFormat)
+      const endOfToday = today.endOf('day').format(queryDateFormat)
+      setPeriod({ name: item.name, query: { start: startOfToday, end: endOfToday } })
+    }
+    else {
+      setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } })
+    }
+  }
+
+  if (!appDetail)
+    return null
+
+  return (
+    <div>
+      <div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
+        <span className='mr-3'>{t('appOverview.analysis.title')}</span>
+        <SimpleSelect
+          items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
+          className='mt-0 !w-40'
+          onSelect={onSelect}
+          defaultValue={7}
+        />
+      </div>
+      {!isWorkflow && (
+        <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
+          <ConversationsChart period={period} id={appId} />
+          <EndUsersChart period={period} id={appId} />
+        </div>
+      )}
+      {!isWorkflow && (
+        <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
+          {isChatApp
+            ? (
+              <AvgSessionInteractions period={period} id={appId} />
+            )
+            : (
+              <AvgResponseTime period={period} id={appId} />
+            )}
+          <TokenPerSecond period={period} id={appId} />
+        </div>
+      )}
+      {!isWorkflow && (
+        <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
+          <UserSatisfactionRate period={period} id={appId} />
+          <CostChart period={period} id={appId} />
+        </div>
+      )}
+      {!isWorkflow && isChatApp && (
+        <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
+          <MessagesChart period={period} id={appId} />
+        </div>
+      )}
+      {isWorkflow && (
+        <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
+          <WorkflowMessagesChart period={period} id={appId} />
+          <WorkflowDailyTerminalsChart period={period} id={appId} />
+        </div>
+      )}
+      {isWorkflow && (
+        <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
+          <WorkflowCostChart period={period} id={appId} />
+          <AvgUserInteractions period={period} id={appId} />
+        </div>
+      )}
+    </div>
+  )
+}

+ 24 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx

@@ -0,0 +1,24 @@
+import React from 'react'
+import ChartView from './chartView'
+import CardView from './cardView'
+import TracingPanel from './tracing/panel'
+import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
+
+export type IDevelopProps = {
+  params: { appId: string }
+}
+
+const Overview = async ({
+  params: { appId },
+}: IDevelopProps) => {
+  return (
+    <div className="h-full px-4 sm:px-16 py-6 overflow-scroll">
+      <ApikeyInfoPanel />
+      <TracingPanel />
+      <CardView appId={appId} />
+      <ChartView appId={appId} />
+    </div>
+  )
+}
+
+export default Overview

+ 87 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx

@@ -0,0 +1,87 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import type { PopupProps } from './config-popup'
+import ConfigPopup from './config-popup'
+import cn from '@/utils/classnames'
+import Button from '@/app/components/base/button'
+import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+
+const I18N_PREFIX = 'app.tracing'
+
+type Props = {
+  readOnly: boolean
+  className?: string
+  hasConfigured: boolean
+  controlShowPopup?: number
+} & PopupProps
+
+const ConfigBtn: FC<Props> = ({
+  className,
+  hasConfigured,
+  controlShowPopup,
+  ...popupProps
+}) => {
+  const { t } = useTranslation()
+  const [open, doSetOpen] = useState(false)
+  const openRef = useRef(open)
+  const setOpen = useCallback((v: boolean) => {
+    doSetOpen(v)
+    openRef.current = v
+  }, [doSetOpen])
+
+  const handleTrigger = useCallback(() => {
+    setOpen(!openRef.current)
+  }, [setOpen])
+
+  useEffect(() => {
+    if (controlShowPopup)
+      // setOpen(!openRef.current)
+      setOpen(true)
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [controlShowPopup])
+
+  if (popupProps.readOnly && !hasConfigured)
+    return null
+
+  const triggerContent = hasConfigured
+    ? (
+      <div className={cn(className, 'p-1 rounded-md hover:bg-black/5 cursor-pointer')}>
+        <Settings04 className='w-4 h-4 text-gray-500' />
+      </div>
+    )
+    : (
+      <Button variant='primary'
+        className={cn(className, '!h-8 !px-3 select-none')}
+      >
+        <Settings04 className='mr-1 w-4 h-4' />
+        <span className='text-[13px]'>{t(`${I18N_PREFIX}.config`)}</span>
+      </Button>
+    )
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-end'
+      offset={{
+        mainAxis: 12,
+        crossAxis: hasConfigured ? 8 : 0,
+      }}
+    >
+      <PortalToFollowElemTrigger onClick={handleTrigger}>
+        {triggerContent}
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent className='z-[11]'>
+        <ConfigPopup {...popupProps} />
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+export default React.memo(ConfigBtn)

+ 180 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx

@@ -0,0 +1,180 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useBoolean } from 'ahooks'
+import TracingIcon from './tracing-icon'
+import ProviderPanel from './provider-panel'
+import type { LangFuseConfig, LangSmithConfig } from './type'
+import { TracingProvider } from './type'
+import ProviderConfigModal from './provider-config-modal'
+import Indicator from '@/app/components/header/indicator'
+import Switch from '@/app/components/base/switch'
+import Tooltip from '@/app/components/base/tooltip'
+
+const I18N_PREFIX = 'app.tracing'
+
+export type PopupProps = {
+  appId: string
+  readOnly: boolean
+  enabled: boolean
+  onStatusChange: (enabled: boolean) => void
+  chosenProvider: TracingProvider | null
+  onChooseProvider: (provider: TracingProvider) => void
+  langSmithConfig: LangSmithConfig | null
+  langFuseConfig: LangFuseConfig | null
+  onConfigUpdated: (provider: TracingProvider, payload: LangSmithConfig | LangFuseConfig) => void
+  onConfigRemoved: (provider: TracingProvider) => void
+}
+
+const ConfigPopup: FC<PopupProps> = ({
+  appId,
+  readOnly,
+  enabled,
+  onStatusChange,
+  chosenProvider,
+  onChooseProvider,
+  langSmithConfig,
+  langFuseConfig,
+  onConfigUpdated,
+  onConfigRemoved,
+}) => {
+  const { t } = useTranslation()
+
+  const [currentProvider, setCurrentProvider] = useState<TracingProvider | null>(TracingProvider.langfuse)
+  const [isShowConfigModal, {
+    setTrue: showConfigModal,
+    setFalse: hideConfigModal,
+  }] = useBoolean(false)
+  const handleOnConfig = useCallback((provider: TracingProvider) => {
+    return () => {
+      setCurrentProvider(provider)
+      showConfigModal()
+    }
+  }, [showConfigModal])
+
+  const handleOnChoose = useCallback((provider: TracingProvider) => {
+    return () => {
+      onChooseProvider(provider)
+    }
+  }, [onChooseProvider])
+
+  const handleConfigUpdated = useCallback((payload: LangSmithConfig | LangFuseConfig) => {
+    onConfigUpdated(currentProvider!, payload)
+    hideConfigModal()
+  }, [currentProvider, hideConfigModal, onConfigUpdated])
+
+  const handleConfigRemoved = useCallback(() => {
+    onConfigRemoved(currentProvider!)
+    hideConfigModal()
+  }, [currentProvider, hideConfigModal, onConfigRemoved])
+
+  const providerAllConfigured = langSmithConfig && langFuseConfig
+  const providerAllNotConfigured = !langSmithConfig && !langFuseConfig
+
+  const switchContent = (
+    <Switch
+      className='ml-3'
+      defaultValue={enabled}
+      onChange={onStatusChange}
+      size='l'
+      disabled={providerAllNotConfigured}
+    />
+  )
+  const langSmithPanel = (
+    <ProviderPanel
+      type={TracingProvider.langSmith}
+      readOnly={readOnly}
+      config={langSmithConfig}
+      hasConfigured={!!langSmithConfig}
+      onConfig={handleOnConfig(TracingProvider.langSmith)}
+      isChosen={chosenProvider === TracingProvider.langSmith}
+      onChoose={handleOnChoose(TracingProvider.langSmith)}
+    />
+  )
+
+  const langfusePanel = (
+    <ProviderPanel
+      type={TracingProvider.langfuse}
+      readOnly={readOnly}
+      config={langFuseConfig}
+      hasConfigured={!!langFuseConfig}
+      onConfig={handleOnConfig(TracingProvider.langfuse)}
+      isChosen={chosenProvider === TracingProvider.langfuse}
+      onChoose={handleOnChoose(TracingProvider.langfuse)}
+    />
+  )
+
+  return (
+    <div className='w-[420px] p-4 rounded-2xl bg-white border-[0.5px] border-black/5 shadow-lg'>
+      <div className='flex justify-between items-center'>
+        <div className='flex items-center'>
+          <TracingIcon size='md' className='mr-2' />
+          <div className='leading-[120%] text-[18px] font-semibold text-gray-900'>{t(`${I18N_PREFIX}.tracing`)}</div>
+        </div>
+        <div className='flex items-center'>
+          <Indicator color={enabled ? 'green' : 'gray'} />
+          <div className='ml-1.5 text-xs font-semibold text-gray-500 uppercase'>
+            {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
+          </div>
+          {!readOnly && (
+            <>
+              {providerAllNotConfigured
+                ? (
+                  <Tooltip
+                    popupContent={t(`${I18N_PREFIX}.disabledTip`)}
+                  >
+                    {switchContent}
+                  </Tooltip>
+                )
+                : switchContent}
+            </>
+          )}
+
+        </div>
+      </div>
+
+      <div className='mt-2 leading-4 text-xs font-normal text-gray-500'>
+        {t(`${I18N_PREFIX}.tracingDescription`)}
+      </div>
+      <div className='mt-3 h-px bg-gray-100'></div>
+      <div className='mt-3'>
+        {(providerAllConfigured || providerAllNotConfigured)
+          ? (
+            <>
+              <div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
+              <div className='mt-2 space-y-2'>
+                {langSmithPanel}
+                {langfusePanel}
+              </div>
+            </>
+          )
+          : (
+            <>
+              <div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
+              <div className='mt-2'>
+                {langSmithConfig ? langSmithPanel : langfusePanel}
+              </div>
+              <div className='mt-3 leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
+              <div className='mt-2'>
+                {!langSmithConfig ? langSmithPanel : langfusePanel}
+              </div>
+            </>
+          )}
+
+      </div>
+      {isShowConfigModal && (
+        <ProviderConfigModal
+          appId={appId}
+          type={currentProvider!}
+          payload={currentProvider === TracingProvider.langSmith ? langSmithConfig : langFuseConfig}
+          onCancel={hideConfigModal}
+          onSaved={handleConfigUpdated}
+          onChosen={onChooseProvider}
+          onRemoved={handleConfigRemoved}
+        />
+      )}
+    </div>
+  )
+}
+export default React.memo(ConfigPopup)

+ 6 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts

@@ -0,0 +1,6 @@
+import { TracingProvider } from './type'
+
+export const docURL = {
+  [TracingProvider.langSmith]: 'https://docs.smith.langchain.com/',
+  [TracingProvider.langfuse]: 'https://docs.langfuse.com',
+}

+ 41 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx

@@ -0,0 +1,41 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import cn from '@/utils/classnames'
+import Input from '@/app/components/base/input'
+
+type Props = {
+  className?: string
+  label: string
+  labelClassName?: string
+  value: string | number
+  onChange: (value: string) => void
+  isRequired?: boolean
+  placeholder?: string
+}
+
+const Field: FC<Props> = ({
+  className,
+  label,
+  labelClassName,
+  value,
+  onChange,
+  isRequired = false,
+  placeholder = '',
+}) => {
+  return (
+    <div className={cn(className)}>
+      <div className='flex py-[7px]'>
+        <div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-gray-900')}>{label} </div>
+        {isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
+      </div>
+      <Input
+        value={value}
+        onChange={e => onChange(e.target.value)}
+        className='h-9'
+        placeholder={placeholder}
+      />
+    </div>
+  )
+}
+export default React.memo(Field)

+ 179 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx

@@ -0,0 +1,179 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { usePathname } from 'next/navigation'
+import { useBoolean } from 'ahooks'
+import type { LangFuseConfig, LangSmithConfig } from './type'
+import { TracingProvider } from './type'
+import TracingIcon from './tracing-icon'
+import ConfigButton from './config-button'
+import cn from '@/utils/classnames'
+import { LangfuseIcon, LangsmithIcon } from '@/app/components/base/icons/src/public/tracing'
+import Indicator from '@/app/components/header/indicator'
+import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
+import type { TracingStatus } from '@/models/app'
+import Toast from '@/app/components/base/toast'
+import { useAppContext } from '@/context/app-context'
+import Loading from '@/app/components/base/loading'
+
+const I18N_PREFIX = 'app.tracing'
+
+const Title = ({
+  className,
+}: {
+  className?: string
+}) => {
+  const { t } = useTranslation()
+
+  return (
+    <div className={cn(className, 'flex items-center text-lg font-semibold text-gray-900')}>
+      {t('common.appMenus.overview')}
+    </div>
+  )
+}
+const Panel: FC = () => {
+  const { t } = useTranslation()
+  const pathname = usePathname()
+  const matched = pathname.match(/\/app\/([^/]+)/)
+  const appId = (matched?.length && matched[1]) ? matched[1] : ''
+  const { isCurrentWorkspaceEditor } = useAppContext()
+  const readOnly = !isCurrentWorkspaceEditor
+
+  const [isLoaded, {
+    setTrue: setLoaded,
+  }] = useBoolean(false)
+
+  const [tracingStatus, setTracingStatus] = useState<TracingStatus | null>(null)
+  const enabled = tracingStatus?.enabled || false
+  const handleTracingStatusChange = async (tracingStatus: TracingStatus, noToast?: boolean) => {
+    await updateTracingStatus({ appId, body: tracingStatus })
+    setTracingStatus(tracingStatus)
+    if (!noToast) {
+      Toast.notify({
+        type: 'success',
+        message: t('common.api.success'),
+      })
+    }
+  }
+
+  const handleTracingEnabledChange = (enabled: boolean) => {
+    handleTracingStatusChange({
+      tracing_provider: tracingStatus?.tracing_provider || null,
+      enabled,
+    })
+  }
+  const handleChooseProvider = (provider: TracingProvider) => {
+    handleTracingStatusChange({
+      tracing_provider: provider,
+      enabled: true,
+    })
+  }
+  const inUseTracingProvider: TracingProvider | null = tracingStatus?.tracing_provider || null
+  const InUseProviderIcon = inUseTracingProvider === TracingProvider.langSmith ? LangsmithIcon : LangfuseIcon
+
+  const [langSmithConfig, setLangSmithConfig] = useState<LangSmithConfig | null>(null)
+  const [langFuseConfig, setLangFuseConfig] = useState<LangFuseConfig | null>(null)
+  const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig)
+
+  const fetchTracingConfig = async () => {
+    const { tracing_config: langSmithConfig, has_not_configured: langSmithHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langSmith })
+    if (!langSmithHasNotConfig)
+      setLangSmithConfig(langSmithConfig as LangSmithConfig)
+    const { tracing_config: langFuseConfig, has_not_configured: langFuseHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langfuse })
+    if (!langFuseHasNotConfig)
+      setLangFuseConfig(langFuseConfig as LangFuseConfig)
+  }
+
+  const handleTracingConfigUpdated = async (provider: TracingProvider) => {
+    // call api to hide secret key value
+    const { tracing_config } = await doFetchTracingConfig({ appId, provider })
+    if (provider === TracingProvider.langSmith)
+      setLangSmithConfig(tracing_config as LangSmithConfig)
+    else
+      setLangFuseConfig(tracing_config as LangFuseConfig)
+  }
+
+  const handleTracingConfigRemoved = (provider: TracingProvider) => {
+    if (provider === TracingProvider.langSmith)
+      setLangSmithConfig(null)
+    else
+      setLangFuseConfig(null)
+    if (provider === inUseTracingProvider) {
+      handleTracingStatusChange({
+        enabled: false,
+        tracing_provider: null,
+      }, true)
+    }
+  }
+
+  useEffect(() => {
+    (async () => {
+      const tracingStatus = await fetchTracingStatus({ appId })
+      setTracingStatus(tracingStatus)
+      await fetchTracingConfig()
+      setLoaded()
+    })()
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  const [controlShowPopup, setControlShowPopup] = useState<number>(0)
+  const showPopup = useCallback(() => {
+    setControlShowPopup(Date.now())
+  }, [setControlShowPopup])
+  if (!isLoaded) {
+    return (
+      <div className='flex items-center justify-between mb-3'>
+        <Title className='h-[41px]' />
+        <div className='w-[200px]'>
+          <Loading />
+        </div>
+      </div>
+    )
+  }
+
+  return (
+    <div className={cn('mb-3 flex justify-between items-center')}>
+      <Title className='h-[41px]' />
+      <div className='flex items-center p-2 rounded-xl border-[0.5px] border-gray-200 shadow-xs cursor-pointer hover:bg-gray-100' onClick={showPopup}>
+        {!inUseTracingProvider
+          ? <>
+            <TracingIcon size='md' className='mr-2' />
+            <div className='leading-5 text-sm font-semibold text-gray-700'>{t(`${I18N_PREFIX}.title`)}</div>
+          </>
+          : <InUseProviderIcon className='ml-1 h-4' />}
+
+        {hasConfiguredTracing && (
+          <div className='ml-4 mr-1 flex items-center'>
+            <Indicator color={enabled ? 'green' : 'gray'} />
+            <div className='ml-1.5 text-xs font-semibold text-gray-500 uppercase'>
+              {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
+            </div>
+          </div>
+        )}
+
+        {hasConfiguredTracing && (
+          <div className='ml-2 w-px h-3.5 bg-gray-200'></div>
+        )}
+        <div className='flex items-center' onClick={e => e.stopPropagation()}>
+          <ConfigButton
+            appId={appId}
+            readOnly={readOnly}
+            hasConfigured
+            className='ml-2'
+            enabled={enabled}
+            onStatusChange={handleTracingEnabledChange}
+            chosenProvider={inUseTracingProvider}
+            onChooseProvider={handleChooseProvider}
+            langSmithConfig={langSmithConfig}
+            langFuseConfig={langFuseConfig}
+            onConfigUpdated={handleTracingConfigUpdated}
+            onConfigRemoved={handleTracingConfigRemoved}
+            controlShowPopup={controlShowPopup}
+          />
+        </div>
+      </div>
+    </div>
+  )
+}
+export default React.memo(Panel)

+ 291 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx

@@ -0,0 +1,291 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useBoolean } from 'ahooks'
+import Field from './field'
+import type { LangFuseConfig, LangSmithConfig } from './type'
+import { TracingProvider } from './type'
+import { docURL } from './config'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+} from '@/app/components/base/portal-to-follow-elem'
+import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
+import Button from '@/app/components/base/button'
+import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
+import Confirm from '@/app/components/base/confirm'
+import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
+import Toast from '@/app/components/base/toast'
+
+type Props = {
+  appId: string
+  type: TracingProvider
+  payload?: LangSmithConfig | LangFuseConfig | null
+  onRemoved: () => void
+  onCancel: () => void
+  onSaved: (payload: LangSmithConfig | LangFuseConfig) => void
+  onChosen: (provider: TracingProvider) => void
+}
+
+const I18N_PREFIX = 'app.tracing.configProvider'
+
+const langSmithConfigTemplate = {
+  api_key: '',
+  project: '',
+  endpoint: '',
+}
+
+const langFuseConfigTemplate = {
+  public_key: '',
+  secret_key: '',
+  host: '',
+}
+
+const ProviderConfigModal: FC<Props> = ({
+  appId,
+  type,
+  payload,
+  onRemoved,
+  onCancel,
+  onSaved,
+  onChosen,
+}) => {
+  const { t } = useTranslation()
+  const isEdit = !!payload
+  const isAdd = !isEdit
+  const [isSaving, setIsSaving] = useState(false)
+  const [config, setConfig] = useState<LangSmithConfig | LangFuseConfig>((() => {
+    if (isEdit)
+      return payload
+
+    if (type === TracingProvider.langSmith)
+      return langSmithConfigTemplate
+
+    return langFuseConfigTemplate
+  })())
+  const [isShowRemoveConfirm, {
+    setTrue: showRemoveConfirm,
+    setFalse: hideRemoveConfirm,
+  }] = useBoolean(false)
+
+  const handleRemove = useCallback(async () => {
+    await removeTracingConfig({
+      appId,
+      provider: type,
+    })
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.remove'),
+    })
+    onRemoved()
+    hideRemoveConfirm()
+  }, [hideRemoveConfirm, appId, type, t, onRemoved])
+
+  const handleConfigChange = useCallback((key: string) => {
+    return (value: string) => {
+      setConfig({
+        ...config,
+        [key]: value,
+      })
+    }
+  }, [config])
+
+  const checkValid = useCallback(() => {
+    let errorMessage = ''
+    if (type === TracingProvider.langSmith) {
+      const postData = config as LangSmithConfig
+      if (!postData.api_key)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
+      if (!errorMessage && !postData.project)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
+    }
+
+    if (type === TracingProvider.langfuse) {
+      const postData = config as LangFuseConfig
+      if (!errorMessage && !postData.secret_key)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.secretKey`) })
+      if (!errorMessage && !postData.public_key)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.publicKey`) })
+      if (!errorMessage && !postData.host)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' })
+    }
+
+    return errorMessage
+  }, [config, t, type])
+  const handleSave = useCallback(async () => {
+    if (isSaving)
+      return
+    const errorMessage = checkValid()
+    if (errorMessage) {
+      Toast.notify({
+        type: 'error',
+        message: errorMessage,
+      })
+      return
+    }
+    const action = isEdit ? updateTracingConfig : addTracingConfig
+    try {
+      await action({
+        appId,
+        body: {
+          tracing_provider: type,
+          tracing_config: config,
+        },
+      })
+      Toast.notify({
+        type: 'success',
+        message: t('common.api.success'),
+      })
+      onSaved(config)
+      if (isAdd)
+        onChosen(type)
+    }
+    finally {
+      setIsSaving(false)
+    }
+  }, [appId, checkValid, config, isAdd, isEdit, isSaving, onChosen, onSaved, t, type])
+
+  return (
+    <>
+      {!isShowRemoveConfirm
+        ? (
+          <PortalToFollowElem open>
+            <PortalToFollowElemContent className='w-full h-full z-[60]'>
+              <div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
+                <div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
+                  <div className='px-8 pt-8'>
+                    <div className='flex justify-between items-center mb-4'>
+                      <div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
+                    </div>
+
+                    <div className='space-y-4'>
+                      {type === TracingProvider.langSmith && (
+                        <>
+                          <Field
+                            label='API Key'
+                            labelClassName='!text-sm'
+                            isRequired
+                            value={(config as LangSmithConfig).api_key}
+                            onChange={handleConfigChange('api_key')}
+                            placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
+                          />
+                          <Field
+                            label={t(`${I18N_PREFIX}.project`)!}
+                            labelClassName='!text-sm'
+                            isRequired
+                            value={(config as LangSmithConfig).project}
+                            onChange={handleConfigChange('project')}
+                            placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
+                          />
+                          <Field
+                            label='Endpoint'
+                            labelClassName='!text-sm'
+                            value={(config as LangSmithConfig).endpoint}
+                            onChange={handleConfigChange('endpoint')}
+                            placeholder={'https://api.smith.langchain.com'}
+                          />
+                        </>
+                      )}
+                      {type === TracingProvider.langfuse && (
+                        <>
+                          <Field
+                            label={t(`${I18N_PREFIX}.secretKey`)!}
+                            labelClassName='!text-sm'
+                            value={(config as LangFuseConfig).secret_key}
+                            isRequired
+                            onChange={handleConfigChange('secret_key')}
+                            placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.secretKey`) })!}
+                          />
+                          <Field
+                            label={t(`${I18N_PREFIX}.publicKey`)!}
+                            labelClassName='!text-sm'
+                            isRequired
+                            value={(config as LangFuseConfig).public_key}
+                            onChange={handleConfigChange('public_key')}
+                            placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!}
+                          />
+                          <Field
+                            label='Host'
+                            labelClassName='!text-sm'
+                            isRequired
+                            value={(config as LangFuseConfig).host}
+                            onChange={handleConfigChange('host')}
+                            placeholder='https://cloud.langfuse.com'
+                          />
+                        </>
+                      )}
+
+                    </div>
+                    <div className='my-8 flex justify-between items-center h-8'>
+                      <a
+                        className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]'
+                        target='_blank'
+                        href={docURL[type]}
+                      >
+                        <span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
+                        <LinkExternal02 className='w-3 h-3' />
+                      </a>
+                      <div className='flex items-center'>
+                        {isEdit && (
+                          <>
+                            <Button
+                              className='h-9 text-sm font-medium text-gray-700'
+                              onClick={showRemoveConfirm}
+                            >
+                              <span className='text-[#D92D20]'>{t('common.operation.remove')}</span>
+                            </Button>
+                            <div className='mx-3 w-px h-[18px] bg-gray-200'></div>
+                          </>
+                        )}
+                        <Button
+                          className='mr-2 h-9 text-sm font-medium text-gray-700'
+                          onClick={onCancel}
+                        >
+                          {t('common.operation.cancel')}
+                        </Button>
+                        <Button
+                          className='h-9 text-sm font-medium'
+                          variant='primary'
+                          onClick={handleSave}
+                          loading={isSaving}
+                        >
+                          {t(`common.operation.${isAdd ? 'saveAndEnable' : 'save'}`)}
+                        </Button>
+                      </div>
+
+                    </div>
+                  </div>
+                  <div className='border-t-[0.5px] border-t-black/5'>
+                    <div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
+                      <Lock01 className='mr-1 w-3 h-3 text-gray-500' />
+                      {t('common.modelProvider.encrypted.front')}
+                      <a
+                        className='text-primary-600 mx-1'
+                        target='_blank' rel='noopener noreferrer'
+                        href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
+                      >
+                        PKCS1_OAEP
+                      </a>
+                      {t('common.modelProvider.encrypted.back')}
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </PortalToFollowElemContent>
+          </PortalToFollowElem>
+        )
+        : (
+          <Confirm
+            isShow
+            type='warning'
+            title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!}
+            content={t(`${I18N_PREFIX}.removeConfirmContent`)}
+            onConfirm={handleRemove}
+            onCancel={hideRemoveConfirm}
+          />
+        )}
+    </>
+  )
+}
+export default React.memo(ProviderConfigModal)

+ 97 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx

@@ -0,0 +1,97 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import { TracingProvider } from './type'
+import cn from '@/utils/classnames'
+import { LangfuseIconBig, LangsmithIconBig } from '@/app/components/base/icons/src/public/tracing'
+import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
+import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general'
+
+const I18N_PREFIX = 'app.tracing'
+
+type Props = {
+  type: TracingProvider
+  readOnly: boolean
+  isChosen: boolean
+  config: any
+  onChoose: () => void
+  hasConfigured: boolean
+  onConfig: () => void
+}
+
+const getIcon = (type: TracingProvider) => {
+  return ({
+    [TracingProvider.langSmith]: LangsmithIconBig,
+    [TracingProvider.langfuse]: LangfuseIconBig,
+  })[type]
+}
+
+const ProviderPanel: FC<Props> = ({
+  type,
+  readOnly,
+  isChosen,
+  config,
+  onChoose,
+  hasConfigured,
+  onConfig,
+}) => {
+  const { t } = useTranslation()
+  const Icon = getIcon(type)
+
+  const handleConfigBtnClick = useCallback((e: React.MouseEvent) => {
+    e.stopPropagation()
+    onConfig()
+  }, [onConfig])
+
+  const viewBtnClick = useCallback((e: React.MouseEvent) => {
+    e.preventDefault()
+    e.stopPropagation()
+
+    const url = config?.project_url
+    if (url)
+      window.open(url, '_blank', 'noopener,noreferrer')
+  }, [config?.project_url])
+
+  const handleChosen = useCallback((e: React.MouseEvent) => {
+    e.stopPropagation()
+    if (isChosen || !hasConfigured || readOnly)
+      return
+    onChoose()
+  }, [hasConfigured, isChosen, onChoose, readOnly])
+  return (
+    <div
+      className={cn(isChosen ? 'border-primary-400' : 'border-transparent', !isChosen && hasConfigured && !readOnly && 'cursor-pointer', 'px-4 py-3 rounded-xl border-[1.5px]  bg-gray-100')}
+      onClick={handleChosen}
+    >
+      <div className={'flex justify-between items-center space-x-1'}>
+        <div className='flex items-center'>
+          <Icon className='h-6' />
+          {isChosen && <div className='ml-1 flex items-center h-4  px-1 rounded-[4px] border border-primary-500 leading-4 text-xs font-medium text-primary-500 uppercase '>{t(`${I18N_PREFIX}.inUse`)}</div>}
+        </div>
+        {!readOnly && (
+          <div className={'flex justify-between items-center space-x-1'}>
+            {hasConfigured && (
+              <div className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1' onClick={viewBtnClick} >
+                <View className='w-3 h-3'/>
+                <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div>
+              </div>
+            )}
+            <div
+              className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1'
+              onClick={handleConfigBtnClick}
+            >
+              <Settings04 className='w-3 h-3' />
+              <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div>
+            </div>
+          </div>
+        )}
+
+      </div>
+      <div className='mt-2 leading-4 text-xs font-normal text-gray-500'>
+        {t(`${I18N_PREFIX}.${type}.description`)}
+      </div>
+    </div>
+  )
+}
+export default React.memo(ProviderPanel)

+ 45 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx

@@ -0,0 +1,45 @@
+'use client'
+import { ChevronDoubleDownIcon } from '@heroicons/react/20/solid'
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import React, { useCallback } from 'react'
+import Tooltip from '@/app/components/base/tooltip'
+
+const I18N_PREFIX = 'app.tracing'
+
+type Props = {
+  isFold: boolean
+  onFoldChange: (isFold: boolean) => void
+}
+
+const ToggleFoldBtn: FC<Props> = ({
+  isFold,
+  onFoldChange,
+}) => {
+  const { t } = useTranslation()
+
+  const handleFoldChange = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
+    e.stopPropagation()
+    onFoldChange(!isFold)
+  }, [isFold, onFoldChange])
+  return (
+    // text-[0px] to hide spacing between tooltip elements
+    <div className='shrink-0 cursor-pointer text-[0px]' onClick={handleFoldChange}>
+      <Tooltip
+        popupContent={t(`${I18N_PREFIX}.${isFold ? 'expand' : 'collapse'}`)}
+      >
+        {isFold && (
+          <div className='p-1 rounded-md text-gray-500 hover:text-gray-800 hover:bg-black/5'>
+            <ChevronDoubleDownIcon className='w-4 h-4' />
+          </div>
+        )}
+        {!isFold && (
+          <div className='p-2 rounded-lg text-gray-500 border-[0.5px] border-gray-200 hover:text-gray-800 hover:bg-black/5'>
+            <ChevronDoubleDownIcon className='w-4 h-4 transform rotate-180' />
+          </div>
+        )}
+      </Tooltip>
+    </div>
+  )
+}
+export default React.memo(ToggleFoldBtn)

+ 28 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx

@@ -0,0 +1,28 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import cn from '@/utils/classnames'
+import { TracingIcon as Icon } from '@/app/components/base/icons/src/public/tracing'
+
+type Props = {
+  className?: string
+  size: 'lg' | 'md'
+}
+
+const sizeClassMap = {
+  lg: 'w-9 h-9 p-2 rounded-[10px]',
+  md: 'w-6 h-6 p-1 rounded-lg',
+}
+
+const TracingIcon: FC<Props> = ({
+  className,
+  size,
+}) => {
+  const sizeClass = sizeClassMap[size]
+  return (
+    <div className={cn(className, sizeClass, 'bg-primary-500 shadow-md')}>
+      <Icon className='w-full h-full' />
+    </div>
+  )
+}
+export default React.memo(TracingIcon)

+ 16 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts

@@ -0,0 +1,16 @@
+export enum TracingProvider {
+  langSmith = 'langsmith',
+  langfuse = 'langfuse',
+}
+
+export type LangSmithConfig = {
+  api_key: string
+  project: string
+  endpoint: string
+}
+
+export type LangFuseConfig = {
+  public_key: string
+  secret_key: string
+  host: string
+}

+ 6 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css

@@ -0,0 +1,6 @@
+.app {
+    flex-grow: 1;
+    height: 0;
+    border-radius: 16px 16px 0px 0px;
+    box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 2px -1px rgba(0, 0, 0, 0.03);
+}

+ 12 - 0
app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx

@@ -0,0 +1,12 @@
+'use client'
+
+import Workflow from '@/app/components/workflow'
+
+const Page = () => {
+  return (
+    <div className='w-full h-full overflow-x-auto'>
+      <Workflow />
+    </div>
+  )
+}
+export default Page

+ 27 - 0
app/(commonLayout)/app/(appDetailLayout)/layout.tsx

@@ -0,0 +1,27 @@
+'use client'
+import type { FC } from 'react'
+import React, { useEffect } from 'react'
+import { useRouter } from 'next/navigation'
+import { useAppContext } from '@/context/app-context'
+
+export type IAppDetail = {
+  children: React.ReactNode
+}
+
+const AppDetail: FC<IAppDetail> = ({ children }) => {
+  const router = useRouter()
+  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
+
+  useEffect(() => {
+    if (isCurrentWorkspaceDatasetOperator)
+      return router.replace('/datasets')
+  }, [isCurrentWorkspaceDatasetOperator])
+
+  return (
+    <>
+      {children}
+    </>
+  )
+}
+
+export default React.memo(AppDetail)

+ 422 - 0
app/(commonLayout)/apps/AppCard.tsx

@@ -0,0 +1,422 @@
+'use client'
+
+import { useContext, useContextSelector } from 'use-context-selector'
+import { useRouter } from 'next/navigation'
+import { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { RiMoreFill } from '@remixicon/react'
+import s from './style.module.css'
+import cn from '@/utils/classnames'
+import type { App } from '@/types/app'
+import Confirm from '@/app/components/base/confirm'
+import { ToastContext } from '@/app/components/base/toast'
+import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
+import DuplicateAppModal from '@/app/components/app/duplicate-modal'
+import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
+import AppIcon from '@/app/components/base/app-icon'
+import AppsContext, { useAppContext } from '@/context/app-context'
+import type { HtmlContentProps } from '@/app/components/base/popover'
+import CustomPopover from '@/app/components/base/popover'
+import Divider from '@/app/components/base/divider'
+import { getRedirection } from '@/utils/app-redirection'
+import { useProviderContext } from '@/context/provider-context'
+import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
+import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
+import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
+import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
+import EditAppModal from '@/app/components/explore/create-app-modal'
+import SwitchAppModal from '@/app/components/app/switch-app-modal'
+import type { Tag } from '@/app/components/base/tag-management/constant'
+import TagSelector from '@/app/components/base/tag-management/selector'
+import type { EnvironmentVariable } from '@/app/components/workflow/types'
+import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
+import { fetchWorkflowDraft } from '@/service/workflow'
+
+export type AppCardProps = {
+  app: App
+  onRefresh?: () => void
+}
+
+const AppCard = ({ app, onRefresh }: AppCardProps) => {
+  const { t } = useTranslation()
+  const { notify } = useContext(ToastContext)
+  const { isCurrentWorkspaceEditor } = useAppContext()
+  const { onPlanInfoChanged } = useProviderContext()
+  const { push } = useRouter()
+
+  const mutateApps = useContextSelector(
+    AppsContext,
+    state => state.mutateApps,
+  )
+
+  const [showEditModal, setShowEditModal] = useState(false)
+  const [showDuplicateModal, setShowDuplicateModal] = useState(false)
+  const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
+  const [showConfirmDelete, setShowConfirmDelete] = useState(false)
+  const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
+
+  const onConfirmDelete = useCallback(async () => {
+    try {
+      await deleteApp(app.id)
+      notify({ type: 'success', message: t('app.appDeleted') })
+      if (onRefresh)
+        onRefresh()
+      mutateApps()
+      onPlanInfoChanged()
+    }
+    catch (e: any) {
+      notify({
+        type: 'error',
+        message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
+      })
+    }
+    setShowConfirmDelete(false)
+  }, [app.id])
+
+  const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
+    name,
+    icon_type,
+    icon,
+    icon_background,
+    description,
+    use_icon_as_answer_icon,
+  }) => {
+    try {
+      await updateAppInfo({
+        appID: app.id,
+        name,
+        icon_type,
+        icon,
+        icon_background,
+        description,
+        use_icon_as_answer_icon,
+      })
+      setShowEditModal(false)
+      notify({
+        type: 'success',
+        message: t('app.editDone'),
+      })
+      if (onRefresh)
+        onRefresh()
+      mutateApps()
+    }
+    catch (e) {
+      notify({ type: 'error', message: t('app.editFailed') })
+    }
+  }, [app.id, mutateApps, notify, onRefresh, t])
+
+  const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => {
+    try {
+      const newApp = await copyApp({
+        appID: app.id,
+        name,
+        icon_type,
+        icon,
+        icon_background,
+        mode: app.mode,
+      })
+      setShowDuplicateModal(false)
+      notify({
+        type: 'success',
+        message: t('app.newApp.appCreated'),
+      })
+      localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
+      if (onRefresh)
+        onRefresh()
+      mutateApps()
+      onPlanInfoChanged()
+      getRedirection(isCurrentWorkspaceEditor, newApp, push)
+    }
+    catch (e) {
+      notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
+    }
+  }
+
+  const onExport = async (include = false) => {
+    try {
+      const { data } = await exportAppConfig({
+        appID: app.id,
+        include,
+      })
+      const a = document.createElement('a')
+      const file = new Blob([data], { type: 'application/yaml' })
+      a.href = URL.createObjectURL(file)
+      a.download = `${app.name}.yml`
+      a.click()
+    }
+    catch (e) {
+      notify({ type: 'error', message: t('app.exportFailed') })
+    }
+  }
+
+  const exportCheck = async () => {
+    if (app.mode !== 'workflow' && app.mode !== 'advanced-chat') {
+      onExport()
+      return
+    }
+    try {
+      const workflowDraft = await fetchWorkflowDraft(`/apps/${app.id}/workflows/draft`)
+      const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
+      if (list.length === 0) {
+        onExport()
+        return
+      }
+      setSecretEnvList(list)
+    }
+    catch (e) {
+      notify({ type: 'error', message: t('app.exportFailed') })
+    }
+  }
+
+  const onSwitch = () => {
+    if (onRefresh)
+      onRefresh()
+    mutateApps()
+    setShowSwitchModal(false)
+  }
+
+  const Operations = (props: HtmlContentProps) => {
+    const onMouseLeave = async () => {
+      props.onClose?.()
+    }
+    const onClickSettings = async (e: React.MouseEvent<HTMLButtonElement>) => {
+      e.stopPropagation()
+      props.onClick?.()
+      e.preventDefault()
+      setShowEditModal(true)
+    }
+    const onClickDuplicate = async (e: React.MouseEvent<HTMLButtonElement>) => {
+      e.stopPropagation()
+      props.onClick?.()
+      e.preventDefault()
+      setShowDuplicateModal(true)
+    }
+    const onClickExport = async (e: React.MouseEvent<HTMLButtonElement>) => {
+      e.stopPropagation()
+      props.onClick?.()
+      e.preventDefault()
+      exportCheck()
+    }
+    const onClickSwitch = async (e: React.MouseEvent<HTMLDivElement>) => {
+      e.stopPropagation()
+      props.onClick?.()
+      e.preventDefault()
+      setShowSwitchModal(true)
+    }
+    const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
+      e.stopPropagation()
+      props.onClick?.()
+      e.preventDefault()
+      setShowConfirmDelete(true)
+    }
+    return (
+      <div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
+        <button className={s.actionItem} onClick={onClickSettings}>
+          <span className={s.actionName}>{t('app.editApp')}</span>
+        </button>
+        <Divider className="!my-1" />
+        <button className={s.actionItem} onClick={onClickDuplicate}>
+          <span className={s.actionName}>{t('app.duplicate')}</span>
+        </button>
+        <button className={s.actionItem} onClick={onClickExport}>
+          <span className={s.actionName}>{t('app.export')}</span>
+        </button>
+        {(app.mode === 'completion' || app.mode === 'chat') && (
+          <>
+            <Divider className="!my-1" />
+            <div
+              className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
+              onClick={onClickSwitch}
+            >
+              <span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
+            </div>
+          </>
+        )}
+        <Divider className="!my-1" />
+        <div
+          className={cn(s.actionItem, s.deleteActionItem, 'group')}
+          onClick={onClickDelete}
+        >
+          <span className={cn(s.actionName, 'group-hover:text-red-500')}>
+            {t('common.operation.delete')}
+          </span>
+        </div>
+      </div>
+    )
+  }
+
+  const [tags, setTags] = useState<Tag[]>(app.tags)
+  useEffect(() => {
+    setTags(app.tags)
+  }, [app.tags])
+
+  return (
+    <>
+      <div
+        onClick={(e) => {
+          e.preventDefault()
+          getRedirection(isCurrentWorkspaceEditor, app, push)
+        }}
+        className='relative group col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
+      >
+        <div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
+          <div className='relative shrink-0'>
+            <AppIcon
+              size="large"
+              iconType={app.icon_type}
+              icon={app.icon}
+              background={app.icon_background}
+              imageUrl={app.icon_url}
+            />
+            <span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
+              {app.mode === 'advanced-chat' && (
+                <ChatBot className='w-3 h-3 text-[#1570EF]' />
+              )}
+              {app.mode === 'agent-chat' && (
+                <CuteRobot className='w-3 h-3 text-indigo-600' />
+              )}
+              {app.mode === 'chat' && (
+                <ChatBot className='w-3 h-3 text-[#1570EF]' />
+              )}
+              {app.mode === 'completion' && (
+                <AiText className='w-3 h-3 text-[#0E9384]' />
+              )}
+              {app.mode === 'workflow' && (
+                <Route className='w-3 h-3 text-[#f79009]' />
+              )}
+            </span>
+          </div>
+          <div className='grow w-0 py-[1px]'>
+            <div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
+              <div className='truncate' title={app.name}>{app.name}</div>
+            </div>
+            <div className='flex items-center text-[10px] leading-[18px] text-gray-500 font-medium'>
+              {app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
+              {app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
+              {app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
+              {app.mode === 'workflow' && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
+              {app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
+            </div>
+          </div>
+        </div>
+        <div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-gray-500'>
+          <div
+            className={cn(tags.length ? 'line-clamp-2' : 'line-clamp-4', 'group-hover:line-clamp-2')}
+            title={app.description}
+          >
+            {app.description}
+          </div>
+        </div>
+        <div className={cn(
+          'absolute bottom-1 left-0 right-0 items-center shrink-0 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
+          tags.length ? 'flex' : '!hidden group-hover:!flex',
+        )}>
+          {isCurrentWorkspaceEditor && (
+            <>
+              <div className={cn('grow flex items-center gap-1 w-0')} onClick={(e) => {
+                e.stopPropagation()
+                e.preventDefault()
+              }}>
+                <div className={cn(
+                  'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
+                  tags.length ? '!block' : '!hidden',
+                )}>
+                  <TagSelector
+                    position='bl'
+                    type='app'
+                    targetID={app.id}
+                    value={tags.map(tag => tag.id)}
+                    selectedTags={tags}
+                    onCacheUpdate={setTags}
+                    onChange={onRefresh}
+                  />
+                </div>
+              </div>
+              <div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200' />
+              <div className='!hidden group-hover:!flex shrink-0'>
+                <CustomPopover
+                  htmlContent={<Operations />}
+                  position="br"
+                  trigger="click"
+                  btnElement={
+                    <div
+                      className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
+                    >
+                      <RiMoreFill className='w-4 h-4 text-gray-700' />
+                    </div>
+                  }
+                  btnClassName={open =>
+                    cn(
+                      open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
+                      'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
+                    )
+                  }
+                  popupClassName={
+                    (app.mode === 'completion' || app.mode === 'chat')
+                      ? '!w-[238px] translate-x-[-110px]'
+                      : ''
+                  }
+                  className={'!w-[128px] h-fit !z-20'}
+                />
+              </div>
+            </>
+          )}
+        </div>
+      </div>
+      {showEditModal && (
+        <EditAppModal
+          isEditModal
+          appName={app.name}
+          appIconType={app.icon_type}
+          appIcon={app.icon}
+          appIconBackground={app.icon_background}
+          appIconUrl={app.icon_url}
+          appDescription={app.description}
+          appMode={app.mode}
+          appUseIconAsAnswerIcon={app.use_icon_as_answer_icon}
+          show={showEditModal}
+          onConfirm={onEdit}
+          onHide={() => setShowEditModal(false)}
+        />
+      )}
+      {showDuplicateModal && (
+        <DuplicateAppModal
+          appName={app.name}
+          icon_type={app.icon_type}
+          icon={app.icon}
+          icon_background={app.icon_background}
+          icon_url={app.icon_url}
+          show={showDuplicateModal}
+          onConfirm={onCopy}
+          onHide={() => setShowDuplicateModal(false)}
+        />
+      )}
+      {showSwitchModal && (
+        <SwitchAppModal
+          show={showSwitchModal}
+          appDetail={app}
+          onClose={() => setShowSwitchModal(false)}
+          onSuccess={onSwitch}
+        />
+      )}
+      {showConfirmDelete && (
+        <Confirm
+          title={t('app.deleteAppConfirmTitle')}
+          content={t('app.deleteAppConfirmContent')}
+          isShow={showConfirmDelete}
+          onConfirm={onConfirmDelete}
+          onCancel={() => setShowConfirmDelete(false)}
+        />
+      )}
+      {secretEnvList.length > 0 && (
+        <DSLExportConfirmModal
+          envList={secretEnvList}
+          onConfirm={onExport}
+          onClose={() => setSecretEnvList([])}
+        />
+      )}
+    </>
+  )
+}
+
+export default AppCard

+ 162 - 0
app/(commonLayout)/apps/Apps.tsx

@@ -0,0 +1,162 @@
+'use client'
+
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { useRouter } from 'next/navigation'
+import useSWRInfinite from 'swr/infinite'
+import { useTranslation } from 'react-i18next'
+import { useDebounceFn } from 'ahooks'
+import {
+  RiApps2Line,
+  RiExchange2Line,
+  RiMessage3Line,
+  RiRobot3Line,
+} from '@remixicon/react'
+import AppCard from './AppCard'
+import NewAppCard from './NewAppCard'
+import useAppsQueryState from './hooks/useAppsQueryState'
+import type { AppListResponse } from '@/models/app'
+import { fetchAppList } from '@/service/apps'
+import { useAppContext } from '@/context/app-context'
+import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
+import { CheckModal } from '@/hooks/use-pay'
+import TabSliderNew from '@/app/components/base/tab-slider-new'
+import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
+import Input from '@/app/components/base/input'
+import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
+import TagManagementModal from '@/app/components/base/tag-management'
+import TagFilter from '@/app/components/base/tag-management/filter'
+
+const getKey = (
+  pageIndex: number,
+  previousPageData: AppListResponse,
+  activeTab: string,
+  tags: string[],
+  keywords: string,
+) => {
+  if (!pageIndex || previousPageData.has_more) {
+    const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords } }
+
+    if (activeTab !== 'all')
+      params.params.mode = activeTab
+    else
+      delete params.params.mode
+
+    if (tags.length)
+      params.params.tag_ids = tags
+
+    return params
+  }
+  return null
+}
+
+const Apps = () => {
+  const { t } = useTranslation()
+  const router = useRouter()
+  const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
+  const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
+  const [activeTab, setActiveTab] = useTabSearchParams({
+    defaultTab: 'all',
+  })
+  const { query: { tagIDs = [], keywords = '' }, setQuery } = useAppsQueryState()
+  const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs)
+  const [searchKeywords, setSearchKeywords] = useState(keywords)
+  const setKeywords = useCallback((keywords: string) => {
+    setQuery(prev => ({ ...prev, keywords }))
+  }, [setQuery])
+  const setTagIDs = useCallback((tagIDs: string[]) => {
+    setQuery(prev => ({ ...prev, tagIDs }))
+  }, [setQuery])
+
+  const { data, isLoading, setSize, mutate } = useSWRInfinite(
+    (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, tagIDs, searchKeywords),
+    fetchAppList,
+    { revalidateFirstPage: true },
+  )
+
+  const anchorRef = useRef<HTMLDivElement>(null)
+  const options = [
+    { value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='w-[14px] h-[14px] mr-1' /> },
+    { value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='w-[14px] h-[14px] mr-1' /> },
+    { value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='w-[14px] h-[14px] mr-1' /> },
+    { value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='w-[14px] h-[14px] mr-1' /> },
+  ]
+
+  useEffect(() => {
+    document.title = `${t('common.menus.apps')} -  Dify`
+    if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
+      localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
+      mutate()
+    }
+  }, [mutate, t])
+
+  useEffect(() => {
+    if (isCurrentWorkspaceDatasetOperator)
+      return router.replace('/datasets')
+  }, [router, isCurrentWorkspaceDatasetOperator])
+
+  useEffect(() => {
+    const hasMore = data?.at(-1)?.has_more ?? true
+    let observer: IntersectionObserver | undefined
+    if (anchorRef.current) {
+      observer = new IntersectionObserver((entries) => {
+        if (entries[0].isIntersecting && !isLoading && hasMore)
+          setSize((size: number) => size + 1)
+      }, { rootMargin: '100px' })
+      observer.observe(anchorRef.current)
+    }
+    return () => observer?.disconnect()
+  }, [isLoading, setSize, anchorRef, mutate, data])
+
+  const { run: handleSearch } = useDebounceFn(() => {
+    setSearchKeywords(keywords)
+  }, { wait: 500 })
+  const handleKeywordsChange = (value: string) => {
+    setKeywords(value)
+    handleSearch()
+  }
+
+  const { run: handleTagsUpdate } = useDebounceFn(() => {
+    setTagIDs(tagFilterValue)
+  }, { wait: 500 })
+  const handleTagsChange = (value: string[]) => {
+    setTagFilterValue(value)
+    handleTagsUpdate()
+  }
+
+  return (
+    <>
+      <div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
+        <TabSliderNew
+          value={activeTab}
+          onChange={setActiveTab}
+          options={options}
+        />
+        <div className='flex items-center gap-2'>
+          <TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} />
+          <Input
+            showLeftIcon
+            showClearIcon
+            wrapperClassName='w-[200px]'
+            value={keywords}
+            onChange={e => handleKeywordsChange(e.target.value)}
+            onClear={() => handleKeywordsChange('')}
+          />
+        </div>
+      </div>
+      <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
+        {isCurrentWorkspaceEditor
+          && <NewAppCard onSuccess={mutate} />}
+        {data?.map(({ data: apps }) => apps.map(app => (
+          <AppCard key={app.id} app={app} onRefresh={mutate} />
+        )))}
+        <CheckModal />
+      </nav>
+      <div ref={anchorRef} className='h-0'> </div>
+      {showTagManagementModal && (
+        <TagManagementModal type='app' show={showTagManagementModal} />
+      )}
+    </>
+  )
+}
+
+export default Apps

+ 101 - 0
app/(commonLayout)/apps/NewAppCard.tsx

@@ -0,0 +1,101 @@
+'use client'
+
+import { forwardRef, useMemo, useState } from 'react'
+import {
+  useRouter,
+  useSearchParams,
+} from 'next/navigation'
+import { useTranslation } from 'react-i18next'
+import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
+import CreateAppModal from '@/app/components/app/create-app-modal'
+import CreateFromDSLModal, { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
+import { useProviderContext } from '@/context/provider-context'
+import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
+
+export type CreateAppCardProps = {
+  onSuccess?: () => void
+}
+
+// eslint-disable-next-line react/display-name
+const CreateAppCard = forwardRef<HTMLAnchorElement, CreateAppCardProps>(({ onSuccess }, ref) => {
+  const { t } = useTranslation()
+  const { onPlanInfoChanged } = useProviderContext()
+  const searchParams = useSearchParams()
+  const { replace } = useRouter()
+  const dslUrl = searchParams.get('remoteInstallUrl') || undefined
+
+  const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
+  const [showNewAppModal, setShowNewAppModal] = useState(false)
+  const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(!!dslUrl)
+
+  const activeTab = useMemo(() => {
+    if (dslUrl)
+      return CreateFromDSLModalTab.FROM_URL
+
+    return undefined
+  }, [dslUrl])
+
+  return (
+    <a
+      ref={ref}
+      className='relative col-span-1 flex flex-col justify-between min-h-[160px] bg-gray-200 rounded-xl border-[0.5px] border-black/5'
+    >
+      <div className='grow p-2 rounded-t-xl'>
+        <div className='px-6 pt-2 pb-1 text-xs font-medium leading-[18px] text-gray-500'>{t('app.createApp')}</div>
+        <div className='flex items-center mb-1 px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-gray-600 cursor-pointer hover:text-primary-600 hover:bg-white' onClick={() => setShowNewAppModal(true)}>
+          <FilePlus01 className='shrink-0 mr-2 w-4 h-4' />
+          {t('app.newApp.startFromBlank')}
+        </div>
+        <div className='flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-gray-600 cursor-pointer hover:text-primary-600 hover:bg-white' onClick={() => setShowNewAppTemplateDialog(true)}>
+          <FilePlus02 className='shrink-0 mr-2 w-4 h-4' />
+          {t('app.newApp.startFromTemplate')}
+        </div>
+      </div>
+      <div
+        className='p-2 border-t-[0.5px] border-black/5 rounded-b-xl'
+        onClick={() => setShowCreateFromDSLModal(true)}
+      >
+        <div className='flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-gray-600 cursor-pointer hover:text-primary-600 hover:bg-white'>
+          <FileArrow01 className='shrink-0 mr-2 w-4 h-4' />
+          {t('app.importDSL')}
+        </div>
+      </div>
+      <CreateAppModal
+        show={showNewAppModal}
+        onClose={() => setShowNewAppModal(false)}
+        onSuccess={() => {
+          onPlanInfoChanged()
+          if (onSuccess)
+            onSuccess()
+        }}
+      />
+      <CreateAppTemplateDialog
+        show={showNewAppTemplateDialog}
+        onClose={() => setShowNewAppTemplateDialog(false)}
+        onSuccess={() => {
+          onPlanInfoChanged()
+          if (onSuccess)
+            onSuccess()
+        }}
+      />
+      <CreateFromDSLModal
+        show={showCreateFromDSLModal}
+        onClose={() => {
+          setShowCreateFromDSLModal(false)
+
+          if (dslUrl)
+            replace('/')
+        }}
+        activeTab={activeTab}
+        dslUrl={dslUrl}
+        onSuccess={() => {
+          onPlanInfoChanged()
+          if (onSuccess)
+            onSuccess()
+        }}
+      />
+    </a>
+  )
+})
+
+export default CreateAppCard

+ 3 - 0
app/(commonLayout)/apps/assets/add.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 4V8M8 8V12M8 8H12M8 8H4" stroke="#6B7280" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
app/(commonLayout)/apps/assets/chat-solid.svg

@@ -0,0 +1,4 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0.631586 8.25C0.631586 6.46656 2.04586 5 3.8158 5C5.58573 5 7.00001 6.46656 7.00001 8.25C7.00001 10.0334 5.58573 11.5 3.8158 11.5C3.45197 11.5 3.10149 11.4375 2.77474 11.3222C2.72073 11.3031 2.68723 11.2913 2.66266 11.2832C2.65821 11.2817 2.65456 11.2806 2.65164 11.2796L2.64892 11.2799C2.63177 11.2818 2.60839 11.285 2.56507 11.2909L1.06766 11.4954C0.905637 11.5175 0.743029 11.459 0.632239 11.3387C0.521449 11.2185 0.476481 11.0516 0.511825 10.8919L0.817497 9.51109C0.828118 9.46311 0.833802 9.43722 0.837453 9.41817C0.83766 9.4171 0.838022 9.41517 0.838022 9.41517C0.837114 9.412 0.835963 9.40808 0.834525 9.40332C0.826292 9.37605 0.814183 9.33888 0.794499 9.27863C0.688657 8.95463 0.631586 8.60857 0.631586 8.25Z" fill="#98A2B3"/>
+<path d="M2.57377 4.1863C2.96256 4.06535 3.37698 4 3.80894 4C6.16566 4 8.00006 5.94534 8.00006 8.24999C8.00006 8.65682 7.9429 9.05245 7.8358 9.42816C8.10681 9.37948 8.36964 9.30678 8.6219 9.21229C8.65748 9.19897 8.69298 9.18534 8.72893 9.17304C8.75795 9.17641 8.78684 9.18093 8.81574 9.18517L10.4222 9.42065C10.498 9.43179 10.5841 9.44444 10.6591 9.4487C10.7422 9.45343 10.8713 9.45292 11.0081 9.39408C11.1789 9.32061 11.3164 9.18628 11.3938 9.01716C11.4558 8.88174 11.4593 8.75269 11.4564 8.66955C11.4539 8.59442 11.4433 8.5081 11.4339 8.43202L11.2309 6.78307C11.2256 6.7402 11.2229 6.71768 11.2213 6.70118C11.23 6.66505 11.2466 6.6301 11.2598 6.59546C11.4492 6.09896 11.5526 5.56093 11.5526 5C11.5526 2.51163 9.52304 0.5 7.02632 0.5C4.80843 0.5 2.95915 2.08742 2.57377 4.1863Z" fill="#98A2B3"/>
+</svg>

+ 3 - 0
app/(commonLayout)/apps/assets/chat.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.1667 6.66634H15.8333C16.2754 6.66634 16.6993 6.84194 17.0118 7.1545C17.3244 7.46706 17.5 7.89098 17.5 8.33301V13.333C17.5 13.775 17.3244 14.199 17.0118 14.5115C16.6993 14.8241 16.2754 14.9997 15.8333 14.9997H14.1667V18.333L10.8333 14.9997H7.5C7.28111 14.9999 7.06433 14.9569 6.86211 14.8731C6.6599 14.7893 6.47623 14.6663 6.32167 14.5113M6.32167 14.5113L9.16667 11.6663H12.5C12.942 11.6663 13.366 11.4907 13.6785 11.1782C13.9911 10.8656 14.1667 10.4417 14.1667 9.99967V4.99967C14.1667 4.55765 13.9911 4.13372 13.6785 3.82116C13.366 3.5086 12.942 3.33301 12.5 3.33301H4.16667C3.72464 3.33301 3.30072 3.5086 2.98816 3.82116C2.67559 4.13372 2.5 4.55765 2.5 4.99967V9.99967C2.5 10.4417 2.67559 10.8656 2.98816 11.1782C3.30072 11.4907 3.72464 11.6663 4.16667 11.6663H5.83333V14.9997L6.32167 14.5113Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
app/(commonLayout)/apps/assets/completion-solid.svg

@@ -0,0 +1,4 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 1.00779C6.5 0.994638 6.5 0.988062 6.49943 0.976137C6.48764 0.729248 6.27052 0.51224 6.02363 0.50056C6.01171 0.499996 6.0078 0.499998 6.00001 0.5H4.37933C3.97686 0.499995 3.64468 0.49999 3.37409 0.522098C3.09304 0.545061 2.83469 0.594343 2.59202 0.717989C2.2157 0.909735 1.90973 1.2157 1.71799 1.59202C1.59434 1.83469 1.54506 2.09304 1.5221 2.37409C1.49999 2.64468 1.49999 2.97686 1.5 3.37934V8.62066C1.49999 9.02313 1.49999 9.35532 1.5221 9.62591C1.54506 9.90696 1.59434 10.1653 1.71799 10.408C1.90973 10.7843 2.2157 11.0903 2.59202 11.282C2.83469 11.4057 3.09304 11.4549 3.37409 11.4779C3.64468 11.5 3.97686 11.5 4.37934 11.5H7.62066C8.02314 11.5 8.35532 11.5 8.62591 11.4779C8.90696 11.4549 9.16531 11.4057 9.40798 11.282C9.78431 11.0903 10.0903 10.7843 10.282 10.408C10.4057 10.1653 10.4549 9.90696 10.4779 9.62591C10.5 9.35532 10.5 9.02314 10.5 8.62066V4.99997C10.5 4.9922 10.5 4.98832 10.4994 4.97641C10.4878 4.72949 10.2707 4.51236 10.0238 4.50057C10.0119 4.50001 10.0054 4.50001 9.99225 4.50001L7.78404 4.50001C7.65786 4.50002 7.53496 4.50004 7.43089 4.49153C7.31659 4.48219 7.18172 4.46016 7.04601 4.39101C6.85785 4.29514 6.70487 4.14216 6.609 3.954C6.53985 3.81828 6.51781 3.68342 6.50848 3.56912C6.49997 3.46504 6.49999 3.34215 6.5 3.21596L6.5 1.00779ZM4 6.5C3.72386 6.5 3.5 6.72386 3.5 7C3.5 7.27614 3.72386 7.5 4 7.5H8C8.27614 7.5 8.5 7.27614 8.5 7C8.5 6.72386 8.27614 6.5 8 6.5H4ZM4 8.5C3.72386 8.5 3.5 8.72386 3.5 9C3.5 9.27614 3.72386 9.5 4 9.5H7C7.27614 9.5 7.5 9.27614 7.5 9C7.5 8.72386 7.27614 8.5 7 8.5H4Z" fill="#98A2B3"/>
+<path d="M9.45398 3.5C9.60079 3.5 9.67419 3.5 9.73432 3.46314C9.81925 3.41107 9.87002 3.28842 9.84674 3.19157C9.83025 3.12299 9.78238 3.07516 9.68665 2.97952L8.02049 1.31336C7.92484 1.21762 7.87701 1.16975 7.80843 1.15326C7.71158 1.12998 7.58893 1.18075 7.53687 1.26567C7.5 1.3258 7.5 1.39921 7.5 1.54602L7.5 3.09998C7.5 3.23999 7.5 3.30999 7.52725 3.36347C7.55122 3.41051 7.58946 3.44876 7.6365 3.47272C7.68998 3.49997 7.75998 3.49997 7.9 3.49998L9.45398 3.5Z" fill="#98A2B3"/>
+</svg>

+ 3 - 0
app/(commonLayout)/apps/assets/completion.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.25 11.875V9.6875C16.25 8.1342 14.9908 6.875 13.4375 6.875H12.1875C11.6697 6.875 11.25 6.45527 11.25 5.9375V4.6875C11.25 3.1342 9.9908 1.875 8.4375 1.875H6.875M6.875 12.5H13.125M6.875 15H10M8.75 1.875H4.6875C4.16973 1.875 3.75 2.29473 3.75 2.8125V17.1875C3.75 17.7053 4.16973 18.125 4.6875 18.125H15.3125C15.8303 18.125 16.25 17.7053 16.25 17.1875V9.375C16.25 5.23286 12.8921 1.875 8.75 1.875Z" stroke="#1F2A37" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
app/(commonLayout)/apps/assets/discord.svg

@@ -0,0 +1,3 @@
+<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M22.0101 4.50191C20.3529 3.74154 18.5759 3.18133 16.7179 2.86048C16.6841 2.85428 16.6503 2.86976 16.6328 2.90071C16.4043 3.30719 16.1511 3.83748 15.9738 4.25429C13.9754 3.95511 11.9873 3.95511 10.0298 4.25429C9.85253 3.82822 9.59019 3.30719 9.36062 2.90071C9.34319 2.87079 9.30939 2.85532 9.27555 2.86048C7.41857 3.18031 5.64152 3.74051 3.98335 4.50191C3.96899 4.5081 3.95669 4.51843 3.94852 4.53183C0.577841 9.56755 -0.345529 14.4795 0.107445 19.3306C0.109495 19.3543 0.122817 19.377 0.141265 19.3914C2.36514 21.0246 4.51935 22.0161 6.63355 22.6732C6.66739 22.6836 6.70324 22.6712 6.72477 22.6433C7.22489 21.9604 7.6707 21.2402 8.05293 20.4829C8.07549 20.4386 8.05396 20.386 8.00785 20.3684C7.30073 20.1002 6.6274 19.7731 5.97971 19.4017C5.92848 19.3718 5.92437 19.2985 5.9715 19.2635C6.1078 19.1613 6.24414 19.0551 6.37428 18.9478C6.39783 18.9282 6.43064 18.924 6.45833 18.9364C10.7134 20.8791 15.32 20.8791 19.5249 18.9364C19.5525 18.923 19.5854 18.9272 19.6099 18.9467C19.7401 19.054 19.8764 19.1613 20.0137 19.2635C20.0609 19.2985 20.0578 19.3718 20.0066 19.4017C19.3589 19.7804 18.6855 20.1002 17.9774 20.3674C17.9313 20.3849 17.9108 20.4386 17.9333 20.4829C18.3238 21.2392 18.7696 21.9593 19.2605 22.6423C19.281 22.6712 19.3179 22.6836 19.3517 22.6732C21.4761 22.0161 23.6303 21.0246 25.8542 19.3914C25.8737 19.377 25.886 19.3553 25.8881 19.3316C26.4302 13.7232 24.98 8.85156 22.0439 4.53286C22.0367 4.51843 22.0245 4.5081 22.0101 4.50191ZM8.68836 16.3768C7.40729 16.3768 6.35173 15.2007 6.35173 13.7563C6.35173 12.3119 7.38682 11.1358 8.68836 11.1358C10.0001 11.1358 11.0455 12.3222 11.025 13.7563C11.025 15.2007 9.98986 16.3768 8.68836 16.3768ZM17.3276 16.3768C16.0466 16.3768 14.991 15.2007 14.991 13.7563C14.991 12.3119 16.0261 11.1358 17.3276 11.1358C18.6394 11.1358 19.6847 12.3222 19.6643 13.7563C19.6643 15.2007 18.6394 16.3768 17.3276 16.3768Z" fill="#5865F2"/>
+</svg>

+ 17 - 0
app/(commonLayout)/apps/assets/github.svg

@@ -0,0 +1,17 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_131_1011)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0003 0.5C9.15149 0.501478 6.39613 1.51046 4.22687 3.34652C2.05761 5.18259 0.615903 7.72601 0.159545 10.522C-0.296814 13.318 0.261927 16.1842 1.73587 18.6082C3.20981 21.0321 5.50284 22.8558 8.20493 23.753C8.80105 23.8636 9.0256 23.4941 9.0256 23.18C9.0256 22.8658 9.01367 21.955 9.0097 20.9592C5.6714 21.6804 4.96599 19.5505 4.96599 19.5505C4.42152 18.1674 3.63464 17.8039 3.63464 17.8039C2.54571 17.065 3.71611 17.0788 3.71611 17.0788C4.92227 17.1637 5.55616 18.3097 5.55616 18.3097C6.62521 20.1333 8.36389 19.6058 9.04745 19.2976C9.15475 18.5251 9.46673 17.9995 9.8105 17.7012C7.14383 17.4008 4.34204 16.3774 4.34204 11.8054C4.32551 10.6197 4.76802 9.47305 5.57801 8.60268C5.45481 8.30236 5.04348 7.08923 5.69524 5.44143C5.69524 5.44143 6.7027 5.12135 8.9958 6.66444C10.9627 6.12962 13.0379 6.12962 15.0047 6.66444C17.2958 5.12135 18.3013 5.44143 18.3013 5.44143C18.9551 7.08528 18.5437 8.29841 18.4205 8.60268C19.2331 9.47319 19.6765 10.6218 19.6585 11.8094C19.6585 16.3912 16.8507 17.4008 14.1801 17.6952C14.6093 18.0667 14.9928 18.7918 14.9928 19.9061C14.9928 21.5026 14.9789 22.7868 14.9789 23.18C14.9789 23.4981 15.1955 23.8695 15.8035 23.753C18.5059 22.8557 20.7992 21.0317 22.2731 18.6073C23.747 16.183 24.3055 13.3163 23.8486 10.5201C23.3917 7.7238 21.9493 5.18035 19.7793 3.34461C17.6093 1.50886 14.8533 0.500541 12.0042 0.5H12.0003Z" fill="#191717"/>
+<path d="M4.54444 17.6321C4.5186 17.6914 4.42322 17.7092 4.34573 17.6677C4.26823 17.6262 4.21061 17.5491 4.23843 17.4879C4.26625 17.4266 4.35964 17.4108 4.43714 17.4523C4.51463 17.4938 4.57424 17.5729 4.54444 17.6321Z" fill="#191717"/>
+<path d="M5.03123 18.1714C4.99008 18.192 4.943 18.1978 4.89805 18.1877C4.8531 18.1776 4.81308 18.1523 4.78483 18.1161C4.70734 18.0331 4.69143 17.9185 4.75104 17.8671C4.81066 17.8157 4.91797 17.8395 4.99546 17.9224C5.07296 18.0054 5.09084 18.12 5.03123 18.1714Z" fill="#191717"/>
+<path d="M5.50425 18.857C5.43072 18.9084 5.30553 18.857 5.23598 18.7543C5.21675 18.7359 5.20146 18.7138 5.19101 18.6893C5.18056 18.6649 5.17517 18.6386 5.17517 18.612C5.17517 18.5855 5.18056 18.5592 5.19101 18.5347C5.20146 18.5103 5.21675 18.4882 5.23598 18.4698C5.3095 18.4204 5.4347 18.4698 5.50425 18.5705C5.57379 18.6713 5.57578 18.8057 5.50425 18.857V18.857Z" fill="#191717"/>
+<path d="M6.14612 19.5207C6.08054 19.5939 5.94741 19.5741 5.83812 19.4753C5.72883 19.3765 5.70299 19.2422 5.76857 19.171C5.83414 19.0999 5.96727 19.1197 6.08054 19.2165C6.1938 19.3133 6.21566 19.4496 6.14612 19.5207V19.5207Z" fill="#191717"/>
+<path d="M7.04617 19.9081C7.01637 20.001 6.88124 20.0425 6.74612 20.003C6.611 19.9635 6.52158 19.8528 6.54741 19.758C6.57325 19.6631 6.71036 19.6197 6.84747 19.6631C6.98457 19.7066 7.07201 19.8113 7.04617 19.9081Z" fill="#191717"/>
+<path d="M8.02783 19.9752C8.02783 20.072 7.91656 20.155 7.77349 20.1569C7.63042 20.1589 7.51318 20.0799 7.51318 19.9831C7.51318 19.8863 7.62445 19.8033 7.76752 19.8013C7.91059 19.7993 8.02783 19.8764 8.02783 19.9752Z" fill="#191717"/>
+<path d="M8.9419 19.8232C8.95978 19.92 8.86042 20.0207 8.71735 20.0445C8.57428 20.0682 8.4491 20.0109 8.43121 19.916C8.41333 19.8212 8.51666 19.7185 8.65576 19.6928C8.79485 19.6671 8.92401 19.7264 8.9419 19.8232Z" fill="#191717"/>
+</g>
+<defs>
+<clipPath id="clip0_131_1011">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 3 - 0
app/(commonLayout)/apps/assets/link-gray.svg

@@ -0,0 +1,3 @@
+<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.41663 3.75033H3.24996C2.96264 3.75033 2.68709 3.86446 2.48393 4.06763C2.28076 4.27079 2.16663 4.54634 2.16663 4.83366V10.2503C2.16663 10.5376 2.28076 10.8132 2.48393 11.0164C2.68709 11.2195 2.96264 11.3337 3.24996 11.3337H8.66663C8.95394 11.3337 9.22949 11.2195 9.43266 11.0164C9.63582 10.8132 9.74996 10.5376 9.74996 10.2503V8.08366M7.58329 2.66699H10.8333M10.8333 2.66699V5.91699M10.8333 2.66699L5.41663 8.08366" stroke="#9CA3AF" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
app/(commonLayout)/apps/assets/link.svg

@@ -0,0 +1,3 @@
+<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.41663 3.75008H3.24996C2.96264 3.75008 2.68709 3.86422 2.48393 4.06738C2.28076 4.27055 2.16663 4.5461 2.16663 4.83341V10.2501C2.16663 10.5374 2.28076 10.8129 2.48393 11.0161C2.68709 11.2193 2.96264 11.3334 3.24996 11.3334H8.66663C8.95394 11.3334 9.22949 11.2193 9.43266 11.0161C9.63582 10.8129 9.74996 10.5374 9.74996 10.2501V8.08341M7.58329 2.66675H10.8333M10.8333 2.66675V5.91675M10.8333 2.66675L5.41663 8.08341" stroke="#1C64F2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
app/(commonLayout)/apps/assets/right-arrow.svg

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 2.5L10.5 6M10.5 6L7 9.5M10.5 6H1.5" stroke="#1C64F2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 53 - 0
app/(commonLayout)/apps/hooks/useAppsQueryState.ts

@@ -0,0 +1,53 @@
+import { type ReadonlyURLSearchParams, usePathname, useRouter, useSearchParams } from 'next/navigation'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+
+type AppsQuery = {
+  tagIDs?: string[]
+  keywords?: string
+}
+
+// Parse the query parameters from the URL search string.
+function parseParams(params: ReadonlyURLSearchParams): AppsQuery {
+  const tagIDs = params.get('tagIDs')?.split(';')
+  const keywords = params.get('keywords') || undefined
+  return { tagIDs, keywords }
+}
+
+// Update the URL search string with the given query parameters.
+function updateSearchParams(query: AppsQuery, current: URLSearchParams) {
+  const { tagIDs, keywords } = query || {}
+
+  if (tagIDs && tagIDs.length > 0)
+    current.set('tagIDs', tagIDs.join(';'))
+  else
+    current.delete('tagIDs')
+
+  if (keywords)
+    current.set('keywords', keywords)
+  else
+    current.delete('keywords')
+}
+
+function useAppsQueryState() {
+  const searchParams = useSearchParams()
+  const [query, setQuery] = useState<AppsQuery>(() => parseParams(searchParams))
+
+  const router = useRouter()
+  const pathname = usePathname()
+  const syncSearchParams = useCallback((params: URLSearchParams) => {
+    const search = params.toString()
+    const query = search ? `?${search}` : ''
+    router.push(`${pathname}${query}`)
+  }, [router, pathname])
+
+  // Update the URL search string whenever the query changes.
+  useEffect(() => {
+    const params = new URLSearchParams(searchParams)
+    updateSearchParams(query, params)
+    syncSearchParams(params)
+  }, [query, searchParams, syncSearchParams])
+
+  return useMemo(() => ({ query, setQuery }), [query])
+}
+
+export default useAppsQueryState

+ 25 - 0
app/(commonLayout)/apps/page.tsx

@@ -0,0 +1,25 @@
+import style from '../list.module.css'
+import Apps from './Apps'
+import classNames from '@/utils/classnames'
+import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server'
+
+const AppList = async () => {
+  const locale = getLocaleOnServer()
+  const { t } = await translate(locale, 'app')
+
+  return (
+    <div className='relative flex flex-col overflow-y-auto bg-gray-100 shrink-0 h-0 grow'>
+      <Apps />
+      <footer className='px-12 py-6 grow-0 shrink-0'>
+        <h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3>
+        <p className='mt-1 text-sm font-normal leading-tight text-gray-700'>{t('communityIntro')}</p>
+        <div className='flex items-center gap-2 mt-3'>
+          <a className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://github.com/langgenius/dify'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
+          <a className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://discord.gg/FngNHpbcY7'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
+        </div>
+      </footer>
+    </div >
+  )
+}
+
+export default AppList

+ 29 - 0
app/(commonLayout)/apps/style.module.css

@@ -0,0 +1,29 @@
+
+.commonIcon {
+  @apply w-4 h-4 inline-block align-middle;
+  background-repeat: no-repeat;
+  background-position: center center;
+  background-size: contain;
+}
+.actionIcon {
+  @apply bg-gray-500;
+  mask-image: url(~@/assets/action.svg);
+}
+.actionItem {
+  @apply h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer;
+  width: calc(100% - 0.5rem);
+}
+.deleteActionItem {
+  @apply hover:bg-red-50 !important;
+}
+.actionName {
+  @apply text-gray-700 text-sm;
+}
+
+/* .completionPic {
+  background-image: url(~@/app/components/app-sidebar/completion.png)
+}
+    
+.expertPic {
+  background-image: url(~@/app/components/app-sidebar/expert.png)
+} */

+ 11 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/api/page.tsx

@@ -0,0 +1,11 @@
+import React from 'react'
+
+type Props = {}
+
+const page = (props: Props) => {
+  return (
+    <div>dataset detail api</div>
+  )
+}
+
+export default page

+ 16 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/[documentId]/page.tsx

@@ -0,0 +1,16 @@
+import React from 'react'
+import MainDetail from '@/app/components/datasets/documents/detail'
+
+export type IDocumentDetailProps = {
+  params: { datasetId: string; documentId: string }
+}
+
+const DocumentDetail = async ({
+  params: { datasetId, documentId },
+}: IDocumentDetailProps) => {
+  return (
+    <MainDetail datasetId={datasetId} documentId={documentId} />
+  )
+}
+
+export default DocumentDetail

+ 16 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/[documentId]/settings/page.tsx

@@ -0,0 +1,16 @@
+import React from 'react'
+import Settings from '@/app/components/datasets/documents/detail/settings'
+
+export type IProps = {
+  params: { datasetId: string; documentId: string }
+}
+
+const DocumentSettings = async ({
+  params: { datasetId, documentId },
+}: IProps) => {
+  return (
+    <Settings datasetId={datasetId} documentId={documentId} />
+  )
+}
+
+export default DocumentSettings

+ 16 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/create/page.tsx

@@ -0,0 +1,16 @@
+import React from 'react'
+import DatasetUpdateForm from '@/app/components/datasets/create'
+
+export type IProps = {
+  params: { datasetId: string }
+}
+
+const Create = async ({
+  params: { datasetId },
+}: IProps) => {
+  return (
+    <DatasetUpdateForm datasetId={datasetId} />
+  )
+}
+
+export default Create

+ 16 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/page.tsx

@@ -0,0 +1,16 @@
+import React from 'react'
+import Main from '@/app/components/datasets/documents'
+
+export type IProps = {
+  params: { datasetId: string }
+}
+
+const Documents = async ({
+  params: { datasetId },
+}: IProps) => {
+  return (
+    <Main datasetId={datasetId} />
+  )
+}
+
+export default Documents

+ 9 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css

@@ -0,0 +1,9 @@
+.logTable td {
+  padding: 7px 8px;
+  box-sizing: border-box;
+  max-width: 200px;
+}
+
+.pagination li {
+  list-style: none;
+}

+ 16 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/hitTesting/page.tsx

@@ -0,0 +1,16 @@
+import React from 'react'
+import Main from '@/app/components/datasets/hit-testing'
+
+type Props = {
+  params: { datasetId: string }
+}
+
+const HitTesting = ({
+  params: { datasetId },
+}: Props) => {
+  return (
+    <Main datasetId={datasetId} />
+  )
+}
+
+export default HitTesting

+ 262 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx

@@ -0,0 +1,262 @@
+'use client'
+import type { FC, SVGProps } from 'react'
+import React, { useEffect, useMemo } from 'react'
+import { usePathname } from 'next/navigation'
+import useSWR from 'swr'
+import { useTranslation } from 'react-i18next'
+import { useBoolean } from 'ahooks'
+import {
+  Cog8ToothIcon,
+  // CommandLineIcon,
+  Squares2X2Icon,
+  // eslint-disable-next-line sort-imports
+  PuzzlePieceIcon,
+  DocumentTextIcon,
+  PaperClipIcon,
+  QuestionMarkCircleIcon,
+} from '@heroicons/react/24/outline'
+import {
+  Cog8ToothIcon as Cog8ToothSolidIcon,
+  // CommandLineIcon as CommandLineSolidIcon,
+  DocumentTextIcon as DocumentTextSolidIcon,
+} from '@heroicons/react/24/solid'
+import Link from 'next/link'
+import s from './style.module.css'
+import classNames from '@/utils/classnames'
+import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
+import type { RelatedApp, RelatedAppResponse } from '@/models/datasets'
+import AppSideBar from '@/app/components/app-sidebar'
+import Divider from '@/app/components/base/divider'
+import AppIcon from '@/app/components/base/app-icon'
+import Loading from '@/app/components/base/loading'
+import FloatPopoverContainer from '@/app/components/base/float-popover-container'
+import DatasetDetailContext from '@/context/dataset-detail'
+import { DataSourceType } from '@/models/datasets'
+import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import { LanguagesSupported } from '@/i18n/language'
+import { useStore } from '@/app/components/app/store'
+import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
+import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
+import { getLocaleOnClient } from '@/i18n'
+import { useAppContext } from '@/context/app-context'
+
+export type IAppDetailLayoutProps = {
+  children: React.ReactNode
+  params: { datasetId: string }
+}
+
+type ILikedItemProps = {
+  type?: 'plugin' | 'app'
+  appStatus?: boolean
+  detail: RelatedApp
+  isMobile: boolean
+}
+
+const LikedItem = ({
+  type = 'app',
+  detail,
+  isMobile,
+}: ILikedItemProps) => {
+  return (
+    <Link className={classNames(s.itemWrapper, 'px-2', isMobile && 'justify-center')} href={`/app/${detail?.id}/overview`}>
+      <div className={classNames(s.iconWrapper, 'mr-0')}>
+        <AppIcon size='tiny' iconType={detail.icon_type} icon={detail.icon} background={detail.icon_background} imageUrl={detail.icon_url} />
+        {type === 'app' && (
+          <span className='absolute bottom-[-2px] right-[-2px] w-3.5 h-3.5 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
+            {detail.mode === 'advanced-chat' && (
+              <ChatBot className='w-2.5 h-2.5 text-[#1570EF]' />
+            )}
+            {detail.mode === 'agent-chat' && (
+              <CuteRobot className='w-2.5 h-2.5 text-indigo-600' />
+            )}
+            {detail.mode === 'chat' && (
+              <ChatBot className='w-2.5 h-2.5 text-[#1570EF]' />
+            )}
+            {detail.mode === 'completion' && (
+              <AiText className='w-2.5 h-2.5 text-[#0E9384]' />
+            )}
+            {detail.mode === 'workflow' && (
+              <Route className='w-2.5 h-2.5 text-[#f79009]' />
+            )}
+          </span>
+        )}
+      </div>
+      {!isMobile && <div className={classNames(s.appInfo, 'ml-2')}>{detail?.name || '--'}</div>}
+    </Link>
+  )
+}
+
+const TargetIcon = ({ className }: SVGProps<SVGElement>) => {
+  return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
+    <g clipPath="url(#clip0_4610_6951)">
+      <path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
+    </g>
+    <defs>
+      <clipPath id="clip0_4610_6951">
+        <rect width="16" height="16" fill="white" />
+      </clipPath>
+    </defs>
+  </svg>
+}
+
+const TargetSolidIcon = ({ className }: SVGProps<SVGElement>) => {
+  return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
+    <path fillRule="evenodd" clipRule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
+    <path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
+    <path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
+  </svg>
+}
+
+const BookOpenIcon = ({ className }: SVGProps<SVGElement>) => {
+  return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
+    <path opacity="0.12" d="M1 3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7V10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1Z" fill="#155EEF" />
+    <path d="M6 10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7M6 10.5V4.7M6 10.5L6.05003 10.425C6.39735 9.90398 6.57101 9.64349 6.80045 9.45491C7.00357 9.28796 7.23762 9.1627 7.4892 9.0863C7.77337 9 8.08645 9 8.71259 9H9.4C9.96005 9 10.2401 9 10.454 8.89101C10.6422 8.79513 10.7951 8.64215 10.891 8.45399C11 8.24008 11 7.96005 11 7.4V3.1C11 2.53995 11 2.25992 10.891 2.04601C10.7951 1.85785 10.6422 1.70487 10.454 1.60899C10.2401 1.5 9.96005 1.5 9.4 1.5H9.2C8.07989 1.5 7.51984 1.5 7.09202 1.71799C6.71569 1.90973 6.40973 2.21569 6.21799 2.59202C6 3.01984 6 3.5799 6 4.7" stroke="#155EEF" strokeLinecap="round" strokeLinejoin="round" />
+  </svg>
+}
+
+type IExtraInfoProps = {
+  isMobile: boolean
+  relatedApps?: RelatedAppResponse
+}
+
+const ExtraInfo = ({ isMobile, relatedApps }: IExtraInfoProps) => {
+  const locale = getLocaleOnClient()
+  const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile)
+  const { t } = useTranslation()
+
+  useEffect(() => {
+    setShowTips(!isMobile)
+  }, [isMobile, setShowTips])
+
+  return <div className='w-full flex flex-col items-center'>
+    <Divider className='mt-5' />
+    {(relatedApps?.data && relatedApps?.data?.length > 0) && (
+      <>
+        {!isMobile && <div className='w-full px-2 pb-1 pt-4 uppercase text-xs text-gray-500 font-medium'>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>}
+        {isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}>
+          {relatedApps?.total || '--'}
+          <PaperClipIcon className='h-4 w-4 text-gray-700' />
+        </div>}
+        {relatedApps?.data?.map((item, index) => (<LikedItem key={index} isMobile={isMobile} detail={item} />))}
+      </>
+    )}
+    {!relatedApps?.data?.length && (
+      <FloatPopoverContainer
+        placement='bottom-start'
+        open={isShowTips}
+        toggle={toggleTips}
+        isMobile={isMobile}
+        triggerElement={
+          <div className={classNames('h-7 w-7 inline-flex justify-center items-center rounded-lg bg-transparent', isShowTips && '!bg-gray-50')}>
+            <QuestionMarkCircleIcon className='h-4 w-4 flex-shrink-0 text-gray-500' />
+          </div>
+        }
+      >
+        <div className={classNames('mt-5 p-3', isMobile && 'border-[0.5px] border-gray-200 shadow-lg rounded-lg bg-white w-[160px]')}>
+          <div className='flex items-center justify-start gap-2'>
+            <div className={s.emptyIconDiv}>
+              <Squares2X2Icon className='w-3 h-3 text-gray-500' />
+            </div>
+            <div className={s.emptyIconDiv}>
+              <PuzzlePieceIcon className='w-3 h-3 text-gray-500' />
+            </div>
+          </div>
+          <div className='text-xs text-gray-500 mt-2'>{t('common.datasetMenus.emptyTip')}</div>
+          <a
+            className='inline-flex items-center text-xs text-primary-600 mt-2 cursor-pointer'
+            href={
+              locale === LanguagesSupported[1]
+                ? 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/integrate_knowledge_within_application'
+                : 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'
+            }
+            target='_blank' rel='noopener noreferrer'
+          >
+            <BookOpenIcon className='mr-1' />
+            {t('common.datasetMenus.viewDoc')}
+          </a>
+        </div>
+      </FloatPopoverContainer>
+    )}
+  </div>
+}
+
+const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
+  const {
+    children,
+    params: { datasetId },
+  } = props
+  const pathname = usePathname()
+  const hideSideBar = /documents\/create$/.test(pathname)
+  const { t } = useTranslation()
+  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
+
+  const media = useBreakpoints()
+  const isMobile = media === MediaType.mobile
+
+  const { data: datasetRes, error, mutate: mutateDatasetRes } = useSWR({
+    url: 'fetchDatasetDetail',
+    datasetId,
+  }, apiParams => fetchDatasetDetail(apiParams.datasetId))
+
+  const { data: relatedApps } = useSWR({
+    action: 'fetchDatasetRelatedApps',
+    datasetId,
+  }, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
+
+  const navigation = useMemo(() => {
+    const baseNavigation = [
+      { name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
+      // { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
+      { name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
+    ]
+
+    if (datasetRes?.provider !== 'external') {
+      baseNavigation.unshift({
+        name: t('common.datasetMenus.documents'),
+        href: `/datasets/${datasetId}/documents`,
+        icon: DocumentTextIcon,
+        selectedIcon: DocumentTextSolidIcon,
+      })
+    }
+    return baseNavigation
+  }, [datasetRes?.provider, datasetId, t])
+
+  useEffect(() => {
+    if (datasetRes)
+      document.title = `${datasetRes.name || 'Dataset'} - Dify`
+  }, [datasetRes])
+
+  const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)
+
+  useEffect(() => {
+    const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
+    const mode = isMobile ? 'collapse' : 'expand'
+    setAppSiderbarExpand(isMobile ? mode : localeMode)
+  }, [isMobile, setAppSiderbarExpand])
+
+  if (!datasetRes && !error)
+    return <Loading />
+
+  return (
+    <div className='grow flex overflow-hidden'>
+      {!hideSideBar && <AppSideBar
+        title={datasetRes?.name || '--'}
+        icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
+        icon_background={datasetRes?.icon_background || '#F5F5F5'}
+        desc={datasetRes?.description || '--'}
+        isExternal={datasetRes?.provider === 'external'}
+        navigation={navigation}
+        extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} /> : undefined}
+        iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}
+      />}
+      <DatasetDetailContext.Provider value={{
+        indexingTechnique: datasetRes?.indexing_technique,
+        dataset: datasetRes,
+        mutateDatasetRes: () => mutateDatasetRes(),
+      }}>
+        <div className="bg-white grow overflow-hidden">{children}</div>
+      </DatasetDetailContext.Provider>
+    </div>
+  )
+}
+export default React.memo(DatasetDetailLayout)

+ 20 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx

@@ -0,0 +1,20 @@
+import React from 'react'
+import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server'
+import Form from '@/app/components/datasets/settings/form'
+
+const Settings = async () => {
+  const locale = getLocaleOnServer()
+  const { t } = await translate(locale, 'dataset-settings')
+
+  return (
+    <div className='bg-white h-full overflow-y-auto'>
+      <div className='px-6 py-3'>
+        <div className='mb-1 text-lg font-semibold text-gray-900'>{t('title')}</div>
+        <div className='text-sm text-gray-500'>{t('desc')}</div>
+      </div>
+      <Form />
+    </div>
+  )
+}
+
+export default Settings

+ 18 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/style.module.css

@@ -0,0 +1,18 @@
+.itemWrapper {
+  @apply flex items-center w-full h-10 rounded-lg hover:bg-gray-50 cursor-pointer;
+}
+.appInfo {
+  @apply truncate text-gray-700 text-sm font-normal;
+}
+.iconWrapper {
+  @apply relative w-6 h-6 rounded-lg;
+}
+.statusPoint {
+  @apply flex justify-center items-center absolute -right-0.5 -bottom-0.5 w-2.5 h-2.5 bg-white rounded;
+}
+.subTitle {
+  @apply uppercase text-xs text-gray-500 font-medium px-3 pb-2 pt-4;
+}
+.emptyIconDiv {
+  @apply h-7 w-7 bg-gray-50 border border-[#EAECF5] inline-flex justify-center items-center rounded-lg;
+}

+ 16 - 0
app/(commonLayout)/datasets/(datasetDetailLayout)/layout.tsx

@@ -0,0 +1,16 @@
+import type { FC } from 'react'
+import React from 'react'
+
+export type IDatasetDetail = {
+  children: React.ReactNode
+}
+
+const AppDetail: FC<IDatasetDetail> = ({ children }) => {
+  return (
+    <>
+      {children}
+    </>
+  )
+}
+
+export default React.memo(AppDetail)

+ 41 - 0
app/(commonLayout)/datasets/ApiServer.tsx

@@ -0,0 +1,41 @@
+'use client'
+
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import CopyFeedback from '@/app/components/base/copy-feedback'
+import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
+import { randomString } from '@/utils'
+
+type ApiServerProps = {
+  apiBaseUrl: string
+}
+const ApiServer: FC<ApiServerProps> = ({
+  apiBaseUrl,
+}) => {
+  const { t } = useTranslation()
+
+  return (
+    <div className='flex items-center flex-wrap gap-y-2'>
+      <div className='flex items-center mr-2 pl-1.5 pr-1 h-8 bg-white/80 border-[0.5px] border-white rounded-lg leading-5'>
+        <div className='mr-0.5 px-1.5 h-5 border border-gray-200 text-[11px] text-gray-500 rounded-md shrink-0'>{t('appApi.apiServer')}</div>
+        <div className='px-1 truncate w-fit sm:w-[248px] text-[13px] font-medium text-gray-800'>{apiBaseUrl}</div>
+        <div className='mx-1 w-[1px] h-[14px] bg-gray-200'></div>
+        <CopyFeedback
+          content={apiBaseUrl}
+          selectorId={randomString(8)}
+          className={'!w-6 !h-6 hover:bg-gray-200'}
+        />
+      </div>
+      <div className='flex items-center mr-2 px-3 h-8 bg-[#ECFDF3] text-xs font-semibold text-[#039855] rounded-lg border-[0.5px] border-[#D1FADF]'>
+        {t('appApi.ok')}
+      </div>
+      <SecretKeyButton
+        className='flex-shrink-0 !h-8 bg-white'
+        textCls='!text-gray-700 font-medium'
+        iconCls='stroke-[1.2px]'
+      />
+    </div>
+  )
+}
+
+export default ApiServer

+ 116 - 0
app/(commonLayout)/datasets/Container.tsx

@@ -0,0 +1,116 @@
+'use client'
+
+// Libraries
+import { useEffect, useMemo, useRef, useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { useTranslation } from 'react-i18next'
+import { useDebounceFn } from 'ahooks'
+import useSWR from 'swr'
+
+// Components
+import ExternalAPIPanel from '../../components/datasets/external-api/external-api-panel'
+import Datasets from './Datasets'
+import DatasetFooter from './DatasetFooter'
+import ApiServer from './ApiServer'
+import Doc from './Doc'
+import TabSliderNew from '@/app/components/base/tab-slider-new'
+import TagManagementModal from '@/app/components/base/tag-management'
+import TagFilter from '@/app/components/base/tag-management/filter'
+import Button from '@/app/components/base/button'
+import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
+import SearchInput from '@/app/components/base/search-input'
+
+// Services
+import { fetchDatasetApiBaseUrl } from '@/service/datasets'
+
+// Hooks
+import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
+import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
+import { useAppContext } from '@/context/app-context'
+import { useExternalApiPanel } from '@/context/external-api-panel-context'
+
+const Container = () => {
+  const { t } = useTranslation()
+  const router = useRouter()
+  const { currentWorkspace } = useAppContext()
+  const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
+  const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
+
+  const options = useMemo(() => {
+    return [
+      { value: 'dataset', text: t('dataset.datasets') },
+      ...(currentWorkspace.role === 'dataset_operator' ? [] : [{ value: 'api', text: t('dataset.datasetsApi') }]),
+    ]
+  }, [currentWorkspace.role, t])
+
+  const [activeTab, setActiveTab] = useTabSearchParams({
+    defaultTab: 'dataset',
+  })
+  const containerRef = useRef<HTMLDivElement>(null)
+  const { data } = useSWR(activeTab === 'dataset' ? null : '/datasets/api-base-info', fetchDatasetApiBaseUrl)
+
+  const [keywords, setKeywords] = useState('')
+  const [searchKeywords, setSearchKeywords] = useState('')
+  const { run: handleSearch } = useDebounceFn(() => {
+    setSearchKeywords(keywords)
+  }, { wait: 500 })
+  const handleKeywordsChange = (value: string) => {
+    setKeywords(value)
+    handleSearch()
+  }
+  const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
+  const [tagIDs, setTagIDs] = useState<string[]>([])
+  const { run: handleTagsUpdate } = useDebounceFn(() => {
+    setTagIDs(tagFilterValue)
+  }, { wait: 500 })
+  const handleTagsChange = (value: string[]) => {
+    setTagFilterValue(value)
+    handleTagsUpdate()
+  }
+
+  useEffect(() => {
+    if (currentWorkspace.role === 'normal')
+      return router.replace('/apps')
+  }, [currentWorkspace, router])
+
+  return (
+    <div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
+      <div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
+        <TabSliderNew
+          value={activeTab}
+          onChange={newActiveTab => setActiveTab(newActiveTab)}
+          options={options}
+        />
+        {activeTab === 'dataset' && (
+          <div className='flex items-center gap-2'>
+            <TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
+            <SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
+            <div className="w-[1px] h-4 bg-divider-regular" />
+            <Button
+              className='gap-0.5 shadows-shadow-xs'
+              onClick={() => setShowExternalApiPanel(true)}
+            >
+              <ApiConnectionMod className='w-4 h-4 text-components-button-secondary-text' />
+              <div className='flex px-0.5 justify-center items-center gap-1 text-components-button-secondary-text system-sm-medium'>{t('dataset.externalAPIPanelTitle')}</div>
+            </Button>
+          </div>
+        )}
+        {activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
+      </div>
+      {activeTab === 'dataset' && (
+        <>
+          <Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} />
+          <DatasetFooter />
+          {showTagManagementModal && (
+            <TagManagementModal type='knowledge' show={showTagManagementModal} />
+          )}
+        </>
+      )}
+      {activeTab === 'api' && data && <Doc apiBaseUrl={data.api_base_url || ''} />}
+
+      {showExternalApiPanel && <ExternalAPIPanel onClose={() => setShowExternalApiPanel(false)} />}
+    </div>
+  )
+}
+
+export default Container

+ 240 - 0
app/(commonLayout)/datasets/DatasetCard.tsx

@@ -0,0 +1,240 @@
+'use client'
+
+import { useContext } from 'use-context-selector'
+import { useRouter } from 'next/navigation'
+import { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { RiMoreFill } from '@remixicon/react'
+import cn from '@/utils/classnames'
+import Confirm from '@/app/components/base/confirm'
+import { ToastContext } from '@/app/components/base/toast'
+import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
+import type { DataSet } from '@/models/datasets'
+import Tooltip from '@/app/components/base/tooltip'
+import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
+import type { HtmlContentProps } from '@/app/components/base/popover'
+import CustomPopover from '@/app/components/base/popover'
+import Divider from '@/app/components/base/divider'
+import RenameDatasetModal from '@/app/components/datasets/rename-modal'
+import type { Tag } from '@/app/components/base/tag-management/constant'
+import TagSelector from '@/app/components/base/tag-management/selector'
+import CornerLabel from '@/app/components/base/corner-label'
+import { useAppContext } from '@/context/app-context'
+
+export type DatasetCardProps = {
+  dataset: DataSet
+  onSuccess?: () => void
+}
+
+const DatasetCard = ({
+  dataset,
+  onSuccess,
+}: DatasetCardProps) => {
+  const { t } = useTranslation()
+  const { notify } = useContext(ToastContext)
+  const { push } = useRouter()
+  const EXTERNAL_PROVIDER = 'external' as const
+
+  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
+  const [tags, setTags] = useState<Tag[]>(dataset.tags)
+
+  const [showRenameModal, setShowRenameModal] = useState(false)
+  const [showConfirmDelete, setShowConfirmDelete] = useState(false)
+  const [confirmMessage, setConfirmMessage] = useState<string>('')
+  const isExternalProvider = (provider: string): boolean => provider === EXTERNAL_PROVIDER
+  const detectIsUsedByApp = useCallback(async () => {
+    try {
+      const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
+      setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
+    }
+    catch (e: any) {
+      const res = await e.json()
+      notify({ type: 'error', message: res?.message || 'Unknown error' })
+    }
+
+    setShowConfirmDelete(true)
+  }, [dataset.id, notify, t])
+  const onConfirmDelete = useCallback(async () => {
+    try {
+      await deleteDataset(dataset.id)
+      notify({ type: 'success', message: t('dataset.datasetDeleted') })
+      if (onSuccess)
+        onSuccess()
+    }
+    catch (e: any) {
+    }
+    setShowConfirmDelete(false)
+  }, [dataset.id, notify, onSuccess, t])
+
+  const Operations = (props: HtmlContentProps & { showDelete: boolean }) => {
+    const onMouseLeave = async () => {
+      props.onClose?.()
+    }
+    const onClickRename = async (e: React.MouseEvent<HTMLDivElement>) => {
+      e.stopPropagation()
+      props.onClick?.()
+      e.preventDefault()
+      setShowRenameModal(true)
+    }
+    const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
+      e.stopPropagation()
+      props.onClick?.()
+      e.preventDefault()
+      detectIsUsedByApp()
+    }
+    return (
+      <div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
+        <div className='h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer' onClick={onClickRename}>
+          <span className='text-gray-700 text-sm'>{t('common.operation.settings')}</span>
+        </div>
+        {props.showDelete && (
+          <>
+            <Divider className="!my-1" />
+            <div
+              className='group h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-red-50 rounded-lg cursor-pointer'
+              onClick={onClickDelete}
+            >
+              <span className={cn('text-gray-700 text-sm', 'group-hover:text-red-500')}>
+                {t('common.operation.delete')}
+              </span>
+            </div>
+          </>
+        )}
+      </div>
+    )
+  }
+
+  useEffect(() => {
+    setTags(dataset.tags)
+  }, [dataset])
+
+  return (
+    <>
+      <div
+        className='group relative col-span-1 bg-white border-[0.5px] border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
+        data-disable-nprogress={true}
+        onClick={(e) => {
+          e.preventDefault()
+          isExternalProvider(dataset.provider)
+            ? push(`/datasets/${dataset.id}/hitTesting`)
+            : push(`/datasets/${dataset.id}/documents`)
+        }}
+      >
+        {isExternalProvider(dataset.provider) && <CornerLabel label='External' className='absolute right-0' labelClassName='rounded-tr-xl' />}
+        <div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
+          <div className={cn(
+            'shrink-0 flex items-center justify-center p-2.5 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]',
+            !dataset.embedding_available && 'opacity-50 hover:opacity-100',
+          )}>
+            <Folder className='w-5 h-5 text-[#444CE7]' />
+          </div>
+          <div className='grow w-0 py-[1px]'>
+            <div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
+              <div className={cn('truncate', !dataset.embedding_available && 'opacity-50 hover:opacity-100')} title={dataset.name}>{dataset.name}</div>
+              {!dataset.embedding_available && (
+                <Tooltip
+                  popupContent={t('dataset.unavailableTip')}
+                >
+                  <span className='shrink-0 inline-flex w-max ml-1 px-1 border border-gray-200 rounded-md text-gray-500 text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
+                </Tooltip>
+              )}
+            </div>
+            <div className='flex items-center mt-[1px] text-xs leading-[18px] text-gray-500'>
+              <div
+                className={cn('truncate', (!dataset.embedding_available || !dataset.document_count) && 'opacity-50')}
+                title={dataset.provider === 'external' ? `${dataset.app_count}${t('dataset.appCount')}` : `${dataset.document_count}${t('dataset.documentCount')} · ${Math.round(dataset.word_count / 1000)}${t('dataset.wordCount')} · ${dataset.app_count}${t('dataset.appCount')}`}
+              >
+                {dataset.provider === 'external'
+                  ? <>
+                    <span>{dataset.app_count}{t('dataset.appCount')}</span>
+                  </>
+                  : <>
+                    <span>{dataset.document_count}{t('dataset.documentCount')}</span>
+                    <span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
+                    <span>{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}</span>
+                    <span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
+                    <span>{dataset.app_count}{t('dataset.appCount')}</span>
+                  </>
+                }
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          className={cn(
+            'grow mb-2 px-[14px] max-h-[72px] text-xs leading-normal text-gray-500 group-hover:line-clamp-2 group-hover:max-h-[36px]',
+            tags.length ? 'line-clamp-2' : 'line-clamp-4',
+            !dataset.embedding_available && 'opacity-50 hover:opacity-100',
+          )}
+          title={dataset.description}>
+          {dataset.description}
+        </div>
+        <div className={cn(
+          'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
+          tags.length ? 'flex' : '!hidden group-hover:!flex',
+        )}>
+          <div className={cn('grow flex items-center gap-1 w-0', !dataset.embedding_available && 'opacity-50 hover:opacity-100')} onClick={(e) => {
+            e.stopPropagation()
+            e.preventDefault()
+          }}>
+            <div className={cn(
+              'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
+              tags.length ? '!block' : '!hidden',
+            )}>
+              <TagSelector
+                position='bl'
+                type='knowledge'
+                targetID={dataset.id}
+                value={tags.map(tag => tag.id)}
+                selectedTags={tags}
+                onCacheUpdate={setTags}
+                onChange={onSuccess}
+              />
+            </div>
+          </div>
+          <div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200' />
+          <div className='!hidden group-hover:!flex shrink-0'>
+            <CustomPopover
+              htmlContent={<Operations showDelete={!isCurrentWorkspaceDatasetOperator} />}
+              position="br"
+              trigger="click"
+              btnElement={
+                <div
+                  className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
+                >
+                  <RiMoreFill className='w-4 h-4 text-gray-700' />
+                </div>
+              }
+              btnClassName={open =>
+                cn(
+                  open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
+                  'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
+                )
+              }
+              className={'!w-[128px] h-fit !z-20'}
+            />
+          </div>
+        </div>
+      </div>
+      {showRenameModal && (
+        <RenameDatasetModal
+          show={showRenameModal}
+          dataset={dataset}
+          onClose={() => setShowRenameModal(false)}
+          onSuccess={onSuccess}
+        />
+      )}
+      {showConfirmDelete && (
+        <Confirm
+          title={t('dataset.deleteDatasetConfirmTitle')}
+          content={confirmMessage}
+          isShow={showConfirmDelete}
+          onConfirm={onConfirmDelete}
+          onCancel={() => setShowConfirmDelete(false)}
+        />
+      )}
+    </>
+  )
+}
+
+export default DatasetCard

+ 19 - 0
app/(commonLayout)/datasets/DatasetFooter.tsx

@@ -0,0 +1,19 @@
+'use client'
+
+import { useTranslation } from 'react-i18next'
+
+const DatasetFooter = () => {
+  const { t } = useTranslation()
+
+  return (
+    <footer className='px-12 py-6 grow-0 shrink-0'>
+      <h3 className='text-xl font-semibold leading-tight text-gradient'>{t('dataset.didYouKnow')}</h3>
+      <p className='mt-1 text-sm font-normal leading-tight text-gray-700'>
+        {t('dataset.intro1')}<a className='inline-flex items-center gap-1 link' target='_blank' rel='noopener noreferrer' href='/'>{t('dataset.intro2')}</a>{t('dataset.intro3')}<br />
+        {t('dataset.intro4')}<a className='inline-flex items-center gap-1 link' target='_blank' rel='noopener noreferrer' href='/'>{t('dataset.intro5')}</a>{t('dataset.intro6')}
+      </p>
+    </footer>
+  )
+}
+
+export default DatasetFooter

+ 87 - 0
app/(commonLayout)/datasets/Datasets.tsx

@@ -0,0 +1,87 @@
+'use client'
+
+import { useEffect, useRef } from 'react'
+import useSWRInfinite from 'swr/infinite'
+import { debounce } from 'lodash-es'
+import { useTranslation } from 'react-i18next'
+import NewDatasetCard from './NewDatasetCard'
+import DatasetCard from './DatasetCard'
+import type { DataSetListResponse } from '@/models/datasets'
+import { fetchDatasets } from '@/service/datasets'
+import { useAppContext } from '@/context/app-context'
+
+const getKey = (
+  pageIndex: number,
+  previousPageData: DataSetListResponse,
+  tags: string[],
+  keyword: string,
+) => {
+  if (!pageIndex || previousPageData.has_more) {
+    const params: any = {
+      url: 'datasets',
+      params: {
+        page: pageIndex + 1,
+        limit: 30,
+      },
+    }
+    if (tags.length)
+      params.params.tag_ids = tags
+    if (keyword)
+      params.params.keyword = keyword
+    return params
+  }
+  return null
+}
+
+type Props = {
+  containerRef: React.RefObject<HTMLDivElement>
+  tags: string[]
+  keywords: string
+}
+
+const Datasets = ({
+  containerRef,
+  tags,
+  keywords,
+}: Props) => {
+  const { isCurrentWorkspaceEditor } = useAppContext()
+  const { data, isLoading, setSize, mutate } = useSWRInfinite(
+    (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords),
+    fetchDatasets,
+    { revalidateFirstPage: false, revalidateAll: true },
+  )
+  const loadingStateRef = useRef(false)
+  const anchorRef = useRef<HTMLAnchorElement>(null)
+
+  const { t } = useTranslation()
+
+  useEffect(() => {
+    loadingStateRef.current = isLoading
+    document.title = `${t('dataset.knowledge')} - Dify`
+  }, [isLoading])
+
+  useEffect(() => {
+    const onScroll = debounce(() => {
+      if (!loadingStateRef.current) {
+        const { scrollTop, clientHeight } = containerRef.current!
+        const anchorOffset = anchorRef.current!.offsetTop
+        if (anchorOffset - scrollTop - clientHeight < 100)
+          setSize(size => size + 1)
+      }
+    }, 50)
+
+    containerRef.current?.addEventListener('scroll', onScroll)
+    return () => containerRef.current?.removeEventListener('scroll', onScroll)
+  }, [])
+
+  return (
+    <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
+      { isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> }
+      {data?.map(({ data: datasets }) => datasets.map(dataset => (
+        <DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
+      ))}
+    </nav>
+  )
+}
+
+export default Datasets

+ 28 - 0
app/(commonLayout)/datasets/Doc.tsx

@@ -0,0 +1,28 @@
+'use client'
+
+import type { FC } from 'react'
+import { useContext } from 'use-context-selector'
+import TemplateEn from './template/template.en.mdx'
+import TemplateZh from './template/template.zh.mdx'
+import I18n from '@/context/i18n'
+import { LanguagesSupported } from '@/i18n/language'
+
+type DocProps = {
+  apiBaseUrl: string
+}
+const Doc: FC<DocProps> = ({
+  apiBaseUrl,
+}) => {
+  const { locale } = useContext(I18n)
+  return (
+    <article className='mx-1 px-4 sm:mx-12 pt-16 bg-white rounded-t-xl prose prose-xl'>
+      {
+        locale !== LanguagesSupported[1]
+          ? <TemplateEn apiBaseUrl={apiBaseUrl} />
+          : <TemplateZh apiBaseUrl={apiBaseUrl} />
+      }
+    </article>
+  )
+}
+
+export default Doc

+ 38 - 0
app/(commonLayout)/datasets/NewDatasetCard.tsx

@@ -0,0 +1,38 @@
+'use client'
+
+import { forwardRef } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  RiAddLine,
+  RiArrowRightLine,
+} from '@remixicon/react'
+
+const CreateAppCard = forwardRef<HTMLAnchorElement>((_, ref) => {
+  const { t } = useTranslation()
+
+  return (
+    <div className='flex flex-col bg-background-default-dimm border-[0.5px] border-components-panel-border rounded-xl
+      min-h-[160px] transition-all duration-200 ease-in-out'
+    >
+      <a ref={ref} className='group flex flex-grow items-start p-4 cursor-pointer' href='/datasets/create'>
+        <div className='flex items-center gap-3'>
+          <div className='w-10 h-10 p-2 flex items-center justify-center border border-dashed border-divider-regular rounded-lg
+            bg-background-default-lighter group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge'
+          >
+            <RiAddLine className='w-4 h-4 text-text-tertiary group-hover:text-text-accent'/>
+          </div>
+          <div className='system-md-semibold text-text-secondary group-hover:text-text-accent'>{t('dataset.createDataset')}</div>
+        </div>
+      </a>
+      <div className='p-4 pt-0 text-text-tertiary system-xs-regular'>{t('dataset.createDatasetIntro')}</div>
+      <a className='group flex p-4 items-center gap-1 border-t-[0.5px] border-divider-subtle rounded-b-xl cursor-pointer' href='/datasets/connect'>
+        <div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div>
+        <RiArrowRightLine className='w-3.5 h-3.5 text-text-tertiary group-hover:text-text-accent' />
+      </a>
+    </div>
+  )
+})
+
+CreateAppCard.displayName = 'CreateAppCard'
+
+export default CreateAppCard

+ 8 - 0
app/(commonLayout)/datasets/connect/page.tsx

@@ -0,0 +1,8 @@
+import React from 'react'
+import ExternalKnowledgeBaseConnector from '@/app/components/datasets/external-knowledge-base/connector'
+
+const ExternalKnowledgeBaseCreation = () => {
+  return <ExternalKnowledgeBaseConnector />
+}
+
+export default ExternalKnowledgeBaseCreation

+ 12 - 0
app/(commonLayout)/datasets/create/page.tsx

@@ -0,0 +1,12 @@
+import React from 'react'
+import DatasetUpdateForm from '@/app/components/datasets/create'
+
+type Props = {}
+
+const DatasetCreation = async (props: Props) => {
+  return (
+    <DatasetUpdateForm />
+  )
+}
+
+export default DatasetCreation

+ 14 - 0
app/(commonLayout)/datasets/layout.tsx

@@ -0,0 +1,14 @@
+'use client'
+
+import { ExternalApiPanelProvider } from '@/context/external-api-panel-context'
+import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
+
+export default function DatasetsLayout({ children }: { children: React.ReactNode }) {
+  return (
+    <ExternalKnowledgeApiProvider>
+      <ExternalApiPanelProvider>
+        {children}
+      </ExternalApiPanelProvider>
+    </ExternalKnowledgeApiProvider>
+  )
+}

+ 11 - 0
app/(commonLayout)/datasets/page.tsx

@@ -0,0 +1,11 @@
+import Container from './Container'
+
+const AppList = async () => {
+  return <Container />
+}
+
+export const metadata = {
+  title: 'Datasets - Dify',
+}
+
+export default AppList

+ 11 - 0
app/(commonLayout)/datasets/store.ts

@@ -0,0 +1,11 @@
+import { create } from 'zustand'
+
+type DatasetStore = {
+  showExternalApiPanel: boolean
+  setShowExternalApiPanel: (show: boolean) => void
+}
+
+export const useDatasetStore = create<DatasetStore>(set => ({
+  showExternalApiPanel: false,
+  setShowExternalApiPanel: show => set({ showExternalApiPanel: show }),
+}))

+ 1319 - 0
app/(commonLayout)/datasets/template/template.en.mdx

@@ -0,0 +1,1319 @@
+import { CodeGroup } from '@/app/components/develop/code.tsx'
+import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '@/app/components/develop/md.tsx'
+
+# Knowledge API
+
+<div>
+  ### Authentication
+
+  Service API of Dify authenticates using an `API-Key`.
+
+  It is suggested that developers store the `API-Key` in the backend instead of sharing or storing it in the client side to avoid the leakage of the `API-Key`, which may lead to property loss.
+
+  All API requests should include your `API-Key` in the **`Authorization`** HTTP Header, as shown below:
+
+  <CodeGroup title="Code">
+    ```javascript
+      Authorization: Bearer {API_KEY}
+
+    ```
+  </CodeGroup>
+</div>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/document/create_by_text'
+  method='POST'
+  title='Create a document from text'
+  name='#create_by_text'
+/>
+<Row>
+  <Col>
+    This api is based on an existing Knowledge and creates a new document through text based on this Knowledge.
+
+    ### Params
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='name' type='string' key='name'>
+        Document name
+      </Property>
+      <Property name='text' type='string' key='text'>
+        Document content
+      </Property>
+      <Property name='indexing_technique' type='string' key='indexing_technique'>
+        Index mode
+          - <code>high_quality</code> High quality: embedding using embedding model, built as vector database index
+          - <code>economy</code> Economy: Build using inverted index of Keyword Table Index
+      </Property>
+      <Property name='process_rule' type='object' key='process_rule'>
+        Processing rules
+          - <code>mode</code> (string) Cleaning, segmentation mode, automatic / custom
+          - <code>rules</code> (object) Custom rules (in automatic mode, this field is empty)
+            - <code>pre_processing_rules</code> (array[object]) Preprocessing rules
+              - <code>id</code> (string) Unique identifier for the preprocessing rule
+                - enumerate
+                  - <code>remove_extra_spaces</code> Replace consecutive spaces, newlines, tabs
+                  - <code>remove_urls_emails</code> Delete URL, email address
+              - <code>enabled</code> (bool) Whether to select this rule or not. If no document ID is passed in, it represents the default value.
+            - <code>segmentation</code> (object) segmentation rules
+              - <code>separator</code> Custom segment identifier, currently only allows one delimiter to be set. Default is \n
+              - <code>max_tokens</code> Maximum length (token) defaults to 1000
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/document/create_by_text"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "text","text": "text","indexing_technique": "high_quality","process_rule": {"mode": "automatic"}}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_text' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+        "name": "text",
+        "text": "text",
+        "indexing_technique": "high_quality",
+        "process_rule": {
+            "mode": "automatic"
+        }
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "document": {
+        "id": "",
+        "position": 1,
+        "data_source_type": "upload_file",
+        "data_source_info": {
+            "upload_file_id": ""
+        },
+        "dataset_process_rule_id": "",
+        "name": "text.txt",
+        "created_from": "api",
+        "created_by": "",
+        "created_at": 1695690280,
+        "tokens": 0,
+        "indexing_status": "waiting",
+        "error": null,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "archived": false,
+        "display_status": "queuing",
+        "word_count": 0,
+        "hit_count": 0,
+        "doc_form": "text_model"
+      },
+      "batch": ""
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/document/create_by_file'
+  method='POST'
+  title='Create documents from files'
+  name='#create_by_file'
+/>
+<Row>
+  <Col>
+    This api is based on an existing Knowledge and creates a new document through a file based on this Knowledge.
+
+    ### Params
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='data' type='multipart/form-data json string' key='data'>
+        - original_document_id Source document ID (optional)
+          - Used to re-upload the document or modify the document cleaning and segmentation configuration. The missing information is copied from the source document
+          - The source document cannot be an archived document
+          - When original_document_id is passed in, the update operation is performed on behalf of the document. process_rule is a fillable item. If not filled in, the segmentation method of the source document will be used by default
+          - When original_document_id is not passed in, the new operation is performed on behalf of the document, and process_rule is required
+
+        - indexing_technique Index mode
+          - <code>high_quality</code> High quality: embedding using embedding model, built as vector database index
+          - <code>economy</code> Economy: Build using inverted index of Keyword Table Index
+
+        - process_rule Processing rules
+          - <code>mode</code> (string) Cleaning, segmentation mode, automatic / custom
+          - <code>rules</code> (object) Custom rules (in automatic mode, this field is empty)
+            - <code>pre_processing_rules</code> (array[object]) Preprocessing rules
+              - <code>id</code> (string) Unique identifier for the preprocessing rule
+                - enumerate
+                  - <code>remove_extra_spaces</code> Replace consecutive spaces, newlines, tabs
+                  - <code>remove_urls_emails</code> Delete URL, email address
+              - <code>enabled</code> (bool) Whether to select this rule or not. If no document ID is passed in, it represents the default value.
+            - <code>segmentation</code> (object) segmentation rules
+              - <code>separator</code> Custom segment identifier, currently only allows one delimiter to be set. Default is \n
+              - <code>max_tokens</code> Maximum length (token) defaults to 1000
+      </Property>
+      <Property name='file' type='multipart/form-data' key='file'>
+        Files that need to be uploaded.
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/document/create_by_file"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_file' \
+    --header 'Authorization: Bearer {api_key}' \
+    --form 'data="{\"name\":\"Dify\",\"indexing_technique\":\"high_quality\",\"process_rule\":{\"rules\":{\"pre_processing_rules\":[{\"id\":\"remove_extra_spaces\",\"enabled\":true},{\"id\":\"remove_urls_emails\",\"enabled\":true}],\"segmentation\":{\"separator\":\"###\",\"max_tokens\":500}},\"mode\":\"custom\"}}";type=text/plain' \
+    --form 'file=@"/path/to/file"'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "document": {
+        "id": "",
+        "position": 1,
+        "data_source_type": "upload_file",
+        "data_source_info": {
+          "upload_file_id": ""
+        },
+        "dataset_process_rule_id": "",
+        "name": "Dify.txt",
+        "created_from": "api",
+        "created_by": "",
+        "created_at": 1695308667,
+        "tokens": 0,
+        "indexing_status": "waiting",
+        "error": null,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "archived": false,
+        "display_status": "queuing",
+        "word_count": 0,
+        "hit_count": 0,
+        "doc_form": "text_model"
+      },
+      "batch": ""
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets'
+  method='POST'
+  title='Create an empty Knowledge'
+  name='#create_empty_dataset'
+/>
+<Row>
+  <Col>
+    ### Request Body
+    <Properties>
+      <Property name='name' type='string' key='name'>
+        Knowledge name
+      </Property>
+      <Property name='description' type='string' key='description'>
+        Knowledge description (optional)
+      </Property>
+      <Property name='indexing_technique' type='string' key='indexing_technique'>
+        Index Technique (optional)
+          - <code>high_quality</code> high_quality
+          - <code>economy</code> economy
+      </Property>
+      <Property name='permission' type='string' key='permission'>
+        Permission
+          - <code>only_me</code> Only me
+          - <code>all_team_members</code> All team members
+          - <code>partial_members</code> Partial members
+      </Property>
+      <Property name='provider' type='string' key='provider'>
+        Provider (optional, default: vendor)
+          - <code>vendor</code> vendor
+          - <code>external</code> external knowledge
+      </Property>
+      <Property name='external_knowledge_api_id' type='str' key='external_knowledge_api_id'>
+        External Knowledge api id (optional)
+      </Property>
+      <Property name='external_knowledge_id' type='str' key='external_knowledge_id'>
+        External Knowledge id (optional)
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup 
+      title="Request" 
+      tag="POST" 
+      label="/datasets"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name", "permission": "only_me"}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${apiBaseUrl}/v1/datasets' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+      "name": "name",
+      "permission": "only_me"
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "id": "",
+      "name": "name",
+      "description": null,
+      "provider": "vendor",
+      "permission": "only_me",
+      "data_source_type": null,
+      "indexing_technique": null,
+      "app_count": 0,
+      "document_count": 0,
+      "word_count": 0,
+      "created_by": "",
+      "created_at": 1695636173,
+      "updated_by": "",
+      "updated_at": 1695636173,
+      "embedding_model": null,
+      "embedding_model_provider": null,
+      "embedding_available": null
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets'
+  method='GET'
+  title='Knowledge list'
+  name='#dataset_list'
+/>
+<Row>
+  <Col>
+    ### Query
+    <Properties>
+      <Property name='page' type='string' key='page'>
+        Page number
+      </Property>
+      <Property name='limit' type='string' key='limit'>
+        Number of items returned, default 20, range 1-100
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup 
+      title="Request" 
+      tag="POST" 
+      label="/datasets"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets?page=1&limit=20' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request GET '${props.apiBaseUrl}/datasets?page=1&limit=20' \
+    --header 'Authorization: Bearer {api_key}'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [
+        {
+          "id": "",
+          "name": "name",
+          "description": "desc",
+          "permission": "only_me",
+          "data_source_type": "upload_file",
+          "indexing_technique": "",
+          "app_count": 2,
+          "document_count": 10,
+          "word_count": 1200,
+          "created_by": "",
+          "created_at": "",
+          "updated_by": "",
+          "updated_at": ""
+        },
+        ...
+      ],
+      "has_more": true,
+      "limit": 20,
+      "total": 50,
+      "page": 1
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}'
+  method='DELETE'
+  title='Delete knowledge'
+  name='#delete_dataset'
+/>
+<Row>
+  <Col>
+    ### Params
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="DELETE"
+      label="/datasets/{dataset_id}"
+      targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}' \
+    --header 'Authorization: Bearer {api_key}'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```text {{ title: 'Response' }}
+    204 No Content
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/update_by_text'
+  method='POST'
+  title='Update document via text'
+  name='#update_by_text'
+/>
+<Row>
+  <Col>
+    This api is based on an existing Knowledge and updates the document through text based on this Knowledge.
+
+    ### Params
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        Document ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='name' type='string' key='name'>
+        Document name (optional)
+      </Property>
+      <Property name='text' type='string' key='text'>
+        Document content (optional)
+      </Property>
+      <Property name='process_rule' type='object' key='process_rule'>
+        Processing rules
+          - <code>mode</code> (string) Cleaning, segmentation mode, automatic / custom
+          - <code>rules</code> (object) Custom rules (in automatic mode, this field is empty)
+            - <code>pre_processing_rules</code> (array[object]) Preprocessing rules
+              - <code>id</code> (string) Unique identifier for the preprocessing rule
+                - enumerate
+                  - <code>remove_extra_spaces</code> Replace consecutive spaces, newlines, tabs
+                  - <code>remove_urls_emails</code> Delete URL, email address
+              - <code>enabled</code> (bool) Whether to select this rule or not. If no document ID is passed in, it represents the default value.
+            - <code>segmentation</code> (object) segmentation rules
+              - <code>separator</code> Custom segment identifier, currently only allows one delimiter to be set. Default is \n
+              - <code>max_tokens</code> Maximum length (token) defaults to 1000
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/documents/{document_id}/update_by_text"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name","text": "text"}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_text' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+        "name": "name",
+        "text": "text"
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "document": {
+        "id": "",
+        "position": 1,
+        "data_source_type": "upload_file",
+        "data_source_info": {
+          "upload_file_id": ""
+        },
+        "dataset_process_rule_id": "",
+        "name": "name.txt",
+        "created_from": "api",
+        "created_by": "",
+        "created_at": 1695308667,
+        "tokens": 0,
+        "indexing_status": "waiting",
+        "error": null,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "archived": false,
+        "display_status": "queuing",
+        "word_count": 0,
+        "hit_count": 0,
+        "doc_form": "text_model"
+      },
+      "batch": ""
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/update_by_file'
+  method='POST'
+  title='Update a document from a file'
+  name='#update_by_file'
+/>
+<Row>
+  <Col>
+    This api is based on an existing Knowledge, and updates documents through files based on this Knowledge
+
+    ### Params
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        Document ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='name' type='string' key='name'>
+        Document name (optional)
+      </Property>
+      <Property name='file' type='multipart/form-data' key='file'>
+        Files to be uploaded
+      </Property>
+      <Property name='process_rule' type='object' key='process_rule'>
+        Processing rules
+          - <code>mode</code> (string) Cleaning, segmentation mode, automatic / custom
+          - <code>rules</code> (object) Custom rules (in automatic mode, this field is empty)
+            - <code>pre_processing_rules</code> (array[object]) Preprocessing rules
+              - <code>id</code> (string) Unique identifier for the preprocessing rule
+                - enumerate
+                  - <code>remove_extra_spaces</code> Replace consecutive spaces, newlines, tabs
+                  - <code>remove_urls_emails</code> Delete URL, email address
+              - <code>enabled</code> (bool) Whether to select this rule or not. If no document ID is passed in, it represents the default value.
+            - <code>segmentation</code> (object) segmentation rules
+              - <code>separator</code> Custom segment identifier, currently only allows one delimiter to be set. Default is \n
+              - <code>max_tokens</code> Maximum length (token) defaults to 1000
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/documents/{document_id}/update_by_file"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"name":"Dify","indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_file' \
+    --header 'Authorization: Bearer {api_key}' \
+    --form 'data="{\"name\":\"Dify\",\"indexing_technique\":\"high_quality\",\"process_rule\":{\"rules\":{\"pre_processing_rules\":[{\"id\":\"remove_extra_spaces\",\"enabled\":true},{\"id\":\"remove_urls_emails\",\"enabled\":true}],\"segmentation\":{\"separator\":\"###\",\"max_tokens\":500}},\"mode\":\"custom\"}}";type=text/plain' \
+    --form 'file=@"/path/to/file"'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "document": {
+        "id": "",
+        "position": 1,
+        "data_source_type": "upload_file",
+        "data_source_info": {
+          "upload_file_id": ""
+        },
+        "dataset_process_rule_id": "",
+        "name": "Dify.txt",
+        "created_from": "api",
+        "created_by": "",
+        "created_at": 1695308667,
+        "tokens": 0,
+        "indexing_status": "waiting",
+        "error": null,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "archived": false,
+        "display_status": "queuing",
+        "word_count": 0,
+        "hit_count": 0,
+        "doc_form": "text_model"
+      },
+      "batch": "20230921150427533684"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{batch}/indexing-status'
+  method='GET'
+  title='Get document embedding status (progress)'
+  name='#indexing_status'
+/>
+<Row>
+  <Col>
+    ### Params
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+      <Property name='batch' type='string' key='batch'>
+        Batch number of uploaded documents
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="GET"
+      label="/datasets/{dataset_id}/documents/{batch}/indexing-status"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{batch}/indexing-status' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{batch}/indexing-status' \
+    --header 'Authorization: Bearer {api_key}' \
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data":[{
+        "id": "",
+        "indexing_status": "indexing",
+        "processing_started_at": 1681623462.0,
+        "parsing_completed_at": 1681623462.0,
+        "cleaning_completed_at": 1681623462.0,
+        "splitting_completed_at": 1681623462.0,
+        "completed_at": null,
+        "paused_at": null,
+        "error": null,
+        "stopped_at": null,
+        "completed_segments": 24,
+        "total_segments": 100
+      }]
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}'
+  method='DELETE'
+  title='Delete document'
+  name='#delete_document'
+/>
+<Row>
+  <Col>
+    ### Params
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        Document ID
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="DELETE"
+      label="/datasets/{dataset_id}/documents/{document_id}"
+      targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \
+    --header 'Authorization: Bearer {api_key}' \
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "result": "success"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents'
+  method='GET'
+  title='Knowledge document list'
+  name='#dataset_document_list'
+/>
+<Row>
+  <Col>
+    ### Params
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+    </Properties>
+
+    ### Query
+    <Properties>
+      <Property name='keyword' type='string' key='keyword'>
+        Search keywords, currently only search document names(optional)
+      </Property>
+      <Property name='page' type='string' key='page'>
+        Page number(optional)
+      </Property>
+      <Property name='limit' type='string' key='limit'>
+        Number of items returned, default 20, range 1-100(optional)
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="GET"
+      label="/datasets/{dataset_id}/documents"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents' \
+    --header 'Authorization: Bearer {api_key}' \
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [
+        {
+          "id": "",
+          "position": 1,
+          "data_source_type": "file_upload",
+          "data_source_info": null,
+          "dataset_process_rule_id": null,
+          "name": "dify",
+          "created_from": "",
+          "created_by": "",
+          "created_at": 1681623639,
+          "tokens": 0,
+          "indexing_status": "waiting",
+          "error": null,
+          "enabled": true,
+          "disabled_at": null,
+          "disabled_by": null,
+          "archived": false
+        },
+      ],
+      "has_more": false,
+      "limit": 20,
+      "total": 9,
+      "page": 1
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/segments'
+  method='POST'
+  title='Add segment'
+  name='#create_new_segment'
+/>
+<Row>
+  <Col>
+    ### Params
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        Document ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='segments' type='object list' key='segments'>
+        - <code>content</code> (text) Text content/question content, required
+        - <code>answer</code> (text) Answer content, if the mode of the Knowledge is qa mode, pass the value(optional)
+        - <code>keywords</code> (list) Keywords(optional)
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/documents/{document_id}/segments"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"segments": [{"content": "1","answer": "1","keywords": ["a"]}]}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+      "segments": [
+        {
+          "content": "1",
+          "answer": "1",
+          "keywords": ["a"]
+        }
+      ]
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [{
+        "id": "",
+        "position": 1,
+        "document_id": "",
+        "content": "1",
+        "answer": "1",
+        "word_count": 25,
+        "tokens": 0,
+        "keywords": [
+          "a"
+        ],
+        "index_node_id": "",
+        "index_node_hash": "",
+        "hit_count": 0,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "status": "completed",
+        "created_by": "",
+        "created_at": 1695312007,
+        "indexing_at": 1695312007,
+        "completed_at": 1695312007,
+        "error": null,
+        "stopped_at": null
+      }],
+      "doc_form": "text_model"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/segments'
+  method='GET'
+  title='get documents segments'
+  name='#get_segment'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        Document ID
+      </Property>
+    </Properties>
+
+     ### Query
+    <Properties>
+      <Property name='keyword' type='string' key='keyword'>
+        keyword,choosable
+      </Property>
+      <Property name='status' type='string' key='status'>
+        Search status,completed
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="GET"
+      label="/datasets/{dataset_id}/documents/{document_id}/segments"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [{
+        "id": "",
+        "position": 1,
+        "document_id": "",
+        "content": "1",
+        "answer": "1",
+        "word_count": 25,
+        "tokens": 0,
+        "keywords": [
+            "a"
+        ],
+        "index_node_id": "",
+        "index_node_hash": "",
+        "hit_count": 0,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "status": "completed",
+        "created_by": "",
+        "created_at": 1695312007,
+        "indexing_at": 1695312007,
+        "completed_at": 1695312007,
+        "error": null,
+        "stopped_at": null
+      }],
+      "doc_form": "text_model"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
+  method='DELETE'
+  title='delete document segment'
+  name='#delete_segment'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        Document ID
+      </Property>
+      <Property name='segment_id' type='string' key='segment_id'>
+        Document Segment ID
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="DELETE"
+      label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}"
+      targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/segments/{segment_id}' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "result": "success"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
+  method='POST'
+  title='update document segment'
+  name='#update_segment'
+/>
+<Row>
+  <Col>
+    ### POST
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Knowledge ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        Document ID
+      </Property>
+      <Property name='segment_id' type='string' key='segment_id'>
+        Document Segment ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='segment' type='object' key='segment'>
+        - <code>content</code> (text) text content/question content,required
+        - <code>answer</code> (text) Answer content, not required, passed if the Knowledge is in qa mode
+        - <code>keywords</code> (list) keyword, not required
+        - <code>enabled</code> (bool) false/true, not required
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'\\\n--data-raw '{\"segment\": {\"content\": \"1\",\"answer\": \"1\", \"keywords\": [\"a\"], \"enabled\": false}}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+      "segment": {
+          "content": "1",
+          "answer": "1",
+          "keywords": ["a"],
+          "enabled": false
+      }
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [{
+        "id": "",
+        "position": 1,
+        "document_id": "",
+        "content": "1",
+        "answer": "1",
+        "word_count": 25,
+        "tokens": 0,
+        "keywords": [
+            "a"
+        ],
+        "index_node_id": "",
+        "index_node_hash": "",
+        "hit_count": 0,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "status": "completed",
+        "created_by": "",
+        "created_at": 1695312007,
+        "indexing_at": 1695312007,
+        "completed_at": 1695312007,
+        "error": null,
+        "stopped_at": null
+      }],
+      "doc_form": "text_model"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/hit_testing'
+  method='POST'
+  title='Dataset hit testing'
+  name='#dataset_hit_testing'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        Dataset ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='query' type='string' key='query'>
+        retrieval keywordc
+      </Property>
+      <Property name='retrieval_model' type='object' key='retrieval_model'>
+        retrieval keyword(Optional, if not filled, it will be recalled according to the default method)
+        - <code>search_method</code> (text) Search method: One of the following four keywords is required
+          - <code>keyword_search</code> Keyword search
+          - <code>semantic_search</code> Semantic search
+          - <code>full_text_search</code> Full-text search
+          - <code>hybrid_search</code> Hybrid search
+        - <code>reranking_enable</code> (bool) Whether to enable reranking, optional, required if the search mode is semantic_search or hybrid_search
+        - <code>reranking_mode</code> (object) Rerank model configuration, optional, required if reranking is enabled
+            - <code>reranking_provider_name</code> (string) Rerank model provider
+            - <code>reranking_model_name</code> (string) Rerank model name
+        - <code>weights</code> (double) Semantic search weight setting in hybrid search mode
+        - <code>top_k</code> (integer) Number of results to return, optional
+        - <code>score_threshold_enabled</code> (bool) Whether to enable score threshold
+        - <code>score_threshold</code> (double) Score threshold
+      </Property>
+      <Property name='external_retrieval_model' type='object' key='external_retrieval_model'>
+          Unused field
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/hit_testing"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/hit_testing' \\\n--header 'Authorization: Bearer {api_key}'\\\n--header 'Content-Type: application/json'\\\n--data-raw '{
+        "query": "test",
+        "retrieval_model": {
+            "search_method": "keyword_search",
+            "reranking_enable": false,
+            "reranking_mode": null,
+            "reranking_model": {
+                "reranking_provider_name": "",
+                "reranking_model_name": ""
+            },
+            "weights": null,
+            "top_k": 1,
+            "score_threshold_enabled": false,
+            "score_threshold": null
+        }
+    }'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/hit_testing' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+        "query": "test",
+        "retrieval_model": {
+            "search_method": "keyword_search",
+            "reranking_enable": false,
+            "reranking_mode": null,
+            "reranking_model": {
+                "reranking_provider_name": "",
+                "reranking_model_name": ""
+            },
+            "weights": null,
+            "top_k": 2,
+            "score_threshold_enabled": false,
+            "score_threshold": null
+        }
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "query": {
+        "content": "test"
+      },
+      "records": [
+        {
+          "segment": {
+            "id": "7fa6f24f-8679-48b3-bc9d-bdf28d73f218",
+            "position": 1,
+            "document_id": "a8c6c36f-9f5d-4d7a-8472-f5d7b75d71d2",
+            "content": "Operation guide",
+            "answer": null,
+            "word_count": 847,
+            "tokens": 280,
+            "keywords": [
+              "install",
+              "java",
+              "base",
+              "scripts",
+              "jdk",
+              "manual",
+              "internal",
+              "opens",
+              "add",
+              "vmoptions"
+            ],
+            "index_node_id": "39dd8443-d960-45a8-bb46-7275ad7fbc8e",
+            "index_node_hash": "0189157697b3c6a418ccf8264a09699f25858975578f3467c76d6bfc94df1d73",
+            "hit_count": 0,
+            "enabled": true,
+            "disabled_at": null,
+            "disabled_by": null,
+            "status": "completed",
+            "created_by": "dbcb1ab5-90c8-41a7-8b78-73b235eb6f6f",
+            "created_at": 1728734540,
+            "indexing_at": 1728734552,
+            "completed_at": 1728734584,
+            "error": null,
+            "stopped_at": null,
+            "document": {
+              "id": "a8c6c36f-9f5d-4d7a-8472-f5d7b75d71d2",
+              "data_source_type": "upload_file",
+              "name": "readme.txt",
+              "doc_type": null
+            }
+          },
+          "score": 3.730463140527718e-05,
+          "tsne_position": null
+        }
+      ]
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Row>
+  <Col>
+    ### Error message
+    <Properties>
+      <Property name='code' type='string' key='code'>
+        Error code
+      </Property>
+    </Properties>
+    <Properties>
+      <Property name='status' type='number' key='status'>
+        Error status
+      </Property>
+    </Properties>
+    <Properties>
+      <Property name='message' type='string' key='message'>
+        Error message
+      </Property>
+    </Properties>
+  </Col>
+  <Col>
+    <CodeGroup title="Example">
+    ```json {{ title: 'Response' }}
+      {
+        "code": "no_file_uploaded",
+        "message": "Please upload your file.",
+        "status": 400
+      }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+<table className="max-w-auto border-collapse border border-slate-400" style={{ maxWidth: 'none', width: 'auto' }}>
+  <thead style={{ background: '#f9fafc' }}>
+    <tr>
+      <th className="p-2 border border-slate-300">code</th>
+      <th className="p-2 border border-slate-300">status</th>
+      <th className="p-2 border border-slate-300">message</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td className="p-2 border border-slate-300">no_file_uploaded</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">Please upload your file.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">too_many_files</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">Only one file is allowed.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">file_too_large</td>
+      <td className="p-2 border border-slate-300">413</td>
+      <td className="p-2 border border-slate-300">File size exceeded.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">unsupported_file_type</td>
+      <td className="p-2 border border-slate-300">415</td>
+      <td className="p-2 border border-slate-300">File type not allowed.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">high_quality_dataset_only</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">Current operation only supports 'high-quality' datasets.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">dataset_not_initialized</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">The dataset is still being initialized or indexing. Please wait a moment.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">archived_document_immutable</td>
+      <td className="p-2 border border-slate-300">403</td>
+      <td className="p-2 border border-slate-300">The archived document is not editable.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">dataset_name_duplicate</td>
+      <td className="p-2 border border-slate-300">409</td>
+      <td className="p-2 border border-slate-300">The dataset name already exists. Please modify your dataset name.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">invalid_action</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">Invalid action.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">document_already_finished</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">The document has been processed. Please refresh the page or go to the document details.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">document_indexing</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">The document is being processed and cannot be edited.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">invalid_metadata</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">The metadata content is incorrect. Please check and verify.</td>
+    </tr>
+  </tbody>
+</table>
+<div className="pb-4" />

+ 1321 - 0
app/(commonLayout)/datasets/template/template.zh.mdx

@@ -0,0 +1,1321 @@
+import { CodeGroup } from '@/app/components/develop/code.tsx'
+import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '@/app/components/develop/md.tsx'
+
+# 知识库 API
+
+<div>
+  ### 鉴权
+
+  Dify Service API 使用 `API-Key` 进行鉴权。
+
+  建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。
+
+  所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:
+
+  <CodeGroup title="Code">
+    ```javascript
+      Authorization: Bearer {API_KEY}
+
+    ```
+  </CodeGroup>
+</div>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/document/create_by_text'
+  method='POST'
+  title='通过文本创建文档'
+  name='#create_by_text'
+/>
+<Row>
+  <Col>
+    此接口基于已存在知识库,在此知识库的基础上通过文本创建新的文档
+
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='name' type='string' key='name'>
+        文档名称
+      </Property>
+      <Property name='text' type='string' key='text'>
+        文档内容
+      </Property>
+      <Property name='indexing_technique' type='string' key='indexing_technique'>
+        索引方式
+          - <code>high_quality</code> 高质量:使用  embedding 模型进行嵌入,构建为向量数据库索引
+          - <code>economy</code> 经济:使用 Keyword Table Index 的倒排索引进行构建
+      </Property>
+      <Property name='process_rule' type='object' key='process_rule'>
+        处理规则
+          - <code>mode</code> (string) 清洗、分段模式 ,automatic 自动 / custom 自定义
+          - <code>rules</code> (object) 自定义规则(自动模式下,该字段为空)
+            - <code>pre_processing_rules</code> (array[object]) 预处理规则
+              - <code>id</code> (string) 预处理规则的唯一标识符
+                - 枚举:
+                  - <code>remove_extra_spaces</code> 替换连续空格、换行符、制表符
+                  - <code>remove_urls_emails</code> 删除 URL、电子邮件地址
+              - <code>enabled</code> (bool) 是否选中该规则,不传入文档 ID 时代表默认值
+            - <code>segmentation</code> (object) 分段规则
+              - <code>separator</code> 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
+              - <code>max_tokens</code> 最大长度 (token) 默认为 1000
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/document/create_by_text"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "text","text": "text","indexing_technique": "high_quality","process_rule": {"mode": "automatic"}}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_text' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+        "name": "text",
+        "text": "text",
+        "indexing_technique": "high_quality",
+        "process_rule": {
+            "mode": "automatic"
+        }
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "document": {
+        "id": "",
+        "position": 1,
+        "data_source_type": "upload_file",
+        "data_source_info": {
+            "upload_file_id": ""
+        },
+        "dataset_process_rule_id": "",
+        "name": "text.txt",
+        "created_from": "api",
+        "created_by": "",
+        "created_at": 1695690280,
+        "tokens": 0,
+        "indexing_status": "waiting",
+        "error": null,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "archived": false,
+        "display_status": "queuing",
+        "word_count": 0,
+        "hit_count": 0,
+        "doc_form": "text_model"
+      },
+      "batch": ""
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/document/create_by_file'
+  method='POST'
+  title='通过文件创建文档 '
+  name='#create_by_file'
+/>
+<Row>
+  <Col>
+    此接口基于已存在知识库,在此知识库的基础上通过文件创建新的文档
+
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='data' type='multipart/form-data json string' key='data'>
+        - original_document_id 源文档 ID (选填)
+          - 用于重新上传文档或修改文档清洗、分段配置,缺失的信息从源文档复制
+          - 源文档不可为归档的文档
+          - 当传入 <code>original_document_id</code> 时,代表文档进行更新操作,<code>process_rule</code> 为可填项目,不填默认使用源文档的分段方式
+          - 未传入 <code>original_document_id</code> 时,代表文档进行新增操作,<code>process_rule</code> 为必填
+
+        - indexing_technique 索引方式
+          - <code>high_quality</code> 高质量:使用  embedding 模型进行嵌入,构建为向量数据库索引
+          - <code>economy</code> 经济:使用 Keyword Table Index 的倒排索引进行构建
+
+        - process_rule 处理规则
+          - <code>mode</code> (string) 清洗、分段模式 ,automatic 自动 / custom 自定义
+          - <code>rules</code> (object) 自定义规则(自动模式下,该字段为空)
+            - <code>pre_processing_rules</code> (array[object]) 预处理规则
+              - <code>id</code> (string) 预处理规则的唯一标识符
+                - 枚举:
+                  - <code>remove_extra_spaces</code> 替换连续空格、换行符、制表符
+                  - <code>remove_urls_emails</code> 删除 URL、电子邮件地址
+              - <code>enabled</code> (bool) 是否选中该规则,不传入文档 ID 时代表默认值
+            - <code>segmentation</code> (object) 分段规则
+              - <code>separator</code> 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
+              - <code>max_tokens</code> 最大长度 (token) 默认为 1000
+      </Property>
+      <Property name='file' type='multipart/form-data' key='file'>
+        需要上传的文件。
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/document/create_by_file"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_file' \
+    --header 'Authorization: Bearer {api_key}' \
+    --form 'data="{\"name\":\"Dify\",\"indexing_technique\":\"high_quality\",\"process_rule\":{\"rules\":{\"pre_processing_rules\":[{\"id\":\"remove_extra_spaces\",\"enabled\":true},{\"id\":\"remove_urls_emails\",\"enabled\":true}],\"segmentation\":{\"separator\":\"###\",\"max_tokens\":500}},\"mode\":\"custom\"}}";type=text/plain' \
+    --form 'file=@"/path/to/file"'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "document": {
+        "id": "",
+        "position": 1,
+        "data_source_type": "upload_file",
+        "data_source_info": {
+          "upload_file_id": ""
+        },
+        "dataset_process_rule_id": "",
+        "name": "Dify.txt",
+        "created_from": "api",
+        "created_by": "",
+        "created_at": 1695308667,
+        "tokens": 0,
+        "indexing_status": "waiting",
+        "error": null,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "archived": false,
+        "display_status": "queuing",
+        "word_count": 0,
+        "hit_count": 0,
+        "doc_form": "text_model"
+      },
+      "batch": ""
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets'
+  method='POST'
+  title='创建空知识库'
+  name='#create_empty_dataset'
+/>
+<Row>
+  <Col>
+    ### Request Body
+    <Properties>
+      <Property name='name' type='string' key='name'>
+        知识库名称(必填)
+      </Property>
+      <Property name='description' type='string' key='description'>
+        知识库描述(选填)
+      </Property>
+      <Property name='indexing_technique' type='string' key='indexing_technique'>
+        索引模式(选填,建议填写)
+          - <code>high_quality</code> 高质量
+          - <code>economy</code> 经济
+      </Property>
+      <Property name='permission' type='string' key='permission'>
+        权限(选填,默认only_me)
+          - <code>only_me</code> 仅自己
+          - <code>all_team_members</code> 所有团队成员
+          - <code>partial_members</code> 部分团队成员
+      </Property>
+      <Property name='provider' type='string' key='provider'>
+        provider,(选填,默认 vendor)
+          - <code>vendor</code> 上传文件
+          - <code>external</code> 外部知识库
+      </Property>
+      <Property name='external_knowledge_api_id' type='str' key='external_knowledge_api_id'>
+        外部知识库 API_ID(选填)
+      </Property>
+      <Property name='external_knowledge_id' type='str' key='external_knowledge_id'>
+        外部知识库 ID(选填)
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup 
+      title="Request" 
+      tag="POST" 
+      label="/datasets"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name", "permission": "only_me"}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+      "name": "name",
+      "permission": "only_me"
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "id": "",
+      "name": "name",
+      "description": null,
+      "provider": "vendor",
+      "permission": "only_me",
+      "data_source_type": null,
+      "indexing_technique": null,
+      "app_count": 0,
+      "document_count": 0,
+      "word_count": 0,
+      "created_by": "",
+      "created_at": 1695636173,
+      "updated_by": "",
+      "updated_at": 1695636173,
+      "embedding_model": null,
+      "embedding_model_provider": null,
+      "embedding_available": null
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets'
+  method='GET'
+  title='知识库列表'
+  name='#dataset_list'
+/>
+<Row>
+  <Col>
+    ### Query
+    <Properties>
+      <Property name='page' type='string' key='page'>
+        页码
+      </Property>
+      <Property name='limit' type='string' key='limit'>
+        返回条数,默认 20,范围 1-100
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets?page=1&limit=20' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request GET '${props.apiBaseUrl}/datasets?page=1&limit=20' \
+    --header 'Authorization: Bearer {api_key}'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [
+        {
+          "id": "",
+          "name": "知识库名称",
+          "description": "描述信息",
+          "permission": "only_me",
+          "data_source_type": "upload_file",
+          "indexing_technique": "",
+          "app_count": 2,
+          "document_count": 10,
+          "word_count": 1200,
+          "created_by": "",
+          "created_at": "",
+          "updated_by": "",
+          "updated_at": ""
+        },
+        ...
+      ],
+      "has_more": true,
+      "limit": 20,
+      "total": 50,
+      "page": 1
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}'
+  method='DELETE'
+  title='删除知识库'
+  name='#delete_dataset'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="DELETE"
+      label="/datasets/{dataset_id}"
+      targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}' \
+    --header 'Authorization: Bearer {api_key}'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```text {{ title: 'Response' }}
+    204 No Content
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/update_by_text'
+  method='POST'
+  title='通过文本更新文档 '
+  name='#update_by_text'
+/>
+<Row>
+  <Col>
+    此接口基于已存在知识库,在此知识库的基础上通过文本更新文档
+
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        文档 ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='name' type='string' key='name'>
+        文档名称 (选填)
+      </Property>
+      <Property name='text' type='string' key='text'>
+        文档内容(选填)
+      </Property>
+      <Property name='process_rule' type='object' key='process_rule'>
+        处理规则(选填)
+          - <code>mode</code> (string) 清洗、分段模式 ,automatic 自动 / custom 自定义
+          - <code>rules</code> (object) 自定义规则(自动模式下,该字段为空)
+            - <code>pre_processing_rules</code> (array[object]) 预处理规则
+              - <code>id</code> (string) 预处理规则的唯一标识符
+                - 枚举:
+                  - <code>remove_extra_spaces</code> 替换连续空格、换行符、制表符
+                  - <code>remove_urls_emails</code> 删除 URL、电子邮件地址
+              - <code>enabled</code> (bool) 是否选中该规则,不传入文档 ID 时代表默认值
+            - <code>segmentation</code> (object) 分段规则
+              - <code>separator</code> 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
+              - <code>max_tokens</code> 最大长度 (token) 默认为 1000
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/documents/{document_id}/update_by_text"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name","text": "text"}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_text' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+        "name": "name",
+        "text": "text"
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "document": {
+        "id": "",
+        "position": 1,
+        "data_source_type": "upload_file",
+        "data_source_info": {
+          "upload_file_id": ""
+        },
+        "dataset_process_rule_id": "",
+        "name": "name.txt",
+        "created_from": "api",
+        "created_by": "",
+        "created_at": 1695308667,
+        "tokens": 0,
+        "indexing_status": "waiting",
+        "error": null,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "archived": false,
+        "display_status": "queuing",
+        "word_count": 0,
+        "hit_count": 0,
+        "doc_form": "text_model"
+      },
+      "batch": ""
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/update_by_file'
+  method='POST'
+  title='通过文件更新文档  '
+  name='#update_by_file'
+/>
+<Row>
+  <Col>
+    此接口基于已存在知识库,在此知识库的基础上通过文件更新文档的操作。
+
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        文档 ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='name' type='string' key='name'>
+        文档名称 (选填)
+      </Property>
+      <Property name='file' type='multipart/form-data' key='file'>
+        需要上传的文件
+      </Property>
+      <Property name='process_rule' type='object' key='process_rule'>
+        处理规则(选填)
+          - <code>mode</code> (string) 清洗、分段模式 ,automatic 自动 / custom 自定义
+          - <code>rules</code> (object) 自定义规则(自动模式下,该字段为空)
+            - <code>pre_processing_rules</code> (array[object]) 预处理规则
+              - <code>id</code> (string) 预处理规则的唯一标识符
+                - 枚举:
+                  - <code>remove_extra_spaces</code> 替换连续空格、换行符、制表符
+                  - <code>remove_urls_emails</code> 删除 URL、电子邮件地址
+              - <code>enabled</code> (bool) 是否选中该规则,不传入文档 ID 时代表默认值
+            - <code>segmentation</code> (object) 分段规则
+              - <code>separator</code> 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
+              - <code>max_tokens</code> 最大长度 (token) 默认为 1000
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/documents/{document_id}/update_by_file"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"name":"Dify","indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_file' \
+    --header 'Authorization: Bearer {api_key}' \
+    --form 'data="{\"name\":\"Dify\",\"indexing_technique\":\"high_quality\",\"process_rule\":{\"rules\":{\"pre_processing_rules\":[{\"id\":\"remove_extra_spaces\",\"enabled\":true},{\"id\":\"remove_urls_emails\",\"enabled\":true}],\"segmentation\":{\"separator\":\"###\",\"max_tokens\":500}},\"mode\":\"custom\"}}";type=text/plain' \
+    --form 'file=@"/path/to/file"'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "document": {
+        "id": "",
+        "position": 1,
+        "data_source_type": "upload_file",
+        "data_source_info": {
+          "upload_file_id": ""
+        },
+        "dataset_process_rule_id": "",
+        "name": "Dify.txt",
+        "created_from": "api",
+        "created_by": "",
+        "created_at": 1695308667,
+        "tokens": 0,
+        "indexing_status": "waiting",
+        "error": null,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "archived": false,
+        "display_status": "queuing",
+        "word_count": 0,
+        "hit_count": 0,
+        "doc_form": "text_model"
+      },
+      "batch": "20230921150427533684"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{batch}/indexing-status'
+  method='GET'
+  title='获取文档嵌入状态(进度)'
+  name='#indexing_status'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+      <Property name='batch' type='string' key='batch'>
+        上传文档的批次号
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="GET"
+      label="/datasets/{dataset_id}/documents/{batch}/indexing-status"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{batch}/indexing-status' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{batch}/indexing-status' \
+    --header 'Authorization: Bearer {api_key}' \
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data":[{
+        "id": "",
+        "indexing_status": "indexing",
+        "processing_started_at": 1681623462.0,
+        "parsing_completed_at": 1681623462.0,
+        "cleaning_completed_at": 1681623462.0,
+        "splitting_completed_at": 1681623462.0,
+        "completed_at": null,
+        "paused_at": null,
+        "error": null,
+        "stopped_at": null,
+        "completed_segments": 24,
+        "total_segments": 100
+      }]
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}'
+  method='DELETE'
+  title='删除文档'
+  name='#delete_document'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        文档 ID
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="DELETE"
+      label="/datasets/{dataset_id}/documents/{document_id}"
+      targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \
+    --header 'Authorization: Bearer {api_key}' \
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "result": "success"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents'
+  method='GET'
+  title='知识库文档列表'
+  name='#dataset_document_list'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+    </Properties>
+
+    ### Query
+    <Properties>
+      <Property name='keyword' type='string' key='keyword'>
+        搜索关键词,可选,目前仅搜索文档名称
+      </Property>
+      <Property name='page' type='string' key='page'>
+        页码,可选
+      </Property>
+      <Property name='limit' type='string' key='limit'>
+        返回条数,可选,默认 20,范围 1-100
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="GET"
+      label="/datasets/{dataset_id}/documents"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents' \\\n--header 'Authorization: Bearer {api_key}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents' \
+    --header 'Authorization: Bearer {api_key}' \
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [
+        {
+          "id": "",
+          "position": 1,
+          "data_source_type": "file_upload",
+          "data_source_info": null,
+          "dataset_process_rule_id": null,
+          "name": "dify",
+          "created_from": "",
+          "created_by": "",
+          "created_at": 1681623639,
+          "tokens": 0,
+          "indexing_status": "waiting",
+          "error": null,
+          "enabled": true,
+          "disabled_at": null,
+          "disabled_by": null,
+          "archived": false
+        },
+      ],
+      "has_more": false,
+      "limit": 20,
+      "total": 9,
+      "page": 1
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/segments'
+  method='POST'
+  title='新增分段'
+  name='#create_new_segment'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        文档 ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='segments' type='object list' key='segments'>
+        - <code>content</code> (text) 文本内容/问题内容,必填
+        - <code>answer</code> (text) 答案内容,非必填,如果知识库的模式为qa模式则传值
+        - <code>keywords</code> (list) 关键字,非必填
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/documents/{document_id}/segments"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"segments": [{"content": "1","answer": "1","keywords": ["a"]}]}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+      "segments": [
+        {
+          "content": "1",
+          "answer": "1",
+          "keywords": ["a"]
+        }
+      ]
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [{
+        "id": "",
+        "position": 1,
+        "document_id": "",
+        "content": "1",
+        "answer": "1",
+        "word_count": 25,
+        "tokens": 0,
+        "keywords": [
+            "a"
+        ],
+        "index_node_id": "",
+        "index_node_hash": "",
+        "hit_count": 0,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "status": "completed",
+        "created_by": "",
+        "created_at": 1695312007,
+        "indexing_at": 1695312007,
+        "completed_at": 1695312007,
+        "error": null,
+        "stopped_at": null
+      }],
+      "doc_form": "text_model"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/segments'
+  method='GET'
+  title='查询文档分段'
+  name='#get_segment'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        文档 ID
+      </Property>
+    </Properties>
+
+     ### Query
+    <Properties>
+      <Property name='keyword' type='string' key='keyword'>
+        搜索关键词,可选
+      </Property>
+      <Property name='status' type='string' key='status'>
+        搜索状态,completed
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="GET"
+      label="/datasets/{dataset_id}/documents/{document_id}/segments"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [{
+        "id": "",
+        "position": 1,
+        "document_id": "",
+        "content": "1",
+        "answer": "1",
+        "word_count": 25,
+        "tokens": 0,
+        "keywords": [
+            "a"
+        ],
+        "index_node_id": "",
+        "index_node_hash": "",
+        "hit_count": 0,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "status": "completed",
+        "created_by": "",
+        "created_at": 1695312007,
+        "indexing_at": 1695312007,
+        "completed_at": 1695312007,
+        "error": null,
+        "stopped_at": null
+      }],
+      "doc_form": "text_model"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
+  method='DELETE'
+  title='删除文档分段'
+  name='#delete_segment'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        文档 ID
+      </Property>
+      <Property name='segment_id' type='string' key='segment_id'>
+        文档分段ID
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="DELETE"
+      label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}"
+      targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "result": "success"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
+  method='POST'
+  title='更新文档分段'
+  name='#update_segment'
+/>
+<Row>
+  <Col>
+    ### POST
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+      <Property name='document_id' type='string' key='document_id'>
+        文档 ID
+      </Property>
+      <Property name='segment_id' type='string' key='segment_id'>
+        文档分段ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='segment' type='object' key='segment'>
+        - <code>content</code> (text) 文本内容/问题内容,必填
+        - <code>answer</code> (text) 答案内容,非必填,如果知识库的模式为qa模式则传值
+        - <code>keywords</code> (list) 关键字,非必填
+        - <code>enabled</code> (bool) false/true,非必填
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}"
+      targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'\\\n--data-raw '{\"segment\": {\"content\": \"1\",\"answer\": \"1\", \"keywords\": [\"a\"], \"enabled\": false}}'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+      "segment": {
+          "content": "1",
+          "answer": "1",
+          "keywords": ["a"],
+          "enabled": false
+      }
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "data": [{
+        "id": "",
+        "position": 1,
+        "document_id": "",
+        "content": "1",
+        "answer": "1",
+        "word_count": 25,
+        "tokens": 0,
+        "keywords": [
+            "a"
+        ],
+        "index_node_id": "",
+        "index_node_hash": "",
+        "hit_count": 0,
+        "enabled": true,
+        "disabled_at": null,
+        "disabled_by": null,
+        "status": "completed",
+        "created_by": "",
+        "created_at": 1695312007,
+        "indexing_at": 1695312007,
+        "completed_at": 1695312007,
+        "error": null,
+        "stopped_at": null
+      }],
+      "doc_form": "text_model"
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+---
+
+<Heading
+  url='/datasets/{dataset_id}/hit_testing'
+  method='POST'
+  title='知识库召回测试'
+  name='#dataset_hit_testing'
+/>
+<Row>
+  <Col>
+    ### Path
+    <Properties>
+      <Property name='dataset_id' type='string' key='dataset_id'>
+        知识库 ID
+      </Property>
+    </Properties>
+
+    ### Request Body
+    <Properties>
+      <Property name='query' type='string' key='query'>
+        召回关键词
+      </Property>
+      <Property name='retrieval_model' type='object' key='retrieval_model'>
+        召回参数(选填,如不填,按照默认方式召回)
+        - <code>search_method</code> (text) 检索方法:以下三个关键字之一,必填
+          - <code>keyword_search</code> 关键字检索
+          - <code>semantic_search</code> 语义检索
+          - <code>full_text_search</code> 全文检索
+          - <code>hybrid_search</code> 混合检索
+        - <code>reranking_enable</code> (bool) 是否启用 Reranking,非必填,如果检索模式为semantic_search模式或者hybrid_search则传值
+        - <code>reranking_mode</code> (object) Rerank模型配置,非必填,如果启用了 reranking 则传值
+            - <code>reranking_provider_name</code> (string) Rerank 模型提供商
+            - <code>reranking_model_name</code> (string) Rerank 模型名称
+        - <code>weights</code> (double) 混合检索模式下语意检索的权重设置
+        - <code>top_k</code> (integer) 返回结果数量,非必填
+        - <code>score_threshold_enabled</code> (bool) 是否开启Score阈值
+        - <code>score_threshold</code> (double) Score阈值
+      </Property>
+      <Property name='external_retrieval_model' type='object' key='external_retrieval_model'>
+          未启用字段
+      </Property>
+    </Properties>
+  </Col>
+  <Col sticky>
+    <CodeGroup
+      title="Request"
+      tag="POST"
+      label="/datasets/{dataset_id}/hit_testing"
+      targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/hit_testing' \\\n--header 'Authorization: Bearer {api_key}'\\\n--header 'Content-Type: application/json'\\\n--data-raw '{
+        "query": "test",
+        "retrieval_model": {
+            "search_method": "keyword_search",
+            "reranking_enable": false,
+            "reranking_mode": null,
+            "reranking_model": {
+                "reranking_provider_name": "",
+                "reranking_model_name": ""
+            },
+            "weights": null,
+            "top_k": 1,
+            "score_threshold_enabled": false,
+            "score_threshold": null
+        }
+    }'`}
+    >
+    ```bash {{ title: 'cURL' }}
+    curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/hit_testing' \
+    --header 'Authorization: Bearer {api_key}' \
+    --header 'Content-Type: application/json' \
+    --data-raw '{
+        "query": "test",
+        "retrieval_model": {
+            "search_method": "keyword_search",
+            "reranking_enable": false,
+            "reranking_mode": null,
+            "reranking_model": {
+                "reranking_provider_name": "",
+                "reranking_model_name": ""
+            },
+            "weights": null,
+            "top_k": 2,
+            "score_threshold_enabled": false,
+            "score_threshold": null
+        }
+    }'
+    ```
+    </CodeGroup>
+    <CodeGroup title="Response">
+    ```json {{ title: 'Response' }}
+    {
+      "query": {
+        "content": "test"
+      },
+      "records": [
+        {
+          "segment": {
+            "id": "7fa6f24f-8679-48b3-bc9d-bdf28d73f218",
+            "position": 1,
+            "document_id": "a8c6c36f-9f5d-4d7a-8472-f5d7b75d71d2",
+            "content": "Operation guide",
+            "answer": null,
+            "word_count": 847,
+            "tokens": 280,
+            "keywords": [
+              "install",
+              "java",
+              "base",
+              "scripts",
+              "jdk",
+              "manual",
+              "internal",
+              "opens",
+              "add",
+              "vmoptions"
+            ],
+            "index_node_id": "39dd8443-d960-45a8-bb46-7275ad7fbc8e",
+            "index_node_hash": "0189157697b3c6a418ccf8264a09699f25858975578f3467c76d6bfc94df1d73",
+            "hit_count": 0,
+            "enabled": true,
+            "disabled_at": null,
+            "disabled_by": null,
+            "status": "completed",
+            "created_by": "dbcb1ab5-90c8-41a7-8b78-73b235eb6f6f",
+            "created_at": 1728734540,
+            "indexing_at": 1728734552,
+            "completed_at": 1728734584,
+            "error": null,
+            "stopped_at": null,
+            "document": {
+              "id": "a8c6c36f-9f5d-4d7a-8472-f5d7b75d71d2",
+              "data_source_type": "upload_file",
+              "name": "readme.txt",
+              "doc_type": null
+            }
+          },
+          "score": 3.730463140527718e-05,
+          "tsne_position": null
+        }
+      ]
+    }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+
+
+---
+
+<Row>
+  <Col>
+    ### 错误信息
+    <Properties>
+      <Property name='code' type='string' key='code'>
+        返回的错误代码
+      </Property>
+    </Properties>
+    <Properties>
+      <Property name='status' type='number' key='status'>
+        返回的错误状态
+      </Property>
+    </Properties>
+    <Properties>
+      <Property name='message' type='string' key='message'>
+        返回的错误信息
+      </Property>
+    </Properties>
+  </Col>
+  <Col>
+    <CodeGroup title="Example">
+    ```json {{ title: 'Response' }}
+      {
+        "code": "no_file_uploaded",
+        "message": "Please upload your file.",
+        "status": 400
+      }
+    ```
+    </CodeGroup>
+  </Col>
+</Row>
+<table className="max-w-auto border-collapse border border-slate-400" style={{ maxWidth: 'none', width: 'auto' }}>
+  <thead style={{ background: '#f9fafc' }}>
+    <tr>
+      <th className="p-2 border border-slate-300">code</th>
+      <th className="p-2 border border-slate-300">status</th>
+      <th className="p-2 border border-slate-300">message</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td className="p-2 border border-slate-300">no_file_uploaded</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">Please upload your file.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">too_many_files</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">Only one file is allowed.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">file_too_large</td>
+      <td className="p-2 border border-slate-300">413</td>
+      <td className="p-2 border border-slate-300">File size exceeded.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">unsupported_file_type</td>
+      <td className="p-2 border border-slate-300">415</td>
+      <td className="p-2 border border-slate-300">File type not allowed.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">high_quality_dataset_only</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">Current operation only supports 'high-quality' datasets.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">dataset_not_initialized</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">The dataset is still being initialized or indexing. Please wait a moment.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">archived_document_immutable</td>
+      <td className="p-2 border border-slate-300">403</td>
+      <td className="p-2 border border-slate-300">The archived document is not editable.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">dataset_name_duplicate</td>
+      <td className="p-2 border border-slate-300">409</td>
+      <td className="p-2 border border-slate-300">The dataset name already exists. Please modify your dataset name.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">invalid_action</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">Invalid action.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">document_already_finished</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">The document has been processed. Please refresh the page or go to the document details.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">document_indexing</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">The document is being processed and cannot be edited.</td>
+    </tr>
+    <tr>
+      <td className="p-2 border border-slate-300">invalid_metadata</td>
+      <td className="p-2 border border-slate-300">400</td>
+      <td className="p-2 border border-slate-300">The metadata content is incorrect. Please check and verify.</td>
+    </tr>
+  </tbody>
+</table>
+<div className="pb-4" />

+ 8 - 0
app/(commonLayout)/explore/apps/page.tsx

@@ -0,0 +1,8 @@
+import React from 'react'
+import AppList from '@/app/components/explore/app-list'
+
+const Apps = () => {
+  return <AppList />
+}
+
+export default React.memo(Apps)

+ 16 - 0
app/(commonLayout)/explore/installed/[appId]/page.tsx

@@ -0,0 +1,16 @@
+import type { FC } from 'react'
+import React from 'react'
+import Main from '@/app/components/explore/installed-app'
+
+export type IInstalledAppProps = {
+  params: {
+    appId: string
+  }
+}
+
+const InstalledApp: FC<IInstalledAppProps> = ({ params: { appId } }) => {
+  return (
+    <Main id={appId} />
+  )
+}
+export default React.memo(InstalledApp)

+ 16 - 0
app/(commonLayout)/explore/layout.tsx

@@ -0,0 +1,16 @@
+import type { FC } from 'react'
+import React from 'react'
+import ExploreClient from '@/app/components/explore'
+export type IAppDetail = {
+  children: React.ReactNode
+}
+
+const AppDetail: FC<IAppDetail> = ({ children }) => {
+  return (
+    <ExploreClient>
+      {children}
+    </ExploreClient>
+  )
+}
+
+export default React.memo(AppDetail)

+ 38 - 0
app/(commonLayout)/layout.tsx

@@ -0,0 +1,38 @@
+import React from 'react'
+import type { ReactNode } from 'react'
+import SwrInitor from '@/app/components/swr-initor'
+import { AppContextProvider } from '@/context/app-context'
+import GA, { GaType } from '@/app/components/base/ga'
+import HeaderWrapper from '@/app/components/header/header-wrapper'
+import Header from '@/app/components/header'
+import { EventEmitterContextProvider } from '@/context/event-emitter'
+import { ProviderContextProvider } from '@/context/provider-context'
+import { ModalContextProvider } from '@/context/modal-context'
+
+const Layout = ({ children }: { children: ReactNode }) => {
+  return (
+    <>
+      <GA gaType={GaType.admin} />
+      <SwrInitor>
+        <AppContextProvider>
+          <EventEmitterContextProvider>
+            <ProviderContextProvider>
+              <ModalContextProvider>
+                <HeaderWrapper>
+                  <Header />
+                </HeaderWrapper>
+                {children}
+              </ModalContextProvider>
+            </ProviderContextProvider>
+          </EventEmitterContextProvider>
+        </AppContextProvider>
+      </SwrInitor>
+    </>
+  )
+}
+
+export const metadata = {
+  title: 'Dify',
+}
+
+export default Layout

+ 225 - 0
app/(commonLayout)/list.module.css

@@ -0,0 +1,225 @@
+.listItem {
+  @apply col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-xs min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg;
+}
+
+.listItem.newItemCard {
+  @apply outline outline-1 outline-gray-200 -outline-offset-1 hover:shadow-sm hover:bg-white;
+  background-color: rgba(229, 231, 235, 0.5);
+}
+
+.listItem.selectable {
+  @apply relative bg-gray-50 outline outline-1 outline-gray-200 -outline-offset-1 shadow-none hover:bg-none hover:shadow-none hover:outline-primary-200 transition-colors;
+}
+
+.listItem.selectable * {
+  @apply relative;
+}
+
+.listItem.selectable::before {
+  content: "";
+  @apply absolute top-0 left-0 block w-full h-full rounded-lg pointer-events-none opacity-0 transition-opacity duration-200 ease-in-out hover:opacity-100;
+  background: linear-gradient(0deg,
+      rgba(235, 245, 255, 0.5),
+      rgba(235, 245, 255, 0.5)),
+    #ffffff;
+}
+
+.listItem.selectable:hover::before {
+  @apply opacity-100;
+}
+
+.listItem.selected {
+  @apply border-primary-600 hover:border-primary-600 border-2;
+}
+
+.listItem.selected::before {
+  @apply opacity-100;
+}
+
+.appIcon {
+  @apply flex items-center justify-center w-8 h-8 bg-pink-100 rounded-lg grow-0 shrink-0;
+}
+
+.appIcon.medium {
+  @apply w-9 h-9;
+}
+
+.appIcon.large {
+  @apply w-10 h-10;
+}
+
+.newItemIcon {
+  @apply flex items-center justify-center w-8 h-8 transition-colors duration-200 ease-in-out border border-gray-200 rounded-lg hover:bg-white grow-0 shrink-0;
+}
+
+.listItem:hover .newItemIcon {
+  @apply bg-gray-50 border-primary-100;
+}
+
+.newItemCard .newItemIcon {
+  @apply bg-gray-100;
+}
+
+.newItemCard:hover .newItemIcon {
+  @apply bg-white;
+}
+
+.selectable .newItemIcon {
+  @apply bg-gray-50;
+}
+
+.selectable:hover .newItemIcon {
+  @apply bg-primary-50;
+}
+
+.newItemIconImage {
+  @apply grow-0 shrink-0 block w-4 h-4 bg-center bg-contain transition-colors duration-200 ease-in-out;
+  color: #1f2a37;
+}
+
+.listItem:hover .newIconImage {
+  @apply text-primary-600;
+}
+
+.newItemIconAdd {
+  background-image: url("./apps/assets/add.svg");
+}
+
+/* .newItemIconChat {
+  background-image: url("~@/app/components/base/icons/assets/public/header-nav/studio/Robot.svg");
+}
+
+.selected .newItemIconChat {
+  background-image: url("~@/app/components/base/icons/assets/public/header-nav/studio/Robot-Active.svg");
+} */
+
+.newItemIconComplete {
+  background-image: url("./apps/assets/completion.svg");
+}
+
+.listItemTitle {
+  @apply flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0;
+}
+
+.listItemHeading {
+  @apply relative h-8 text-sm font-medium leading-8 grow;
+}
+
+.listItemHeadingContent {
+  @apply absolute top-0 left-0 w-full h-full overflow-hidden text-ellipsis whitespace-nowrap;
+}
+
+.actionIconWrapper {
+  @apply hidden h-8 w-8 p-2 rounded-md border-none hover:bg-gray-100 !important;
+}
+
+.listItem:hover .actionIconWrapper {
+  @apply !inline-flex;
+}
+
+.deleteDatasetIcon {
+  @apply hidden grow-0 shrink-0 basis-8 w-8 h-8 rounded-lg transition-colors duration-200 ease-in-out bg-white border border-gray-200 hover:bg-gray-100 bg-center bg-no-repeat;
+  background-size: 16px;
+  background-image: url('~@/assets/delete.svg');
+}
+
+.listItem:hover .deleteDatasetIcon {
+  @apply block;
+}
+
+.listItemDescription {
+  @apply mb-3 px-[14px] h-9 text-xs leading-normal text-gray-500 line-clamp-2;
+}
+
+.listItemDescription.noClip {
+  @apply line-clamp-none;
+}
+
+.listItemFooter {
+  @apply flex items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px];
+}
+
+.listItemFooter.datasetCardFooter {
+  @apply flex items-center gap-4 text-xs text-gray-500;
+}
+
+.listItemStats {
+  @apply flex items-center gap-1;
+}
+
+.listItemFooterIcon {
+  @apply block w-3 h-3 bg-center bg-contain;
+}
+
+.solidChatIcon {
+  background-image: url("./apps/assets/chat-solid.svg");
+}
+
+.solidCompletionIcon {
+  background-image: url("./apps/assets/completion-solid.svg");
+}
+
+.newItemCardHeading {
+  @apply transition-colors duration-200 ease-in-out;
+}
+
+.listItem:hover .newItemCardHeading {
+  @apply text-primary-600;
+}
+
+.listItemLink {
+  @apply inline-flex items-center gap-1 text-xs text-gray-400 transition-colors duration-200 ease-in-out;
+}
+
+.listItem:hover .listItemLink {
+  @apply text-primary-600;
+}
+
+.linkIcon {
+  @apply block w-[13px] h-[13px] bg-center bg-contain;
+  background-image: url("./apps/assets/link.svg");
+}
+
+.linkIcon.grayLinkIcon {
+  background-image: url("./apps/assets/link-gray.svg");
+}
+
+.listItem:hover .grayLinkIcon {
+  background-image: url("./apps/assets/link.svg");
+}
+
+.rightIcon {
+  @apply block w-[13px] h-[13px] bg-center bg-contain;
+  background-image: url("./apps/assets/right-arrow.svg");
+}
+
+.socialMediaLink {
+  @apply flex items-center justify-center w-8 h-8 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out;
+}
+
+.socialMediaIcon {
+  @apply block w-6 h-6 bg-center bg-contain;
+}
+
+.githubIcon {
+  background-image: url("./apps/assets/github.svg");
+}
+
+.discordIcon {
+  background-image: url("./apps/assets/discord.svg");
+}
+
+/* #region new app dialog */
+.newItemCaption {
+  @apply inline-flex items-center mb-2 text-sm font-medium;
+}
+
+/* #endregion new app dialog */
+
+.unavailable {
+  @apply opacity-50;
+}
+
+.listItem:hover .unavailable {
+  @apply opacity-100;
+}

+ 28 - 0
app/(commonLayout)/tools/page.tsx

@@ -0,0 +1,28 @@
+'use client'
+import type { FC } from 'react'
+import { useRouter } from 'next/navigation'
+import { useTranslation } from 'react-i18next'
+import React, { useEffect } from 'react'
+import ToolProviderList from '@/app/components/tools/provider-list'
+import { useAppContext } from '@/context/app-context'
+
+const Layout: FC = () => {
+  const { t } = useTranslation()
+  const router = useRouter()
+  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
+
+  useEffect(() => {
+    if (typeof window !== 'undefined')
+      document.title = `${t('tools.title')} - Dify`
+    if (isCurrentWorkspaceDatasetOperator)
+      return router.replace('/datasets')
+  }, [isCurrentWorkspaceDatasetOperator, router, t])
+
+  useEffect(() => {
+    if (isCurrentWorkspaceDatasetOperator)
+      return router.replace('/datasets')
+  }, [isCurrentWorkspaceDatasetOperator, router])
+
+  return <ToolProviderList />
+}
+export default React.memo(Layout)

+ 11 - 0
app/(shareLayout)/chat/[token]/page.tsx

@@ -0,0 +1,11 @@
+'use client'
+import React from 'react'
+import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history'
+
+const Chat = () => {
+  return (
+    <ChatWithHistoryWrap />
+  )
+}
+
+export default React.memo(Chat)

+ 11 - 0
app/(shareLayout)/chatbot/[token]/page.tsx

@@ -0,0 +1,11 @@
+'use client'
+import React from 'react'
+import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot'
+
+const Chatbot = () => {
+  return (
+    <EmbeddedChatbot />
+  )
+}
+
+export default React.memo(Chatbot)

+ 10 - 0
app/(shareLayout)/completion/[token]/page.tsx

@@ -0,0 +1,10 @@
+import React from 'react'
+import Main from '@/app/components/share/text-generation'
+
+const Completion = () => {
+  return (
+    <Main />
+  )
+}
+
+export default React.memo(Completion)

+ 21 - 0
app/(shareLayout)/layout.tsx

@@ -0,0 +1,21 @@
+import React from 'react'
+import type { FC } from 'react'
+import type { Metadata } from 'next'
+import GA, { GaType } from '@/app/components/base/ga'
+
+export const metadata: Metadata = {
+  icons: 'data:,', // prevent browser from using default favicon
+}
+
+const Layout: FC<{
+  children: React.ReactNode
+}> = ({ children }) => {
+  return (
+    <div className="min-w-[300px] h-full pb-[env(safe-area-inset-bottom)]">
+      <GA gaType={GaType.webapp} />
+      {children}
+    </div>
+  )
+}
+
+export default Layout

+ 103 - 0
app/(shareLayout)/webapp-signin/page.tsx

@@ -0,0 +1,103 @@
+'use client'
+import { useRouter, useSearchParams } from 'next/navigation'
+import type { FC } from 'react'
+import React, { useEffect } from 'react'
+import cn from '@/utils/classnames'
+import Toast from '@/app/components/base/toast'
+import { fetchSystemFeatures, fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
+import { setAccessToken } from '@/app/components/share/utils'
+import Loading from '@/app/components/base/loading'
+
+const WebSSOForm: FC = () => {
+  const searchParams = useSearchParams()
+  const router = useRouter()
+
+  const redirectUrl = searchParams.get('redirect_url')
+  const tokenFromUrl = searchParams.get('web_sso_token')
+  const message = searchParams.get('message')
+
+  const showErrorToast = (message: string) => {
+    Toast.notify({
+      type: 'error',
+      message,
+    })
+  }
+
+  const getAppCodeFromRedirectUrl = () => {
+    const appCode = redirectUrl?.split('/').pop()
+    if (!appCode)
+      return null
+
+    return appCode
+  }
+
+  const processTokenAndRedirect = async () => {
+    const appCode = getAppCodeFromRedirectUrl()
+    if (!appCode || !tokenFromUrl || !redirectUrl) {
+      showErrorToast('redirect url or app code or token is invalid.')
+      return
+    }
+
+    await setAccessToken(appCode, tokenFromUrl)
+    router.push(redirectUrl)
+  }
+
+  const handleSSOLogin = async (protocol: string) => {
+    const appCode = getAppCodeFromRedirectUrl()
+    if (!appCode || !redirectUrl) {
+      showErrorToast('redirect url or app code is invalid.')
+      return
+    }
+
+    switch (protocol) {
+      case 'saml': {
+        const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl)
+        router.push(samlRes.url)
+        break
+      }
+      case 'oidc': {
+        const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl)
+        router.push(oidcRes.url)
+        break
+      }
+      case 'oauth2': {
+        const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl)
+        router.push(oauth2Res.url)
+        break
+      }
+      default:
+        showErrorToast('SSO protocol is not supported.')
+    }
+  }
+
+  useEffect(() => {
+    const init = async () => {
+      const res = await fetchSystemFeatures()
+      const protocol = res.sso_enforced_for_web_protocol
+
+      if (message) {
+        showErrorToast(message)
+        return
+      }
+
+      if (!tokenFromUrl) {
+        await handleSSOLogin(protocol)
+        return
+      }
+
+      await processTokenAndRedirect()
+    }
+
+    init()
+  }, [message, tokenFromUrl]) // Added dependencies to useEffect
+
+  return (
+    <div className="flex items-center justify-center h-full">
+      <div className={cn('flex flex-col items-center w-full grow justify-center', 'px-6', 'md:px-[108px]')}>
+        <Loading type='area' />
+      </div>
+    </div>
+  )
+}
+
+export default React.memo(WebSSOForm)

+ 11 - 0
app/(shareLayout)/workflow/[token]/page.tsx

@@ -0,0 +1,11 @@
+import React from 'react'
+
+import Main from '@/app/components/share/text-generation'
+
+const Workflow = () => {
+  return (
+    <Main isWorkflow />
+  )
+}
+
+export default React.memo(Workflow)

+ 9 - 0
app/account/account-page/index.module.css

@@ -0,0 +1,9 @@
+.modal {
+  padding: 24px 32px !important;
+  width: 400px !important;
+}
+
+.bg {
+  background: linear-gradient(180deg, rgba(217, 45, 32, 0.05) 0%, rgba(217, 45, 32, 0.00) 24.02%), #F9FAFB;
+}
+

+ 335 - 0
app/account/account-page/index.tsx

@@ -0,0 +1,335 @@
+'use client'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+import { useContext } from 'use-context-selector'
+import s from './index.module.css'
+import Collapse from '@/app/components/header/account-setting/collapse'
+import type { IItem } from '@/app/components/header/account-setting/collapse'
+import Modal from '@/app/components/base/modal'
+import Confirm from '@/app/components/base/confirm'
+import Button from '@/app/components/base/button'
+import { updateUserProfile } from '@/service/common'
+import { useAppContext } from '@/context/app-context'
+import { ToastContext } from '@/app/components/base/toast'
+import AppIcon from '@/app/components/base/app-icon'
+import Avatar from '@/app/components/base/avatar'
+import { IS_CE_EDITION } from '@/config'
+import Input from '@/app/components/base/input'
+
+const titleClassName = `
+  text-sm font-medium text-gray-900
+`
+const descriptionClassName = `
+  mt-1 text-xs font-normal text-gray-500
+`
+
+const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
+
+export default function AccountPage() {
+  const { t } = useTranslation()
+  const { systemFeatures } = useAppContext()
+  const { mutateUserProfile, userProfile, apps } = useAppContext()
+  const { notify } = useContext(ToastContext)
+  const [editNameModalVisible, setEditNameModalVisible] = useState(false)
+  const [editName, setEditName] = useState('')
+  const [editing, setEditing] = useState(false)
+  const [editPasswordModalVisible, setEditPasswordModalVisible] = useState(false)
+  const [currentPassword, setCurrentPassword] = useState('')
+  const [password, setPassword] = useState('')
+  const [confirmPassword, setConfirmPassword] = useState('')
+  const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false)
+  const [showCurrentPassword, setShowCurrentPassword] = useState(false)
+  const [showPassword, setShowPassword] = useState(false)
+  const [showConfirmPassword, setShowConfirmPassword] = useState(false)
+
+  const handleEditName = () => {
+    setEditNameModalVisible(true)
+    setEditName(userProfile.name)
+  }
+  const handleSaveName = async () => {
+    try {
+      setEditing(true)
+      await updateUserProfile({ url: 'account/name', body: { name: editName } })
+      notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
+      mutateUserProfile()
+      setEditNameModalVisible(false)
+      setEditing(false)
+    }
+    catch (e) {
+      notify({ type: 'error', message: (e as Error).message })
+      setEditNameModalVisible(false)
+      setEditing(false)
+    }
+  }
+
+  const showErrorMessage = (message: string) => {
+    notify({
+      type: 'error',
+      message,
+    })
+  }
+  const valid = () => {
+    if (!password.trim()) {
+      showErrorMessage(t('login.error.passwordEmpty'))
+      return false
+    }
+    if (!validPassword.test(password)) {
+      showErrorMessage(t('login.error.passwordInvalid'))
+      return false
+    }
+    if (password !== confirmPassword) {
+      showErrorMessage(t('common.account.notEqual'))
+      return false
+    }
+
+    return true
+  }
+  const resetPasswordForm = () => {
+    setCurrentPassword('')
+    setPassword('')
+    setConfirmPassword('')
+  }
+  const handleSavePassword = async () => {
+    if (!valid())
+      return
+    try {
+      setEditing(true)
+      await updateUserProfile({
+        url: 'account/password',
+        body: {
+          password: currentPassword,
+          new_password: password,
+          repeat_new_password: confirmPassword,
+        },
+      })
+      notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
+      mutateUserProfile()
+      setEditPasswordModalVisible(false)
+      resetPasswordForm()
+      setEditing(false)
+    }
+    catch (e) {
+      notify({ type: 'error', message: (e as Error).message })
+      setEditPasswordModalVisible(false)
+      setEditing(false)
+    }
+  }
+
+  const renderAppItem = (item: IItem) => {
+    return (
+      <div className='flex px-3 py-1'>
+        <div className='mr-3'>
+          <AppIcon size='tiny' />
+        </div>
+        <div className='mt-[3px] text-xs font-medium text-gray-700 leading-[18px]'>{item.name}</div>
+      </div>
+    )
+  }
+
+  return (
+    <>
+      <div className='pt-2 pb-3'>
+        <h4 className='title-2xl-semi-bold text-primary'>{t('common.account.myAccount')}</h4>
+      </div>
+      <div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'>
+        <Avatar name={userProfile.name} size={64} />
+        <div className='ml-4'>
+          <p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
+          <p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
+        </div>
+      </div>
+      <div className='mb-8'>
+        <div className={titleClassName}>{t('common.account.name')}</div>
+        <div className='flex items-center justify-between gap-2 w-full mt-2'>
+          <div className='flex-1 bg-gray-100 rounded-md p-2 system-sm-regular text-components-input-text-filled '>
+            <span className='pl-1'>{userProfile.name}</span>
+          </div>
+          <div className=' bg-gray-100 rounded-md py-2 px-3 cursor-pointer system-sm-medium text-components-input-text-filled' onClick={handleEditName}>
+            {t('common.operation.edit')}
+          </div>
+        </div>
+      </div>
+      <div className='mb-8'>
+        <div className={titleClassName}>{t('common.account.email')}</div>
+        <div className='flex items-center justify-between gap-2 w-full mt-2'>
+          <div className='flex-1 bg-gray-100 rounded-md p-2 system-sm-regular text-components-input-text-filled '>
+            <span className='pl-1'>{userProfile.email}</span>
+          </div>
+        </div>
+      </div>
+      {
+        systemFeatures.enable_email_password_login && (
+          <div className='mb-8 flex justify-between gap-2'>
+            <div>
+              <div className='mb-1 text-sm font-medium text-gray-900'>{t('common.account.password')}</div>
+              <div className='mb-2 text-xs text-gray-500'>{t('common.account.passwordTip')}</div>
+            </div>
+            <Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button>
+          </div>
+        )
+      }
+      <div className='mb-6 border-[0.5px] border-gray-100' />
+      <div className='mb-8'>
+        <div className={titleClassName}>{t('common.account.langGeniusAccount')}</div>
+        <div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div>
+        {!!apps.length && (
+          <Collapse
+            title={`${t('common.account.showAppLength', { length: apps.length })}`}
+            items={apps.map(app => ({ key: app.id, name: app.name }))}
+            renderItem={renderAppItem}
+            wrapperClassName='mt-2'
+          />
+        )}
+        {!IS_CE_EDITION && <Button className='mt-2 text-[#D92D20]' onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>}
+      </div>
+      {
+        editNameModalVisible && (
+          <Modal
+            isShow
+            onClose={() => setEditNameModalVisible(false)}
+            className={s.modal}
+          >
+            <div className='mb-6 text-lg font-medium text-gray-900'>{t('common.account.editName')}</div>
+            <div className={titleClassName}>{t('common.account.name')}</div>
+            <Input className='mt-2'
+              value={editName}
+              onChange={e => setEditName(e.target.value)}
+            />
+            <div className='flex justify-end mt-10'>
+              <Button className='mr-2' onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button>
+              <Button
+                disabled={editing || !editName}
+                variant='primary'
+                onClick={handleSaveName}
+              >
+                {t('common.operation.save')}
+              </Button>
+            </div>
+          </Modal>
+        )
+      }
+      {
+        editPasswordModalVisible && (
+          <Modal
+            isShow
+            onClose={() => {
+              setEditPasswordModalVisible(false)
+              resetPasswordForm()
+            }}
+            className={s.modal}
+          >
+            <div className='mb-6 text-lg font-medium text-gray-900'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
+            {userProfile.is_password_set && (
+              <>
+                <div className={titleClassName}>{t('common.account.currentPassword')}</div>
+                <div className='relative mt-2'>
+                  <Input
+                    type={showCurrentPassword ? 'text' : 'password'}
+                    value={currentPassword}
+                    onChange={e => setCurrentPassword(e.target.value)}
+                  />
+
+                  <div className="absolute inset-y-0 right-0 flex items-center">
+                    <Button
+                      type="button"
+                      variant='ghost'
+                      onClick={() => setShowCurrentPassword(!showCurrentPassword)}
+                    >
+                      {showCurrentPassword ? '👀' : '😝'}
+                    </Button>
+                  </div>
+                </div>
+              </>
+            )}
+            <div className='mt-8 text-sm font-medium text-gray-900'>
+              {userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
+            </div>
+            <div className='relative mt-2'>
+              <Input
+                type={showPassword ? 'text' : 'password'}
+                value={password}
+                onChange={e => setPassword(e.target.value)}
+              />
+              <div className="absolute inset-y-0 right-0 flex items-center">
+                <Button
+                  type="button"
+                  variant='ghost'
+                  onClick={() => setShowPassword(!showPassword)}
+                >
+                  {showPassword ? '👀' : '😝'}
+                </Button>
+              </div>
+            </div>
+            <div className='mt-8 text-sm font-medium text-gray-900'>{t('common.account.confirmPassword')}</div>
+            <div className='relative mt-2'>
+              <Input
+                type={showConfirmPassword ? 'text' : 'password'}
+                value={confirmPassword}
+                onChange={e => setConfirmPassword(e.target.value)}
+              />
+              <div className="absolute inset-y-0 right-0 flex items-center">
+                <Button
+                  type="button"
+                  variant='ghost'
+                  onClick={() => setShowConfirmPassword(!showConfirmPassword)}
+                >
+                  {showConfirmPassword ? '👀' : '😝'}
+                </Button>
+              </div>
+            </div>
+            <div className='flex justify-end mt-10'>
+              <Button className='mr-2' onClick={() => {
+                setEditPasswordModalVisible(false)
+                resetPasswordForm()
+              }}>{t('common.operation.cancel')}</Button>
+              <Button
+                disabled={editing}
+                variant='primary'
+                onClick={handleSavePassword}
+              >
+                {userProfile.is_password_set ? t('common.operation.reset') : t('common.operation.save')}
+              </Button>
+            </div>
+          </Modal>
+        )
+      }
+      {
+        showDeleteAccountModal && (
+          <Confirm
+            isShow
+            onCancel={() => setShowDeleteAccountModal(false)}
+            onConfirm={() => setShowDeleteAccountModal(false)}
+            showCancel={false}
+            type='warning'
+            title={t('common.account.delete')}
+            content={
+              <>
+                <div className='my-1 text-[#D92D20] text-sm leading-5'>
+                  {t('common.account.deleteTip')}
+                </div>
+                <div className='mt-3 text-sm leading-5'>
+                  <span>{t('common.account.deleteConfirmTip')}</span>
+                  <a
+                    className='text-primary-600 cursor'
+                    href={`mailto:support@dify.ai?subject=Delete Account Request&body=Delete Account: ${userProfile.email}`}
+                    target='_blank'
+                    rel='noreferrer noopener'
+                    onClick={(e) => {
+                      e.preventDefault()
+                      window.location.href = e.currentTarget.href
+                    }}
+                  >
+                    support@dify.ai
+                  </a>
+                </div>
+                <div className='my-2 px-3 py-2 rounded-lg bg-gray-100 text-sm font-medium leading-5 text-gray-800'>{`${t('common.account.delete')}: ${userProfile.email}`}</div>
+              </>
+            }
+            confirmText={t('common.operation.ok') as string}
+          />
+        )
+      }
+    </>
+  )
+}

+ 94 - 0
app/account/avatar.tsx

@@ -0,0 +1,94 @@
+'use client'
+import { useTranslation } from 'react-i18next'
+import { Fragment } from 'react'
+import { useRouter } from 'next/navigation'
+import { Menu, Transition } from '@headlessui/react'
+import Avatar from '@/app/components/base/avatar'
+import { logout } from '@/service/common'
+import { useAppContext } from '@/context/app-context'
+import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
+
+export type IAppSelector = {
+  isMobile: boolean
+}
+
+export default function AppSelector() {
+  const router = useRouter()
+  const { t } = useTranslation()
+  const { userProfile } = useAppContext()
+
+  const handleLogout = async () => {
+    await logout({
+      url: '/logout',
+      params: {},
+    })
+
+    if (localStorage?.getItem('console_token'))
+      localStorage.removeItem('console_token')
+
+    router.push('/signin')
+  }
+
+  return (
+    <Menu as="div" className="relative inline-block text-left">
+      {
+        ({ open }) => (
+          <>
+            <div>
+              <Menu.Button
+                className={`
+                    inline-flex items-center
+                    rounded-[20px] p-1x text-sm
+                  text-gray-700 hover:bg-gray-200
+                    mobile:px-1
+                    ${open && 'bg-gray-200'}
+                  `}
+              >
+                <Avatar name={userProfile.name} size={32} />
+              </Menu.Button>
+            </div>
+            <Transition
+              as={Fragment}
+              enter="transition ease-out duration-100"
+              enterFrom="transform opacity-0 scale-95"
+              enterTo="transform opacity-100 scale-100"
+              leave="transition ease-in duration-75"
+              leaveFrom="transform opacity-100 scale-100"
+              leaveTo="transform opacity-0 scale-95"
+            >
+              <Menu.Items
+                className="
+                    absolute -right-2 -top-1 w-60 max-w-80
+                    divide-y divide-gray-100 origin-top-right rounded-lg bg-white
+                    shadow-lg
+                  "
+              >
+                <Menu.Item>
+                  <div className='p-1'>
+                    <div className='flex flex-nowrap items-center px-3 py-2'>
+                      <div className='grow'>
+                        <div className='system-md-medium text-text-primary break-all'>{userProfile.name}</div>
+                        <div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
+                      </div>
+                      <Avatar name={userProfile.name} size={32} />
+                    </div>
+                  </div>
+                </Menu.Item>
+                <Menu.Item>
+                  <div className='p-1' onClick={() => handleLogout()}>
+                    <div
+                      className='flex items-center justify-start h-9 px-3 rounded-lg cursor-pointer group hover:bg-gray-50'
+                    >
+                      <LogOut01 className='w-4 h-4 text-gray-500 flex mr-1' />
+                      <div className='font-normal text-[14px] text-gray-700'>{t('common.userProfile.logout')}</div>
+                    </div>
+                  </div>
+                </Menu.Item>
+              </Menu.Items>
+            </Transition>
+          </>
+        )
+      }
+    </Menu>
+  )
+}

+ 37 - 0
app/account/header.tsx

@@ -0,0 +1,37 @@
+'use client'
+import { useTranslation } from 'react-i18next'
+import { RiArrowRightUpLine, RiRobot2Line } from '@remixicon/react'
+import { useRouter } from 'next/navigation'
+import Button from '../components/base/button'
+import Avatar from './avatar'
+import LogoSite from '@/app/components/base/logo/logo-site'
+
+const Header = () => {
+  const { t } = useTranslation()
+  const router = useRouter()
+
+  const back = () => {
+    router.back()
+  }
+  return (
+    <div className='flex flex-1 items-center justify-between px-4'>
+      <div className='flex items-center gap-3'>
+        <div className='flex items-center cursor-pointer' onClick={back}>
+          <LogoSite className='object-contain' />
+        </div>
+        <div className='w-[1px] h-4 bg-divider-regular' />
+        <p className='text-text-primary title-3xl-semi-bold'>{t('common.account.account')}</p>
+      </div>
+      <div className='flex items-center flex-shrink-0 gap-3'>
+        <Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}>
+          <RiRobot2Line className='w-4 h-4' />
+          <p>{t('common.account.studio')}</p>
+          <RiArrowRightUpLine className='w-4 h-4' />
+        </Button>
+        <div className='w-[1px] h-4 bg-divider-regular' />
+        <Avatar />
+      </div>
+    </div>
+  )
+}
+export default Header

+ 40 - 0
app/account/layout.tsx

@@ -0,0 +1,40 @@
+import React from 'react'
+import type { ReactNode } from 'react'
+import Header from './header'
+import SwrInitor from '@/app/components/swr-initor'
+import { AppContextProvider } from '@/context/app-context'
+import GA, { GaType } from '@/app/components/base/ga'
+import HeaderWrapper from '@/app/components/header/header-wrapper'
+import { EventEmitterContextProvider } from '@/context/event-emitter'
+import { ProviderContextProvider } from '@/context/provider-context'
+import { ModalContextProvider } from '@/context/modal-context'
+
+const Layout = ({ children }: { children: ReactNode }) => {
+  return (
+    <>
+      <GA gaType={GaType.admin} />
+      <SwrInitor>
+        <AppContextProvider>
+          <EventEmitterContextProvider>
+            <ProviderContextProvider>
+              <ModalContextProvider>
+                <HeaderWrapper>
+                  <Header />
+                </HeaderWrapper>
+                <div className='relative flex flex-col overflow-y-auto bg-white shrink-0 h-0 grow'>
+                  {children}
+                </div>
+              </ModalContextProvider>
+            </ProviderContextProvider>
+          </EventEmitterContextProvider>
+        </AppContextProvider>
+      </SwrInitor>
+    </>
+  )
+}
+
+export const metadata = {
+  title: 'Dify',
+}
+
+export default Layout

+ 7 - 0
app/account/page.tsx

@@ -0,0 +1,7 @@
+import AccountPage from './account-page'
+
+export default function Account() {
+  return <div className='max-w-[640px] w-full mx-auto pt-12 px-6'>
+    <AccountPage />
+  </div>
+}

+ 67 - 0
app/activate/activateForm.tsx

@@ -0,0 +1,67 @@
+'use client'
+import { useTranslation } from 'react-i18next'
+import useSWR from 'swr'
+import { useRouter, useSearchParams } from 'next/navigation'
+import cn from '@/utils/classnames'
+import Button from '@/app/components/base/button'
+
+import { invitationCheck } from '@/service/common'
+import Loading from '@/app/components/base/loading'
+
+const ActivateForm = () => {
+  const router = useRouter()
+  const { t } = useTranslation()
+  const searchParams = useSearchParams()
+  const workspaceID = searchParams.get('workspace_id')
+  const email = searchParams.get('email')
+  const token = searchParams.get('token')
+
+  const checkParams = {
+    url: '/activate/check',
+    params: {
+      ...workspaceID && { workspace_id: workspaceID },
+      ...email && { email },
+      token,
+    },
+  }
+  const { data: checkRes } = useSWR(checkParams, invitationCheck, {
+    revalidateOnFocus: false,
+    onSuccess(data) {
+      if (data.is_valid) {
+        const params = new URLSearchParams(searchParams)
+        const { email, workspace_id } = data.data
+        params.set('email', encodeURIComponent(email))
+        params.set('workspace_id', encodeURIComponent(workspace_id))
+        params.set('invite_token', encodeURIComponent(token as string))
+        router.replace(`/signin?${params.toString()}`)
+      }
+    },
+  })
+
+  return (
+    <div className={
+      cn(
+        'flex flex-col items-center w-full grow justify-center',
+        'px-6',
+        'md:px-[108px]',
+      )
+    }>
+      {!checkRes && <Loading />}
+      {checkRes && !checkRes.is_valid && (
+        <div className="flex flex-col md:w-[400px]">
+          <div className="w-full mx-auto">
+            <div className="mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold">🤷‍♂️</div>
+            <h2 className="text-[32px] font-bold text-gray-900">{t('login.invalid')}</h2>
+          </div>
+          <div className="w-full mx-auto mt-6">
+            <Button variant='primary' className='w-full !text-sm'>
+              <a href="https://dify.ai">{t('login.explore')}</a>
+            </Button>
+          </div>
+        </div>
+      )}
+    </div>
+  )
+}
+
+export default ActivateForm

+ 32 - 0
app/activate/page.tsx

@@ -0,0 +1,32 @@
+import React from 'react'
+import Header from '../signin/_header'
+import style from '../signin/page.module.css'
+import ActivateForm from './activateForm'
+import cn from '@/utils/classnames'
+
+const Activate = () => {
+  return (
+    <div className={cn(
+      style.background,
+      'flex w-full min-h-screen',
+      'sm:p-4 lg:p-8',
+      'gap-x-20',
+      'justify-center lg:justify-start',
+    )}>
+      <div className={
+        cn(
+          'flex w-full flex-col bg-white shadow rounded-2xl shrink-0',
+          'space-between',
+        )
+      }>
+        <Header />
+        <ActivateForm />
+        <div className='px-8 py-6 text-sm font-normal text-gray-500'>
+          © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default Activate

+ 4 - 0
app/activate/style.module.css

@@ -0,0 +1,4 @@
+.logo {
+  background: #fff center no-repeat url(./team-28x28.png);
+  background-size: 56px;
+}

BIN
app/activate/team-28x28.png


Vissa filer visades inte eftersom för många filer har ändrats