MGFeng commited on
Commit
c867d8f
·
verified ·
1 Parent(s): 0f2fdcc

Upload 2985 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. web/.dockerignore +24 -0
  3. web/.editorconfig +22 -0
  4. web/.env.example +27 -0
  5. web/.eslintignore +7 -0
  6. web/.eslintrc.json +31 -0
  7. web/.gitignore +53 -0
  8. web/.husky/pre-commit +82 -0
  9. web/.storybook/main.ts +19 -0
  10. web/.storybook/preview.tsx +37 -0
  11. web/.storybook/storybook.css +6 -0
  12. web/Dockerfile +72 -0
  13. web/__mocks__/mime.js +0 -0
  14. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx +15 -0
  15. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/configuration/page.tsx +10 -0
  16. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/develop/page.tsx +15 -0
  17. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx +161 -0
  18. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx +11 -0
  19. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx +140 -0
  20. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx +101 -0
  21. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx +24 -0
  22. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx +87 -0
  23. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx +180 -0
  24. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts +6 -0
  25. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx +41 -0
  26. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +179 -0
  27. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +291 -0
  28. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx +97 -0
  29. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx +45 -0
  30. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx +28 -0
  31. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts +16 -0
  32. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css +6 -0
  33. web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx +12 -0
  34. web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx +27 -0
  35. web/app/(commonLayout)/apps/AppCard.tsx +422 -0
  36. web/app/(commonLayout)/apps/Apps.tsx +162 -0
  37. web/app/(commonLayout)/apps/NewAppCard.tsx +101 -0
  38. web/app/(commonLayout)/apps/assets/add.svg +3 -0
  39. web/app/(commonLayout)/apps/assets/chat-solid.svg +4 -0
  40. web/app/(commonLayout)/apps/assets/chat.svg +3 -0
  41. web/app/(commonLayout)/apps/assets/completion-solid.svg +4 -0
  42. web/app/(commonLayout)/apps/assets/completion.svg +3 -0
  43. web/app/(commonLayout)/apps/assets/discord.svg +3 -0
  44. web/app/(commonLayout)/apps/assets/github.svg +17 -0
  45. web/app/(commonLayout)/apps/assets/link-gray.svg +3 -0
  46. web/app/(commonLayout)/apps/assets/link.svg +3 -0
  47. web/app/(commonLayout)/apps/assets/right-arrow.svg +3 -0
  48. web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts +53 -0
  49. web/app/(commonLayout)/apps/page.tsx +25 -0
  50. web/app/(commonLayout)/apps/style.module.css +29 -0
.gitattributes CHANGED
@@ -5,3 +5,4 @@
5
  # them.
6
 
7
  *.sh text eol=lf
 
 
5
  # them.
6
 
7
  *.sh text eol=lf
8
+ web/app/signin/assets/background.png filter=lfs diff=lfs merge=lfs -text
web/.dockerignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ .env.*
3
+
4
+ # Logs
5
+ logs
6
+ *.log*
7
+
8
+ # node
9
+ node_modules
10
+ .husky
11
+ .next
12
+
13
+ # vscode
14
+ .vscode
15
+
16
+ # webstorm
17
+ .idea
18
+ *.iml
19
+ *.iws
20
+ *.ipr
21
+
22
+
23
+ # Jetbrains
24
+ .idea
web/.editorconfig ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ # Unix-style newlines with a newline ending every file
7
+ [*]
8
+ end_of_line = lf
9
+ insert_final_newline = true
10
+
11
+ # Matches multiple files with brace expansion notation
12
+ # Set default charset
13
+ [*.{js,tsx}]
14
+ charset = utf-8
15
+ indent_style = space
16
+ indent_size = 2
17
+
18
+
19
+ # Matches the exact files either package.json or .travis.yml
20
+ [{package.json,.travis.yml}]
21
+ indent_style = space
22
+ indent_size = 2
web/.env.example ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # For production release, change this to PRODUCTION
2
+ NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT
3
+ # The deployment edition, SELF_HOSTED
4
+ NEXT_PUBLIC_EDITION=SELF_HOSTED
5
+ # The base URL of console application, refers to the Console base URL of WEB service if console domain is
6
+ # different from api or web app domain.
7
+ # example: http://cloud.dify.ai/console/api
8
+ NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
9
+ # The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
10
+ # console or api domain.
11
+ # example: http://udify.app/api
12
+ NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
13
+
14
+ # SENTRY
15
+ NEXT_PUBLIC_SENTRY_DSN=
16
+
17
+ # Disable Next.js Telemetry (https://nextjs.org/telemetry)
18
+ NEXT_TELEMETRY_DISABLED=1
19
+
20
+ # Disable Upload Image as WebApp icon default is false
21
+ NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON=false
22
+
23
+ # The timeout for the text generation in millisecond
24
+ NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
25
+
26
+ # CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
27
+ NEXT_PUBLIC_CSP_WHITELIST=
web/.eslintignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ /**/node_modules/*
2
+ node_modules/
3
+
4
+ dist/
5
+ build/
6
+ out/
7
+ .next/
web/.eslintrc.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": [
3
+ "next",
4
+ "@antfu",
5
+ "plugin:storybook/recommended"
6
+ ],
7
+ "rules": {
8
+ "@typescript-eslint/consistent-type-definitions": [
9
+ "error",
10
+ "type"
11
+ ],
12
+ "@typescript-eslint/no-var-requires": "off",
13
+ "no-console": "off",
14
+ "indent": "off",
15
+ "@typescript-eslint/indent": [
16
+ "error",
17
+ 2,
18
+ {
19
+ "SwitchCase": 1,
20
+ "flatTernaryExpressions": false,
21
+ "ignoredNodes": [
22
+ "PropertyDefinition[decorators]",
23
+ "TSUnionType",
24
+ "FunctionExpression[params]:has(Identifier[decorators])"
25
+ ]
26
+ }
27
+ ],
28
+ "react-hooks/exhaustive-deps": "warn",
29
+ "react/display-name": "warn"
30
+ }
31
+ }
web/.gitignore ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ /.history
19
+
20
+ # misc
21
+ .DS_Store
22
+ *.pem
23
+
24
+ # debug
25
+ npm-debug.log*
26
+ yarn-debug.log*
27
+ yarn-error.log*
28
+ .pnpm-debug.log*
29
+
30
+ # local env files
31
+ .env*.local
32
+
33
+ # vercel
34
+ .vercel
35
+
36
+ # typescript
37
+ *.tsbuildinfo
38
+ next-env.d.ts
39
+
40
+ # npm
41
+ package-lock.json
42
+
43
+ # yarn
44
+ .pnp.cjs
45
+ .pnp.loader.mjs
46
+ .yarn/
47
+ .yarnrc.yml
48
+
49
+ # pmpm
50
+ pnpm-lock.yaml
51
+
52
+ .favorites.json
53
+ *storybook.log
web/.husky/pre-commit ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ # get the list of modified files
5
+ files=$(git diff --cached --name-only)
6
+
7
+ # check if api or web directory is modified
8
+
9
+ api_modified=false
10
+ web_modified=false
11
+
12
+ for file in $files
13
+ do
14
+ if [[ $file == "api/"* && $file == *.py ]]; then
15
+ # set api_modified flag to true
16
+ api_modified=true
17
+ elif [[ $file == "web/"* ]]; then
18
+ # set web_modified flag to true
19
+ web_modified=true
20
+ fi
21
+ done
22
+
23
+ # run linters based on the modified modules
24
+
25
+ if $api_modified; then
26
+ echo "Running Ruff linter on api module"
27
+
28
+ # python style checks rely on `ruff` in path
29
+ if ! command -v ruff &> /dev/null; then
30
+ echo "Installing linting tools (Ruff, dotenv-linter ...) ..."
31
+ poetry install -C api --only lint
32
+ fi
33
+
34
+ # run Ruff linter auto-fixing
35
+ ruff check --fix ./api
36
+
37
+ # run Ruff linter checks
38
+ ruff check --preview ./api || status=$?
39
+
40
+ status=${status:-0}
41
+
42
+
43
+ if [ $status -ne 0 ]; then
44
+ echo "Ruff linter on api module error, exit code: $status"
45
+ echo "Please run 'dev/reformat' to fix the fixable linting errors."
46
+ exit 1
47
+ fi
48
+ fi
49
+
50
+ if $web_modified; then
51
+ echo "Running ESLint on web module"
52
+ cd ./web || exit 1
53
+ npx lint-staged
54
+
55
+ echo "Running unit tests check"
56
+ modified_files=$(git diff --cached --name-only -- utils | grep -v '\.spec\.ts$' || true)
57
+
58
+ if [ -n "$modified_files" ]; then
59
+ for file in $modified_files; do
60
+ test_file="${file%.*}.spec.ts"
61
+ echo "Checking for test file: $test_file"
62
+
63
+ # check if the test file exists
64
+ if [ -f "../$test_file" ]; then
65
+ echo "Detected changes in $file, running corresponding unit tests..."
66
+ npm run test "../$test_file"
67
+
68
+ if [ $? -ne 0 ]; then
69
+ echo "Unit tests failed. Please fix the errors before committing."
70
+ exit 1
71
+ fi
72
+ echo "Unit tests for $file passed."
73
+ else
74
+ echo "Warning: $file does not have a corresponding test file."
75
+ fi
76
+
77
+ done
78
+ echo "All unit tests for modified web/utils files have passed."
79
+ fi
80
+
81
+ cd ../
82
+ fi
web/.storybook/main.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { StorybookConfig } from '@storybook/nextjs'
2
+
3
+ const config: StorybookConfig = {
4
+ // stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
5
+ stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
6
+ addons: [
7
+ '@storybook/addon-onboarding',
8
+ '@storybook/addon-links',
9
+ '@storybook/addon-essentials',
10
+ '@chromatic-com/storybook',
11
+ '@storybook/addon-interactions',
12
+ ],
13
+ framework: {
14
+ name: '@storybook/nextjs',
15
+ options: {},
16
+ },
17
+ staticDirs: ['../public'],
18
+ }
19
+ export default config
web/.storybook/preview.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import type { Preview } from '@storybook/react'
3
+ import { withThemeByDataAttribute } from '@storybook/addon-themes';
4
+ import I18nServer from '../app/components/i18n-server'
5
+
6
+ import '../app/styles/globals.css'
7
+ import '../app/styles/markdown.scss'
8
+ import './storybook.css'
9
+
10
+ export const decorators = [
11
+ withThemeByDataAttribute({
12
+ themes: {
13
+ light: 'light',
14
+ dark: 'dark',
15
+ },
16
+ defaultTheme: 'light',
17
+ attributeName: 'data-theme',
18
+ }),
19
+ Story => {
20
+ return <I18nServer>
21
+ <Story />
22
+ </I18nServer>
23
+ }
24
+ ];
25
+
26
+ const preview: Preview = {
27
+ parameters: {
28
+ controls: {
29
+ matchers: {
30
+ color: /(background|color)$/i,
31
+ date: /Date$/i,
32
+ },
33
+ },
34
+ },
35
+ }
36
+
37
+ export default preview
web/.storybook/storybook.css ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ html,
2
+ body {
3
+ max-width: unset;
4
+ overflow: auto;
5
+ user-select: text;
6
+ }
web/Dockerfile ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # base image
2
+ FROM node:20.11-alpine3.19 AS base
3
+ LABEL maintainer="takatost@gmail.com"
4
+
5
+ # if you located in China, you can use aliyun mirror to speed up
6
+ # RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
7
+
8
+ RUN apk add --no-cache tzdata
9
+
10
+
11
+ # install packages
12
+ FROM base AS packages
13
+
14
+ WORKDIR /app/web
15
+
16
+ COPY package.json .
17
+ COPY yarn.lock .
18
+
19
+ # if you located in China, you can use taobao registry to speed up
20
+ # RUN yarn install --frozen-lockfile --registry https://registry.npmmirror.com/
21
+
22
+ RUN yarn install --frozen-lockfile
23
+
24
+ # build resources
25
+ FROM base AS builder
26
+ WORKDIR /app/web
27
+ COPY --from=packages /app/web/ .
28
+ COPY . .
29
+
30
+ RUN yarn build
31
+
32
+
33
+ # production stage
34
+ FROM base AS production
35
+
36
+ ENV NODE_ENV=production
37
+ ENV EDITION=SELF_HOSTED
38
+ ENV DEPLOY_ENV=PRODUCTION
39
+ ENV CONSOLE_API_URL=http://127.0.0.1:5001
40
+ ENV APP_API_URL=http://127.0.0.1:5001
41
+ ENV PORT=3000
42
+ ENV NEXT_TELEMETRY_DISABLED=1
43
+
44
+ # set timezone
45
+ ENV TZ=UTC
46
+ RUN ln -s /usr/share/zoneinfo/${TZ} /etc/localtime \
47
+ && echo ${TZ} > /etc/timezone
48
+
49
+
50
+ WORKDIR /app/web
51
+ COPY --from=builder /app/web/public ./public
52
+ COPY --from=builder /app/web/.next/standalone ./
53
+ COPY --from=builder /app/web/.next/static ./.next/static
54
+
55
+ COPY docker/pm2.json ./pm2.json
56
+ COPY docker/entrypoint.sh ./entrypoint.sh
57
+
58
+
59
+ # global runtime packages
60
+ RUN yarn global add pm2 \
61
+ && yarn cache clean \
62
+ && mkdir /.pm2 \
63
+ && chown -R 1001:0 /.pm2 /app/web \
64
+ && chmod -R g=u /.pm2 /app/web
65
+
66
+
67
+ ARG COMMIT_SHA
68
+ ENV COMMIT_SHA=${COMMIT_SHA}
69
+
70
+ USER 1001
71
+ EXPOSE 3000
72
+ ENTRYPOINT ["/bin/sh", "./entrypoint.sh"]
web/__mocks__/mime.js ADDED
File without changes
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import Main from '@/app/components/app/log-annotation'
3
+ import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
4
+
5
+ export type IProps = {
6
+ params: { appId: string }
7
+ }
8
+
9
+ const Logs = async () => {
10
+ return (
11
+ <Main pageType={PageType.annotation} />
12
+ )
13
+ }
14
+
15
+ export default Logs
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/configuration/page.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import Configuration from '@/app/components/app/configuration'
3
+
4
+ const IConfiguration = async () => {
5
+ return (
6
+ <Configuration />
7
+ )
8
+ }
9
+
10
+ export default IConfiguration
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/develop/page.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import { type Locale } from '@/i18n'
3
+ import DevelopMain from '@/app/components/develop'
4
+
5
+ export type IDevelopProps = {
6
+ params: { locale: Locale; appId: string }
7
+ }
8
+
9
+ const Develop = async ({
10
+ params: { appId },
11
+ }: IDevelopProps) => {
12
+ return <DevelopMain appId={appId} />
13
+ }
14
+
15
+ export default Develop
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import { useUnmount } from 'ahooks'
4
+ import React, { useCallback, useEffect, useState } from 'react'
5
+ import { usePathname, useRouter } from 'next/navigation'
6
+ import {
7
+ RiDashboard2Fill,
8
+ RiDashboard2Line,
9
+ RiFileList3Fill,
10
+ RiFileList3Line,
11
+ RiTerminalBoxFill,
12
+ RiTerminalBoxLine,
13
+ RiTerminalWindowFill,
14
+ RiTerminalWindowLine,
15
+ } from '@remixicon/react'
16
+ import { useTranslation } from 'react-i18next'
17
+ import { useShallow } from 'zustand/react/shallow'
18
+ import { useContextSelector } from 'use-context-selector'
19
+ import s from './style.module.css'
20
+ import cn from '@/utils/classnames'
21
+ import { useStore } from '@/app/components/app/store'
22
+ import AppSideBar from '@/app/components/app-sidebar'
23
+ import type { NavIcon } from '@/app/components/app-sidebar/navLink'
24
+ import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
25
+ import AppContext, { useAppContext } from '@/context/app-context'
26
+ import Loading from '@/app/components/base/loading'
27
+ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
28
+
29
+ export type IAppDetailLayoutProps = {
30
+ children: React.ReactNode
31
+ params: { appId: string }
32
+ }
33
+
34
+ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
35
+ const {
36
+ children,
37
+ params: { appId }, // get appId in path
38
+ } = props
39
+ const { t } = useTranslation()
40
+ const router = useRouter()
41
+ const pathname = usePathname()
42
+ const media = useBreakpoints()
43
+ const isMobile = media === MediaType.mobile
44
+ const { isCurrentWorkspaceEditor } = useAppContext()
45
+ const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({
46
+ appDetail: state.appDetail,
47
+ setAppDetail: state.setAppDetail,
48
+ setAppSiderbarExpand: state.setAppSiderbarExpand,
49
+ })))
50
+ const [navigation, setNavigation] = useState<Array<{
51
+ name: string
52
+ href: string
53
+ icon: NavIcon
54
+ selectedIcon: NavIcon
55
+ }>>([])
56
+ const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
57
+
58
+ const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
59
+ const navs = [
60
+ ...(isCurrentWorkspaceEditor
61
+ ? [{
62
+ name: t('common.appMenus.promptEng'),
63
+ href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
64
+ icon: RiTerminalWindowLine,
65
+ selectedIcon: RiTerminalWindowFill,
66
+ }]
67
+ : []
68
+ ),
69
+ {
70
+ name: t('common.appMenus.apiAccess'),
71
+ href: `/app/${appId}/develop`,
72
+ icon: RiTerminalBoxLine,
73
+ selectedIcon: RiTerminalBoxFill,
74
+ },
75
+ ...(isCurrentWorkspaceEditor
76
+ ? [{
77
+ name: mode !== 'workflow'
78
+ ? t('common.appMenus.logAndAnn')
79
+ : t('common.appMenus.logs'),
80
+ href: `/app/${appId}/logs`,
81
+ icon: RiFileList3Line,
82
+ selectedIcon: RiFileList3Fill,
83
+ }]
84
+ : []
85
+ ),
86
+ {
87
+ name: t('common.appMenus.overview'),
88
+ href: `/app/${appId}/overview`,
89
+ icon: RiDashboard2Line,
90
+ selectedIcon: RiDashboard2Fill,
91
+ },
92
+ ]
93
+ return navs
94
+ }, [t])
95
+
96
+ useEffect(() => {
97
+ if (appDetail) {
98
+ document.title = `${(appDetail.name || 'App')} - Dify`
99
+ const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
100
+ const mode = isMobile ? 'collapse' : 'expand'
101
+ setAppSiderbarExpand(isMobile ? mode : localeMode)
102
+ // TODO: consider screen size and mode
103
+ // if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
104
+ // setAppSiderbarExpand('collapse')
105
+ }
106
+ }, [appDetail, isMobile])
107
+
108
+ useEffect(() => {
109
+ setAppDetail()
110
+ fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
111
+ // redirection
112
+ const canIEditApp = isCurrentWorkspaceEditor
113
+ if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
114
+ router.replace(`/app/${appId}/overview`)
115
+ return
116
+ }
117
+ if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
118
+ router.replace(`/app/${appId}/workflow`)
119
+ }
120
+ else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
121
+ router.replace(`/app/${appId}/configuration`)
122
+ }
123
+ else {
124
+ setAppDetail({ ...res, enable_sso: false })
125
+ setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
126
+ if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
127
+ fetchAppSSO({ appId }).then((ssoRes) => {
128
+ setAppDetail({ ...res, enable_sso: ssoRes.enabled })
129
+ })
130
+ }
131
+ }
132
+ }).catch((e: any) => {
133
+ if (e.status === 404)
134
+ router.replace('/apps')
135
+ })
136
+ }, [appId, isCurrentWorkspaceEditor, systemFeatures, getNavigations, pathname, router, setAppDetail])
137
+
138
+ useUnmount(() => {
139
+ setAppDetail()
140
+ })
141
+
142
+ if (!appDetail) {
143
+ return (
144
+ <div className='flex h-full items-center justify-center bg-white'>
145
+ <Loading />
146
+ </div>
147
+ )
148
+ }
149
+
150
+ return (
151
+ <div className={cn(s.app, 'flex', 'overflow-hidden')}>
152
+ {appDetail && (
153
+ <AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background} desc={appDetail.mode} navigation={navigation} />
154
+ )}
155
+ <div className="bg-white grow overflow-hidden">
156
+ {children}
157
+ </div>
158
+ </div>
159
+ )
160
+ }
161
+ export default React.memo(AppDetailLayout)
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import Main from '@/app/components/app/log-annotation'
3
+ import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
4
+
5
+ const Logs = async () => {
6
+ return (
7
+ <Main pageType={PageType.log} />
8
+ )
9
+ }
10
+
11
+ export default Logs
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import React from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { useContext, useContextSelector } from 'use-context-selector'
6
+ import AppCard from '@/app/components/app/overview/appCard'
7
+ import Loading from '@/app/components/base/loading'
8
+ import { ToastContext } from '@/app/components/base/toast'
9
+ import {
10
+ fetchAppDetail,
11
+ fetchAppSSO,
12
+ updateAppSSO,
13
+ updateAppSiteAccessToken,
14
+ updateAppSiteConfig,
15
+ updateAppSiteStatus,
16
+ } from '@/service/apps'
17
+ import type { App, AppSSO } from '@/types/app'
18
+ import type { UpdateAppSiteCodeResponse } from '@/models/app'
19
+ import { asyncRunSafe } from '@/utils'
20
+ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
21
+ import type { IAppCardProps } from '@/app/components/app/overview/appCard'
22
+ import { useStore as useAppStore } from '@/app/components/app/store'
23
+ import AppContext from '@/context/app-context'
24
+
25
+ export type ICardViewProps = {
26
+ appId: string
27
+ }
28
+
29
+ const CardView: FC<ICardViewProps> = ({ appId }) => {
30
+ const { t } = useTranslation()
31
+ const { notify } = useContext(ToastContext)
32
+ const appDetail = useAppStore(state => state.appDetail)
33
+ const setAppDetail = useAppStore(state => state.setAppDetail)
34
+ const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
35
+
36
+ const updateAppDetail = async () => {
37
+ try {
38
+ const res = await fetchAppDetail({ url: '/apps', id: appId })
39
+ if (systemFeatures.enable_web_sso_switch_component) {
40
+ const ssoRes = await fetchAppSSO({ appId })
41
+ setAppDetail({ ...res, enable_sso: ssoRes.enabled })
42
+ }
43
+ else {
44
+ setAppDetail({ ...res })
45
+ }
46
+ }
47
+ catch (error) { console.error(error) }
48
+ }
49
+
50
+ const handleCallbackResult = (err: Error | null, message?: string) => {
51
+ const type = err ? 'error' : 'success'
52
+
53
+ message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
54
+
55
+ if (type === 'success')
56
+ updateAppDetail()
57
+
58
+ notify({
59
+ type,
60
+ message: t(`common.actionMsg.${message}`),
61
+ })
62
+ }
63
+
64
+ const onChangeSiteStatus = async (value: boolean) => {
65
+ const [err] = await asyncRunSafe<App>(
66
+ updateAppSiteStatus({
67
+ url: `/apps/${appId}/site-enable`,
68
+ body: { enable_site: value },
69
+ }) as Promise<App>,
70
+ )
71
+
72
+ handleCallbackResult(err)
73
+ }
74
+
75
+ const onChangeApiStatus = async (value: boolean) => {
76
+ const [err] = await asyncRunSafe<App>(
77
+ updateAppSiteStatus({
78
+ url: `/apps/${appId}/api-enable`,
79
+ body: { enable_api: value },
80
+ }) as Promise<App>,
81
+ )
82
+
83
+ handleCallbackResult(err)
84
+ }
85
+
86
+ const onSaveSiteConfig: IAppCardProps['onSaveSiteConfig'] = async (params) => {
87
+ const [err] = await asyncRunSafe<App>(
88
+ updateAppSiteConfig({
89
+ url: `/apps/${appId}/site`,
90
+ body: params,
91
+ }) as Promise<App>,
92
+ )
93
+ if (!err)
94
+ localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
95
+
96
+ if (systemFeatures.enable_web_sso_switch_component) {
97
+ const [sso_err] = await asyncRunSafe<AppSSO>(
98
+ updateAppSSO({ id: appId, enabled: Boolean(params.enable_sso) }) as Promise<AppSSO>,
99
+ )
100
+ if (sso_err) {
101
+ handleCallbackResult(sso_err)
102
+ return
103
+ }
104
+ }
105
+
106
+ handleCallbackResult(err)
107
+ }
108
+
109
+ const onGenerateCode = async () => {
110
+ const [err] = await asyncRunSafe<UpdateAppSiteCodeResponse>(
111
+ updateAppSiteAccessToken({
112
+ url: `/apps/${appId}/site/access-token-reset`,
113
+ }) as Promise<UpdateAppSiteCodeResponse>,
114
+ )
115
+
116
+ handleCallbackResult(err, err ? 'generatedUnsuccessfully' : 'generatedSuccessfully')
117
+ }
118
+
119
+ if (!appDetail)
120
+ return <Loading />
121
+
122
+ return (
123
+ <div className="grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6">
124
+ <AppCard
125
+ appInfo={appDetail}
126
+ cardType="webapp"
127
+ onChangeStatus={onChangeSiteStatus}
128
+ onGenerateCode={onGenerateCode}
129
+ onSaveSiteConfig={onSaveSiteConfig}
130
+ />
131
+ <AppCard
132
+ cardType="api"
133
+ appInfo={appDetail}
134
+ onChangeStatus={onChangeApiStatus}
135
+ />
136
+ </div>
137
+ )
138
+ }
139
+
140
+ export default CardView
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import React, { useState } from 'react'
3
+ import dayjs from 'dayjs'
4
+ import quarterOfYear from 'dayjs/plugin/quarterOfYear'
5
+ import { useTranslation } from 'react-i18next'
6
+ import type { PeriodParams } from '@/app/components/app/overview/appChart'
7
+ import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/appChart'
8
+ import type { Item } from '@/app/components/base/select'
9
+ import { SimpleSelect } from '@/app/components/base/select'
10
+ import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter'
11
+ import { useStore as useAppStore } from '@/app/components/app/store'
12
+
13
+ dayjs.extend(quarterOfYear)
14
+
15
+ const today = dayjs()
16
+
17
+ const queryDateFormat = 'YYYY-MM-DD HH:mm'
18
+
19
+ export type IChartViewProps = {
20
+ appId: string
21
+ }
22
+
23
+ export default function ChartView({ appId }: IChartViewProps) {
24
+ const { t } = useTranslation()
25
+ const appDetail = useAppStore(state => state.appDetail)
26
+ const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
27
+ const isWorkflow = appDetail?.mode === 'workflow'
28
+ 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) } })
29
+
30
+ const onSelect = (item: Item) => {
31
+ if (item.value === 'all') {
32
+ setPeriod({ name: item.name, query: undefined })
33
+ }
34
+ else if (item.value === 0) {
35
+ const startOfToday = today.startOf('day').format(queryDateFormat)
36
+ const endOfToday = today.endOf('day').format(queryDateFormat)
37
+ setPeriod({ name: item.name, query: { start: startOfToday, end: endOfToday } })
38
+ }
39
+ else {
40
+ setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } })
41
+ }
42
+ }
43
+
44
+ if (!appDetail)
45
+ return null
46
+
47
+ return (
48
+ <div>
49
+ <div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
50
+ <span className='mr-3'>{t('appOverview.analysis.title')}</span>
51
+ <SimpleSelect
52
+ items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
53
+ className='mt-0 !w-40'
54
+ onSelect={onSelect}
55
+ defaultValue={7}
56
+ />
57
+ </div>
58
+ {!isWorkflow && (
59
+ <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
60
+ <ConversationsChart period={period} id={appId} />
61
+ <EndUsersChart period={period} id={appId} />
62
+ </div>
63
+ )}
64
+ {!isWorkflow && (
65
+ <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
66
+ {isChatApp
67
+ ? (
68
+ <AvgSessionInteractions period={period} id={appId} />
69
+ )
70
+ : (
71
+ <AvgResponseTime period={period} id={appId} />
72
+ )}
73
+ <TokenPerSecond period={period} id={appId} />
74
+ </div>
75
+ )}
76
+ {!isWorkflow && (
77
+ <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
78
+ <UserSatisfactionRate period={period} id={appId} />
79
+ <CostChart period={period} id={appId} />
80
+ </div>
81
+ )}
82
+ {!isWorkflow && isChatApp && (
83
+ <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
84
+ <MessagesChart period={period} id={appId} />
85
+ </div>
86
+ )}
87
+ {isWorkflow && (
88
+ <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
89
+ <WorkflowMessagesChart period={period} id={appId} />
90
+ <WorkflowDailyTerminalsChart period={period} id={appId} />
91
+ </div>
92
+ )}
93
+ {isWorkflow && (
94
+ <div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
95
+ <WorkflowCostChart period={period} id={appId} />
96
+ <AvgUserInteractions period={period} id={appId} />
97
+ </div>
98
+ )}
99
+ </div>
100
+ )
101
+ }
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ChartView from './chartView'
3
+ import CardView from './cardView'
4
+ import TracingPanel from './tracing/panel'
5
+ import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
6
+
7
+ export type IDevelopProps = {
8
+ params: { appId: string }
9
+ }
10
+
11
+ const Overview = async ({
12
+ params: { appId },
13
+ }: IDevelopProps) => {
14
+ return (
15
+ <div className="h-full px-4 sm:px-16 py-6 overflow-scroll">
16
+ <ApikeyInfoPanel />
17
+ <TracingPanel />
18
+ <CardView appId={appId} />
19
+ <ChartView appId={appId} />
20
+ </div>
21
+ )
22
+ }
23
+
24
+ export default Overview
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import type { PopupProps } from './config-popup'
6
+ import ConfigPopup from './config-popup'
7
+ import cn from '@/utils/classnames'
8
+ import Button from '@/app/components/base/button'
9
+ import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
10
+ import {
11
+ PortalToFollowElem,
12
+ PortalToFollowElemContent,
13
+ PortalToFollowElemTrigger,
14
+ } from '@/app/components/base/portal-to-follow-elem'
15
+
16
+ const I18N_PREFIX = 'app.tracing'
17
+
18
+ type Props = {
19
+ readOnly: boolean
20
+ className?: string
21
+ hasConfigured: boolean
22
+ controlShowPopup?: number
23
+ } & PopupProps
24
+
25
+ const ConfigBtn: FC<Props> = ({
26
+ className,
27
+ hasConfigured,
28
+ controlShowPopup,
29
+ ...popupProps
30
+ }) => {
31
+ const { t } = useTranslation()
32
+ const [open, doSetOpen] = useState(false)
33
+ const openRef = useRef(open)
34
+ const setOpen = useCallback((v: boolean) => {
35
+ doSetOpen(v)
36
+ openRef.current = v
37
+ }, [doSetOpen])
38
+
39
+ const handleTrigger = useCallback(() => {
40
+ setOpen(!openRef.current)
41
+ }, [setOpen])
42
+
43
+ useEffect(() => {
44
+ if (controlShowPopup)
45
+ // setOpen(!openRef.current)
46
+ setOpen(true)
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, [controlShowPopup])
49
+
50
+ if (popupProps.readOnly && !hasConfigured)
51
+ return null
52
+
53
+ const triggerContent = hasConfigured
54
+ ? (
55
+ <div className={cn(className, 'p-1 rounded-md hover:bg-black/5 cursor-pointer')}>
56
+ <Settings04 className='w-4 h-4 text-gray-500' />
57
+ </div>
58
+ )
59
+ : (
60
+ <Button variant='primary'
61
+ className={cn(className, '!h-8 !px-3 select-none')}
62
+ >
63
+ <Settings04 className='mr-1 w-4 h-4' />
64
+ <span className='text-[13px]'>{t(`${I18N_PREFIX}.config`)}</span>
65
+ </Button>
66
+ )
67
+
68
+ return (
69
+ <PortalToFollowElem
70
+ open={open}
71
+ onOpenChange={setOpen}
72
+ placement='bottom-end'
73
+ offset={{
74
+ mainAxis: 12,
75
+ crossAxis: hasConfigured ? 8 : 0,
76
+ }}
77
+ >
78
+ <PortalToFollowElemTrigger onClick={handleTrigger}>
79
+ {triggerContent}
80
+ </PortalToFollowElemTrigger>
81
+ <PortalToFollowElemContent className='z-[11]'>
82
+ <ConfigPopup {...popupProps} />
83
+ </PortalToFollowElemContent>
84
+ </PortalToFollowElem>
85
+ )
86
+ }
87
+ export default React.memo(ConfigBtn)
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import React, { useCallback, useState } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { useBoolean } from 'ahooks'
6
+ import TracingIcon from './tracing-icon'
7
+ import ProviderPanel from './provider-panel'
8
+ import type { LangFuseConfig, LangSmithConfig } from './type'
9
+ import { TracingProvider } from './type'
10
+ import ProviderConfigModal from './provider-config-modal'
11
+ import Indicator from '@/app/components/header/indicator'
12
+ import Switch from '@/app/components/base/switch'
13
+ import Tooltip from '@/app/components/base/tooltip'
14
+
15
+ const I18N_PREFIX = 'app.tracing'
16
+
17
+ export type PopupProps = {
18
+ appId: string
19
+ readOnly: boolean
20
+ enabled: boolean
21
+ onStatusChange: (enabled: boolean) => void
22
+ chosenProvider: TracingProvider | null
23
+ onChooseProvider: (provider: TracingProvider) => void
24
+ langSmithConfig: LangSmithConfig | null
25
+ langFuseConfig: LangFuseConfig | null
26
+ onConfigUpdated: (provider: TracingProvider, payload: LangSmithConfig | LangFuseConfig) => void
27
+ onConfigRemoved: (provider: TracingProvider) => void
28
+ }
29
+
30
+ const ConfigPopup: FC<PopupProps> = ({
31
+ appId,
32
+ readOnly,
33
+ enabled,
34
+ onStatusChange,
35
+ chosenProvider,
36
+ onChooseProvider,
37
+ langSmithConfig,
38
+ langFuseConfig,
39
+ onConfigUpdated,
40
+ onConfigRemoved,
41
+ }) => {
42
+ const { t } = useTranslation()
43
+
44
+ const [currentProvider, setCurrentProvider] = useState<TracingProvider | null>(TracingProvider.langfuse)
45
+ const [isShowConfigModal, {
46
+ setTrue: showConfigModal,
47
+ setFalse: hideConfigModal,
48
+ }] = useBoolean(false)
49
+ const handleOnConfig = useCallback((provider: TracingProvider) => {
50
+ return () => {
51
+ setCurrentProvider(provider)
52
+ showConfigModal()
53
+ }
54
+ }, [showConfigModal])
55
+
56
+ const handleOnChoose = useCallback((provider: TracingProvider) => {
57
+ return () => {
58
+ onChooseProvider(provider)
59
+ }
60
+ }, [onChooseProvider])
61
+
62
+ const handleConfigUpdated = useCallback((payload: LangSmithConfig | LangFuseConfig) => {
63
+ onConfigUpdated(currentProvider!, payload)
64
+ hideConfigModal()
65
+ }, [currentProvider, hideConfigModal, onConfigUpdated])
66
+
67
+ const handleConfigRemoved = useCallback(() => {
68
+ onConfigRemoved(currentProvider!)
69
+ hideConfigModal()
70
+ }, [currentProvider, hideConfigModal, onConfigRemoved])
71
+
72
+ const providerAllConfigured = langSmithConfig && langFuseConfig
73
+ const providerAllNotConfigured = !langSmithConfig && !langFuseConfig
74
+
75
+ const switchContent = (
76
+ <Switch
77
+ className='ml-3'
78
+ defaultValue={enabled}
79
+ onChange={onStatusChange}
80
+ size='l'
81
+ disabled={providerAllNotConfigured}
82
+ />
83
+ )
84
+ const langSmithPanel = (
85
+ <ProviderPanel
86
+ type={TracingProvider.langSmith}
87
+ readOnly={readOnly}
88
+ config={langSmithConfig}
89
+ hasConfigured={!!langSmithConfig}
90
+ onConfig={handleOnConfig(TracingProvider.langSmith)}
91
+ isChosen={chosenProvider === TracingProvider.langSmith}
92
+ onChoose={handleOnChoose(TracingProvider.langSmith)}
93
+ />
94
+ )
95
+
96
+ const langfusePanel = (
97
+ <ProviderPanel
98
+ type={TracingProvider.langfuse}
99
+ readOnly={readOnly}
100
+ config={langFuseConfig}
101
+ hasConfigured={!!langFuseConfig}
102
+ onConfig={handleOnConfig(TracingProvider.langfuse)}
103
+ isChosen={chosenProvider === TracingProvider.langfuse}
104
+ onChoose={handleOnChoose(TracingProvider.langfuse)}
105
+ />
106
+ )
107
+
108
+ return (
109
+ <div className='w-[420px] p-4 rounded-2xl bg-white border-[0.5px] border-black/5 shadow-lg'>
110
+ <div className='flex justify-between items-center'>
111
+ <div className='flex items-center'>
112
+ <TracingIcon size='md' className='mr-2' />
113
+ <div className='leading-[120%] text-[18px] font-semibold text-gray-900'>{t(`${I18N_PREFIX}.tracing`)}</div>
114
+ </div>
115
+ <div className='flex items-center'>
116
+ <Indicator color={enabled ? 'green' : 'gray'} />
117
+ <div className='ml-1.5 text-xs font-semibold text-gray-500 uppercase'>
118
+ {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
119
+ </div>
120
+ {!readOnly && (
121
+ <>
122
+ {providerAllNotConfigured
123
+ ? (
124
+ <Tooltip
125
+ popupContent={t(`${I18N_PREFIX}.disabledTip`)}
126
+ >
127
+ {switchContent}
128
+ </Tooltip>
129
+ )
130
+ : switchContent}
131
+ </>
132
+ )}
133
+
134
+ </div>
135
+ </div>
136
+
137
+ <div className='mt-2 leading-4 text-xs font-normal text-gray-500'>
138
+ {t(`${I18N_PREFIX}.tracingDescription`)}
139
+ </div>
140
+ <div className='mt-3 h-px bg-gray-100'></div>
141
+ <div className='mt-3'>
142
+ {(providerAllConfigured || providerAllNotConfigured)
143
+ ? (
144
+ <>
145
+ <div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
146
+ <div className='mt-2 space-y-2'>
147
+ {langSmithPanel}
148
+ {langfusePanel}
149
+ </div>
150
+ </>
151
+ )
152
+ : (
153
+ <>
154
+ <div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
155
+ <div className='mt-2'>
156
+ {langSmithConfig ? langSmithPanel : langfusePanel}
157
+ </div>
158
+ <div className='mt-3 leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
159
+ <div className='mt-2'>
160
+ {!langSmithConfig ? langSmithPanel : langfusePanel}
161
+ </div>
162
+ </>
163
+ )}
164
+
165
+ </div>
166
+ {isShowConfigModal && (
167
+ <ProviderConfigModal
168
+ appId={appId}
169
+ type={currentProvider!}
170
+ payload={currentProvider === TracingProvider.langSmith ? langSmithConfig : langFuseConfig}
171
+ onCancel={hideConfigModal}
172
+ onSaved={handleConfigUpdated}
173
+ onChosen={onChooseProvider}
174
+ onRemoved={handleConfigRemoved}
175
+ />
176
+ )}
177
+ </div>
178
+ )
179
+ }
180
+ export default React.memo(ConfigPopup)
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { TracingProvider } from './type'
2
+
3
+ export const docURL = {
4
+ [TracingProvider.langSmith]: 'https://docs.smith.langchain.com/',
5
+ [TracingProvider.langfuse]: 'https://docs.langfuse.com',
6
+ }
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import React from 'react'
4
+ import cn from '@/utils/classnames'
5
+ import Input from '@/app/components/base/input'
6
+
7
+ type Props = {
8
+ className?: string
9
+ label: string
10
+ labelClassName?: string
11
+ value: string | number
12
+ onChange: (value: string) => void
13
+ isRequired?: boolean
14
+ placeholder?: string
15
+ }
16
+
17
+ const Field: FC<Props> = ({
18
+ className,
19
+ label,
20
+ labelClassName,
21
+ value,
22
+ onChange,
23
+ isRequired = false,
24
+ placeholder = '',
25
+ }) => {
26
+ return (
27
+ <div className={cn(className)}>
28
+ <div className='flex py-[7px]'>
29
+ <div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-gray-900')}>{label} </div>
30
+ {isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
31
+ </div>
32
+ <Input
33
+ value={value}
34
+ onChange={e => onChange(e.target.value)}
35
+ className='h-9'
36
+ placeholder={placeholder}
37
+ />
38
+ </div>
39
+ )
40
+ }
41
+ export default React.memo(Field)
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import React, { useCallback, useEffect, useState } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { usePathname } from 'next/navigation'
6
+ import { useBoolean } from 'ahooks'
7
+ import type { LangFuseConfig, LangSmithConfig } from './type'
8
+ import { TracingProvider } from './type'
9
+ import TracingIcon from './tracing-icon'
10
+ import ConfigButton from './config-button'
11
+ import cn from '@/utils/classnames'
12
+ import { LangfuseIcon, LangsmithIcon } from '@/app/components/base/icons/src/public/tracing'
13
+ import Indicator from '@/app/components/header/indicator'
14
+ import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
15
+ import type { TracingStatus } from '@/models/app'
16
+ import Toast from '@/app/components/base/toast'
17
+ import { useAppContext } from '@/context/app-context'
18
+ import Loading from '@/app/components/base/loading'
19
+
20
+ const I18N_PREFIX = 'app.tracing'
21
+
22
+ const Title = ({
23
+ className,
24
+ }: {
25
+ className?: string
26
+ }) => {
27
+ const { t } = useTranslation()
28
+
29
+ return (
30
+ <div className={cn(className, 'flex items-center text-lg font-semibold text-gray-900')}>
31
+ {t('common.appMenus.overview')}
32
+ </div>
33
+ )
34
+ }
35
+ const Panel: FC = () => {
36
+ const { t } = useTranslation()
37
+ const pathname = usePathname()
38
+ const matched = pathname.match(/\/app\/([^/]+)/)
39
+ const appId = (matched?.length && matched[1]) ? matched[1] : ''
40
+ const { isCurrentWorkspaceEditor } = useAppContext()
41
+ const readOnly = !isCurrentWorkspaceEditor
42
+
43
+ const [isLoaded, {
44
+ setTrue: setLoaded,
45
+ }] = useBoolean(false)
46
+
47
+ const [tracingStatus, setTracingStatus] = useState<TracingStatus | null>(null)
48
+ const enabled = tracingStatus?.enabled || false
49
+ const handleTracingStatusChange = async (tracingStatus: TracingStatus, noToast?: boolean) => {
50
+ await updateTracingStatus({ appId, body: tracingStatus })
51
+ setTracingStatus(tracingStatus)
52
+ if (!noToast) {
53
+ Toast.notify({
54
+ type: 'success',
55
+ message: t('common.api.success'),
56
+ })
57
+ }
58
+ }
59
+
60
+ const handleTracingEnabledChange = (enabled: boolean) => {
61
+ handleTracingStatusChange({
62
+ tracing_provider: tracingStatus?.tracing_provider || null,
63
+ enabled,
64
+ })
65
+ }
66
+ const handleChooseProvider = (provider: TracingProvider) => {
67
+ handleTracingStatusChange({
68
+ tracing_provider: provider,
69
+ enabled: true,
70
+ })
71
+ }
72
+ const inUseTracingProvider: TracingProvider | null = tracingStatus?.tracing_provider || null
73
+ const InUseProviderIcon = inUseTracingProvider === TracingProvider.langSmith ? LangsmithIcon : LangfuseIcon
74
+
75
+ const [langSmithConfig, setLangSmithConfig] = useState<LangSmithConfig | null>(null)
76
+ const [langFuseConfig, setLangFuseConfig] = useState<LangFuseConfig | null>(null)
77
+ const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig)
78
+
79
+ const fetchTracingConfig = async () => {
80
+ const { tracing_config: langSmithConfig, has_not_configured: langSmithHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langSmith })
81
+ if (!langSmithHasNotConfig)
82
+ setLangSmithConfig(langSmithConfig as LangSmithConfig)
83
+ const { tracing_config: langFuseConfig, has_not_configured: langFuseHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langfuse })
84
+ if (!langFuseHasNotConfig)
85
+ setLangFuseConfig(langFuseConfig as LangFuseConfig)
86
+ }
87
+
88
+ const handleTracingConfigUpdated = async (provider: TracingProvider) => {
89
+ // call api to hide secret key value
90
+ const { tracing_config } = await doFetchTracingConfig({ appId, provider })
91
+ if (provider === TracingProvider.langSmith)
92
+ setLangSmithConfig(tracing_config as LangSmithConfig)
93
+ else
94
+ setLangFuseConfig(tracing_config as LangFuseConfig)
95
+ }
96
+
97
+ const handleTracingConfigRemoved = (provider: TracingProvider) => {
98
+ if (provider === TracingProvider.langSmith)
99
+ setLangSmithConfig(null)
100
+ else
101
+ setLangFuseConfig(null)
102
+ if (provider === inUseTracingProvider) {
103
+ handleTracingStatusChange({
104
+ enabled: false,
105
+ tracing_provider: null,
106
+ }, true)
107
+ }
108
+ }
109
+
110
+ useEffect(() => {
111
+ (async () => {
112
+ const tracingStatus = await fetchTracingStatus({ appId })
113
+ setTracingStatus(tracingStatus)
114
+ await fetchTracingConfig()
115
+ setLoaded()
116
+ })()
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
118
+ }, [])
119
+
120
+ const [controlShowPopup, setControlShowPopup] = useState<number>(0)
121
+ const showPopup = useCallback(() => {
122
+ setControlShowPopup(Date.now())
123
+ }, [setControlShowPopup])
124
+ if (!isLoaded) {
125
+ return (
126
+ <div className='flex items-center justify-between mb-3'>
127
+ <Title className='h-[41px]' />
128
+ <div className='w-[200px]'>
129
+ <Loading />
130
+ </div>
131
+ </div>
132
+ )
133
+ }
134
+
135
+ return (
136
+ <div className={cn('mb-3 flex justify-between items-center')}>
137
+ <Title className='h-[41px]' />
138
+ <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}>
139
+ {!inUseTracingProvider
140
+ ? <>
141
+ <TracingIcon size='md' className='mr-2' />
142
+ <div className='leading-5 text-sm font-semibold text-gray-700'>{t(`${I18N_PREFIX}.title`)}</div>
143
+ </>
144
+ : <InUseProviderIcon className='ml-1 h-4' />}
145
+
146
+ {hasConfiguredTracing && (
147
+ <div className='ml-4 mr-1 flex items-center'>
148
+ <Indicator color={enabled ? 'green' : 'gray'} />
149
+ <div className='ml-1.5 text-xs font-semibold text-gray-500 uppercase'>
150
+ {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
151
+ </div>
152
+ </div>
153
+ )}
154
+
155
+ {hasConfiguredTracing && (
156
+ <div className='ml-2 w-px h-3.5 bg-gray-200'></div>
157
+ )}
158
+ <div className='flex items-center' onClick={e => e.stopPropagation()}>
159
+ <ConfigButton
160
+ appId={appId}
161
+ readOnly={readOnly}
162
+ hasConfigured
163
+ className='ml-2'
164
+ enabled={enabled}
165
+ onStatusChange={handleTracingEnabledChange}
166
+ chosenProvider={inUseTracingProvider}
167
+ onChooseProvider={handleChooseProvider}
168
+ langSmithConfig={langSmithConfig}
169
+ langFuseConfig={langFuseConfig}
170
+ onConfigUpdated={handleTracingConfigUpdated}
171
+ onConfigRemoved={handleTracingConfigRemoved}
172
+ controlShowPopup={controlShowPopup}
173
+ />
174
+ </div>
175
+ </div>
176
+ </div>
177
+ )
178
+ }
179
+ export default React.memo(Panel)
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import React, { useCallback, useState } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { useBoolean } from 'ahooks'
6
+ import Field from './field'
7
+ import type { LangFuseConfig, LangSmithConfig } from './type'
8
+ import { TracingProvider } from './type'
9
+ import { docURL } from './config'
10
+ import {
11
+ PortalToFollowElem,
12
+ PortalToFollowElemContent,
13
+ } from '@/app/components/base/portal-to-follow-elem'
14
+ import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
15
+ import Button from '@/app/components/base/button'
16
+ import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
17
+ import Confirm from '@/app/components/base/confirm'
18
+ import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
19
+ import Toast from '@/app/components/base/toast'
20
+
21
+ type Props = {
22
+ appId: string
23
+ type: TracingProvider
24
+ payload?: LangSmithConfig | LangFuseConfig | null
25
+ onRemoved: () => void
26
+ onCancel: () => void
27
+ onSaved: (payload: LangSmithConfig | LangFuseConfig) => void
28
+ onChosen: (provider: TracingProvider) => void
29
+ }
30
+
31
+ const I18N_PREFIX = 'app.tracing.configProvider'
32
+
33
+ const langSmithConfigTemplate = {
34
+ api_key: '',
35
+ project: '',
36
+ endpoint: '',
37
+ }
38
+
39
+ const langFuseConfigTemplate = {
40
+ public_key: '',
41
+ secret_key: '',
42
+ host: '',
43
+ }
44
+
45
+ const ProviderConfigModal: FC<Props> = ({
46
+ appId,
47
+ type,
48
+ payload,
49
+ onRemoved,
50
+ onCancel,
51
+ onSaved,
52
+ onChosen,
53
+ }) => {
54
+ const { t } = useTranslation()
55
+ const isEdit = !!payload
56
+ const isAdd = !isEdit
57
+ const [isSaving, setIsSaving] = useState(false)
58
+ const [config, setConfig] = useState<LangSmithConfig | LangFuseConfig>((() => {
59
+ if (isEdit)
60
+ return payload
61
+
62
+ if (type === TracingProvider.langSmith)
63
+ return langSmithConfigTemplate
64
+
65
+ return langFuseConfigTemplate
66
+ })())
67
+ const [isShowRemoveConfirm, {
68
+ setTrue: showRemoveConfirm,
69
+ setFalse: hideRemoveConfirm,
70
+ }] = useBoolean(false)
71
+
72
+ const handleRemove = useCallback(async () => {
73
+ await removeTracingConfig({
74
+ appId,
75
+ provider: type,
76
+ })
77
+ Toast.notify({
78
+ type: 'success',
79
+ message: t('common.api.remove'),
80
+ })
81
+ onRemoved()
82
+ hideRemoveConfirm()
83
+ }, [hideRemoveConfirm, appId, type, t, onRemoved])
84
+
85
+ const handleConfigChange = useCallback((key: string) => {
86
+ return (value: string) => {
87
+ setConfig({
88
+ ...config,
89
+ [key]: value,
90
+ })
91
+ }
92
+ }, [config])
93
+
94
+ const checkValid = useCallback(() => {
95
+ let errorMessage = ''
96
+ if (type === TracingProvider.langSmith) {
97
+ const postData = config as LangSmithConfig
98
+ if (!postData.api_key)
99
+ errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
100
+ if (!errorMessage && !postData.project)
101
+ errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
102
+ }
103
+
104
+ if (type === TracingProvider.langfuse) {
105
+ const postData = config as LangFuseConfig
106
+ if (!errorMessage && !postData.secret_key)
107
+ errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.secretKey`) })
108
+ if (!errorMessage && !postData.public_key)
109
+ errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.publicKey`) })
110
+ if (!errorMessage && !postData.host)
111
+ errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' })
112
+ }
113
+
114
+ return errorMessage
115
+ }, [config, t, type])
116
+ const handleSave = useCallback(async () => {
117
+ if (isSaving)
118
+ return
119
+ const errorMessage = checkValid()
120
+ if (errorMessage) {
121
+ Toast.notify({
122
+ type: 'error',
123
+ message: errorMessage,
124
+ })
125
+ return
126
+ }
127
+ const action = isEdit ? updateTracingConfig : addTracingConfig
128
+ try {
129
+ await action({
130
+ appId,
131
+ body: {
132
+ tracing_provider: type,
133
+ tracing_config: config,
134
+ },
135
+ })
136
+ Toast.notify({
137
+ type: 'success',
138
+ message: t('common.api.success'),
139
+ })
140
+ onSaved(config)
141
+ if (isAdd)
142
+ onChosen(type)
143
+ }
144
+ finally {
145
+ setIsSaving(false)
146
+ }
147
+ }, [appId, checkValid, config, isAdd, isEdit, isSaving, onChosen, onSaved, t, type])
148
+
149
+ return (
150
+ <>
151
+ {!isShowRemoveConfirm
152
+ ? (
153
+ <PortalToFollowElem open>
154
+ <PortalToFollowElemContent className='w-full h-full z-[60]'>
155
+ <div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
156
+ <div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
157
+ <div className='px-8 pt-8'>
158
+ <div className='flex justify-between items-center mb-4'>
159
+ <div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
160
+ </div>
161
+
162
+ <div className='space-y-4'>
163
+ {type === TracingProvider.langSmith && (
164
+ <>
165
+ <Field
166
+ label='API Key'
167
+ labelClassName='!text-sm'
168
+ isRequired
169
+ value={(config as LangSmithConfig).api_key}
170
+ onChange={handleConfigChange('api_key')}
171
+ placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
172
+ />
173
+ <Field
174
+ label={t(`${I18N_PREFIX}.project`)!}
175
+ labelClassName='!text-sm'
176
+ isRequired
177
+ value={(config as LangSmithConfig).project}
178
+ onChange={handleConfigChange('project')}
179
+ placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
180
+ />
181
+ <Field
182
+ label='Endpoint'
183
+ labelClassName='!text-sm'
184
+ value={(config as LangSmithConfig).endpoint}
185
+ onChange={handleConfigChange('endpoint')}
186
+ placeholder={'https://api.smith.langchain.com'}
187
+ />
188
+ </>
189
+ )}
190
+ {type === TracingProvider.langfuse && (
191
+ <>
192
+ <Field
193
+ label={t(`${I18N_PREFIX}.secretKey`)!}
194
+ labelClassName='!text-sm'
195
+ value={(config as LangFuseConfig).secret_key}
196
+ isRequired
197
+ onChange={handleConfigChange('secret_key')}
198
+ placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.secretKey`) })!}
199
+ />
200
+ <Field
201
+ label={t(`${I18N_PREFIX}.publicKey`)!}
202
+ labelClassName='!text-sm'
203
+ isRequired
204
+ value={(config as LangFuseConfig).public_key}
205
+ onChange={handleConfigChange('public_key')}
206
+ placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!}
207
+ />
208
+ <Field
209
+ label='Host'
210
+ labelClassName='!text-sm'
211
+ isRequired
212
+ value={(config as LangFuseConfig).host}
213
+ onChange={handleConfigChange('host')}
214
+ placeholder='https://cloud.langfuse.com'
215
+ />
216
+ </>
217
+ )}
218
+
219
+ </div>
220
+ <div className='my-8 flex justify-between items-center h-8'>
221
+ <a
222
+ className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]'
223
+ target='_blank'
224
+ href={docURL[type]}
225
+ >
226
+ <span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
227
+ <LinkExternal02 className='w-3 h-3' />
228
+ </a>
229
+ <div className='flex items-center'>
230
+ {isEdit && (
231
+ <>
232
+ <Button
233
+ className='h-9 text-sm font-medium text-gray-700'
234
+ onClick={showRemoveConfirm}
235
+ >
236
+ <span className='text-[#D92D20]'>{t('common.operation.remove')}</span>
237
+ </Button>
238
+ <div className='mx-3 w-px h-[18px] bg-gray-200'></div>
239
+ </>
240
+ )}
241
+ <Button
242
+ className='mr-2 h-9 text-sm font-medium text-gray-700'
243
+ onClick={onCancel}
244
+ >
245
+ {t('common.operation.cancel')}
246
+ </Button>
247
+ <Button
248
+ className='h-9 text-sm font-medium'
249
+ variant='primary'
250
+ onClick={handleSave}
251
+ loading={isSaving}
252
+ >
253
+ {t(`common.operation.${isAdd ? 'saveAndEnable' : 'save'}`)}
254
+ </Button>
255
+ </div>
256
+
257
+ </div>
258
+ </div>
259
+ <div className='border-t-[0.5px] border-t-black/5'>
260
+ <div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
261
+ <Lock01 className='mr-1 w-3 h-3 text-gray-500' />
262
+ {t('common.modelProvider.encrypted.front')}
263
+ <a
264
+ className='text-primary-600 mx-1'
265
+ target='_blank' rel='noopener noreferrer'
266
+ href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
267
+ >
268
+ PKCS1_OAEP
269
+ </a>
270
+ {t('common.modelProvider.encrypted.back')}
271
+ </div>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ </PortalToFollowElemContent>
276
+ </PortalToFollowElem>
277
+ )
278
+ : (
279
+ <Confirm
280
+ isShow
281
+ type='warning'
282
+ title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!}
283
+ content={t(`${I18N_PREFIX}.removeConfirmContent`)}
284
+ onConfirm={handleRemove}
285
+ onCancel={hideRemoveConfirm}
286
+ />
287
+ )}
288
+ </>
289
+ )
290
+ }
291
+ export default React.memo(ProviderConfigModal)
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import React, { useCallback } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { TracingProvider } from './type'
6
+ import cn from '@/utils/classnames'
7
+ import { LangfuseIconBig, LangsmithIconBig } from '@/app/components/base/icons/src/public/tracing'
8
+ import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
9
+ import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general'
10
+
11
+ const I18N_PREFIX = 'app.tracing'
12
+
13
+ type Props = {
14
+ type: TracingProvider
15
+ readOnly: boolean
16
+ isChosen: boolean
17
+ config: any
18
+ onChoose: () => void
19
+ hasConfigured: boolean
20
+ onConfig: () => void
21
+ }
22
+
23
+ const getIcon = (type: TracingProvider) => {
24
+ return ({
25
+ [TracingProvider.langSmith]: LangsmithIconBig,
26
+ [TracingProvider.langfuse]: LangfuseIconBig,
27
+ })[type]
28
+ }
29
+
30
+ const ProviderPanel: FC<Props> = ({
31
+ type,
32
+ readOnly,
33
+ isChosen,
34
+ config,
35
+ onChoose,
36
+ hasConfigured,
37
+ onConfig,
38
+ }) => {
39
+ const { t } = useTranslation()
40
+ const Icon = getIcon(type)
41
+
42
+ const handleConfigBtnClick = useCallback((e: React.MouseEvent) => {
43
+ e.stopPropagation()
44
+ onConfig()
45
+ }, [onConfig])
46
+
47
+ const viewBtnClick = useCallback((e: React.MouseEvent) => {
48
+ e.preventDefault()
49
+ e.stopPropagation()
50
+
51
+ const url = config?.project_url
52
+ if (url)
53
+ window.open(url, '_blank', 'noopener,noreferrer')
54
+ }, [config?.project_url])
55
+
56
+ const handleChosen = useCallback((e: React.MouseEvent) => {
57
+ e.stopPropagation()
58
+ if (isChosen || !hasConfigured || readOnly)
59
+ return
60
+ onChoose()
61
+ }, [hasConfigured, isChosen, onChoose, readOnly])
62
+ return (
63
+ <div
64
+ 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')}
65
+ onClick={handleChosen}
66
+ >
67
+ <div className={'flex justify-between items-center space-x-1'}>
68
+ <div className='flex items-center'>
69
+ <Icon className='h-6' />
70
+ {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>}
71
+ </div>
72
+ {!readOnly && (
73
+ <div className={'flex justify-between items-center space-x-1'}>
74
+ {hasConfigured && (
75
+ <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} >
76
+ <View className='w-3 h-3'/>
77
+ <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div>
78
+ </div>
79
+ )}
80
+ <div
81
+ 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'
82
+ onClick={handleConfigBtnClick}
83
+ >
84
+ <Settings04 className='w-3 h-3' />
85
+ <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div>
86
+ </div>
87
+ </div>
88
+ )}
89
+
90
+ </div>
91
+ <div className='mt-2 leading-4 text-xs font-normal text-gray-500'>
92
+ {t(`${I18N_PREFIX}.${type}.description`)}
93
+ </div>
94
+ </div>
95
+ )
96
+ }
97
+ export default React.memo(ProviderPanel)
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import { ChevronDoubleDownIcon } from '@heroicons/react/20/solid'
3
+ import type { FC } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import React, { useCallback } from 'react'
6
+ import Tooltip from '@/app/components/base/tooltip'
7
+
8
+ const I18N_PREFIX = 'app.tracing'
9
+
10
+ type Props = {
11
+ isFold: boolean
12
+ onFoldChange: (isFold: boolean) => void
13
+ }
14
+
15
+ const ToggleFoldBtn: FC<Props> = ({
16
+ isFold,
17
+ onFoldChange,
18
+ }) => {
19
+ const { t } = useTranslation()
20
+
21
+ const handleFoldChange = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
22
+ e.stopPropagation()
23
+ onFoldChange(!isFold)
24
+ }, [isFold, onFoldChange])
25
+ return (
26
+ // text-[0px] to hide spacing between tooltip elements
27
+ <div className='shrink-0 cursor-pointer text-[0px]' onClick={handleFoldChange}>
28
+ <Tooltip
29
+ popupContent={t(`${I18N_PREFIX}.${isFold ? 'expand' : 'collapse'}`)}
30
+ >
31
+ {isFold && (
32
+ <div className='p-1 rounded-md text-gray-500 hover:text-gray-800 hover:bg-black/5'>
33
+ <ChevronDoubleDownIcon className='w-4 h-4' />
34
+ </div>
35
+ )}
36
+ {!isFold && (
37
+ <div className='p-2 rounded-lg text-gray-500 border-[0.5px] border-gray-200 hover:text-gray-800 hover:bg-black/5'>
38
+ <ChevronDoubleDownIcon className='w-4 h-4 transform rotate-180' />
39
+ </div>
40
+ )}
41
+ </Tooltip>
42
+ </div>
43
+ )
44
+ }
45
+ export default React.memo(ToggleFoldBtn)
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import React from 'react'
4
+ import cn from '@/utils/classnames'
5
+ import { TracingIcon as Icon } from '@/app/components/base/icons/src/public/tracing'
6
+
7
+ type Props = {
8
+ className?: string
9
+ size: 'lg' | 'md'
10
+ }
11
+
12
+ const sizeClassMap = {
13
+ lg: 'w-9 h-9 p-2 rounded-[10px]',
14
+ md: 'w-6 h-6 p-1 rounded-lg',
15
+ }
16
+
17
+ const TracingIcon: FC<Props> = ({
18
+ className,
19
+ size,
20
+ }) => {
21
+ const sizeClass = sizeClassMap[size]
22
+ return (
23
+ <div className={cn(className, sizeClass, 'bg-primary-500 shadow-md')}>
24
+ <Icon className='w-full h-full' />
25
+ </div>
26
+ )
27
+ }
28
+ export default React.memo(TracingIcon)
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export enum TracingProvider {
2
+ langSmith = 'langsmith',
3
+ langfuse = 'langfuse',
4
+ }
5
+
6
+ export type LangSmithConfig = {
7
+ api_key: string
8
+ project: string
9
+ endpoint: string
10
+ }
11
+
12
+ export type LangFuseConfig = {
13
+ public_key: string
14
+ secret_key: string
15
+ host: string
16
+ }
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .app {
2
+ flex-grow: 1;
3
+ height: 0;
4
+ border-radius: 16px 16px 0px 0px;
5
+ box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 2px -1px rgba(0, 0, 0, 0.03);
6
+ }
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import Workflow from '@/app/components/workflow'
4
+
5
+ const Page = () => {
6
+ return (
7
+ <div className='w-full h-full overflow-x-auto'>
8
+ <Workflow />
9
+ </div>
10
+ )
11
+ }
12
+ export default Page
web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import type { FC } from 'react'
3
+ import React, { useEffect } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useAppContext } from '@/context/app-context'
6
+
7
+ export type IAppDetail = {
8
+ children: React.ReactNode
9
+ }
10
+
11
+ const AppDetail: FC<IAppDetail> = ({ children }) => {
12
+ const router = useRouter()
13
+ const { isCurrentWorkspaceDatasetOperator } = useAppContext()
14
+
15
+ useEffect(() => {
16
+ if (isCurrentWorkspaceDatasetOperator)
17
+ return router.replace('/datasets')
18
+ }, [isCurrentWorkspaceDatasetOperator])
19
+
20
+ return (
21
+ <>
22
+ {children}
23
+ </>
24
+ )
25
+ }
26
+
27
+ export default React.memo(AppDetail)
web/app/(commonLayout)/apps/AppCard.tsx ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useContext, useContextSelector } from 'use-context-selector'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useCallback, useEffect, useState } from 'react'
6
+ import { useTranslation } from 'react-i18next'
7
+ import { RiMoreFill } from '@remixicon/react'
8
+ import s from './style.module.css'
9
+ import cn from '@/utils/classnames'
10
+ import type { App } from '@/types/app'
11
+ import Confirm from '@/app/components/base/confirm'
12
+ import { ToastContext } from '@/app/components/base/toast'
13
+ import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
14
+ import DuplicateAppModal from '@/app/components/app/duplicate-modal'
15
+ import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
16
+ import AppIcon from '@/app/components/base/app-icon'
17
+ import AppsContext, { useAppContext } from '@/context/app-context'
18
+ import type { HtmlContentProps } from '@/app/components/base/popover'
19
+ import CustomPopover from '@/app/components/base/popover'
20
+ import Divider from '@/app/components/base/divider'
21
+ import { getRedirection } from '@/utils/app-redirection'
22
+ import { useProviderContext } from '@/context/provider-context'
23
+ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
24
+ import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
25
+ import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
26
+ import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
27
+ import EditAppModal from '@/app/components/explore/create-app-modal'
28
+ import SwitchAppModal from '@/app/components/app/switch-app-modal'
29
+ import type { Tag } from '@/app/components/base/tag-management/constant'
30
+ import TagSelector from '@/app/components/base/tag-management/selector'
31
+ import type { EnvironmentVariable } from '@/app/components/workflow/types'
32
+ import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
33
+ import { fetchWorkflowDraft } from '@/service/workflow'
34
+
35
+ export type AppCardProps = {
36
+ app: App
37
+ onRefresh?: () => void
38
+ }
39
+
40
+ const AppCard = ({ app, onRefresh }: AppCardProps) => {
41
+ const { t } = useTranslation()
42
+ const { notify } = useContext(ToastContext)
43
+ const { isCurrentWorkspaceEditor } = useAppContext()
44
+ const { onPlanInfoChanged } = useProviderContext()
45
+ const { push } = useRouter()
46
+
47
+ const mutateApps = useContextSelector(
48
+ AppsContext,
49
+ state => state.mutateApps,
50
+ )
51
+
52
+ const [showEditModal, setShowEditModal] = useState(false)
53
+ const [showDuplicateModal, setShowDuplicateModal] = useState(false)
54
+ const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
55
+ const [showConfirmDelete, setShowConfirmDelete] = useState(false)
56
+ const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
57
+
58
+ const onConfirmDelete = useCallback(async () => {
59
+ try {
60
+ await deleteApp(app.id)
61
+ notify({ type: 'success', message: t('app.appDeleted') })
62
+ if (onRefresh)
63
+ onRefresh()
64
+ mutateApps()
65
+ onPlanInfoChanged()
66
+ }
67
+ catch (e: any) {
68
+ notify({
69
+ type: 'error',
70
+ message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
71
+ })
72
+ }
73
+ setShowConfirmDelete(false)
74
+ }, [app.id])
75
+
76
+ const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
77
+ name,
78
+ icon_type,
79
+ icon,
80
+ icon_background,
81
+ description,
82
+ use_icon_as_answer_icon,
83
+ }) => {
84
+ try {
85
+ await updateAppInfo({
86
+ appID: app.id,
87
+ name,
88
+ icon_type,
89
+ icon,
90
+ icon_background,
91
+ description,
92
+ use_icon_as_answer_icon,
93
+ })
94
+ setShowEditModal(false)
95
+ notify({
96
+ type: 'success',
97
+ message: t('app.editDone'),
98
+ })
99
+ if (onRefresh)
100
+ onRefresh()
101
+ mutateApps()
102
+ }
103
+ catch (e) {
104
+ notify({ type: 'error', message: t('app.editFailed') })
105
+ }
106
+ }, [app.id, mutateApps, notify, onRefresh, t])
107
+
108
+ const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => {
109
+ try {
110
+ const newApp = await copyApp({
111
+ appID: app.id,
112
+ name,
113
+ icon_type,
114
+ icon,
115
+ icon_background,
116
+ mode: app.mode,
117
+ })
118
+ setShowDuplicateModal(false)
119
+ notify({
120
+ type: 'success',
121
+ message: t('app.newApp.appCreated'),
122
+ })
123
+ localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
124
+ if (onRefresh)
125
+ onRefresh()
126
+ mutateApps()
127
+ onPlanInfoChanged()
128
+ getRedirection(isCurrentWorkspaceEditor, newApp, push)
129
+ }
130
+ catch (e) {
131
+ notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
132
+ }
133
+ }
134
+
135
+ const onExport = async (include = false) => {
136
+ try {
137
+ const { data } = await exportAppConfig({
138
+ appID: app.id,
139
+ include,
140
+ })
141
+ const a = document.createElement('a')
142
+ const file = new Blob([data], { type: 'application/yaml' })
143
+ a.href = URL.createObjectURL(file)
144
+ a.download = `${app.name}.yml`
145
+ a.click()
146
+ }
147
+ catch (e) {
148
+ notify({ type: 'error', message: t('app.exportFailed') })
149
+ }
150
+ }
151
+
152
+ const exportCheck = async () => {
153
+ if (app.mode !== 'workflow' && app.mode !== 'advanced-chat') {
154
+ onExport()
155
+ return
156
+ }
157
+ try {
158
+ const workflowDraft = await fetchWorkflowDraft(`/apps/${app.id}/workflows/draft`)
159
+ const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
160
+ if (list.length === 0) {
161
+ onExport()
162
+ return
163
+ }
164
+ setSecretEnvList(list)
165
+ }
166
+ catch (e) {
167
+ notify({ type: 'error', message: t('app.exportFailed') })
168
+ }
169
+ }
170
+
171
+ const onSwitch = () => {
172
+ if (onRefresh)
173
+ onRefresh()
174
+ mutateApps()
175
+ setShowSwitchModal(false)
176
+ }
177
+
178
+ const Operations = (props: HtmlContentProps) => {
179
+ const onMouseLeave = async () => {
180
+ props.onClose?.()
181
+ }
182
+ const onClickSettings = async (e: React.MouseEvent<HTMLButtonElement>) => {
183
+ e.stopPropagation()
184
+ props.onClick?.()
185
+ e.preventDefault()
186
+ setShowEditModal(true)
187
+ }
188
+ const onClickDuplicate = async (e: React.MouseEvent<HTMLButtonElement>) => {
189
+ e.stopPropagation()
190
+ props.onClick?.()
191
+ e.preventDefault()
192
+ setShowDuplicateModal(true)
193
+ }
194
+ const onClickExport = async (e: React.MouseEvent<HTMLButtonElement>) => {
195
+ e.stopPropagation()
196
+ props.onClick?.()
197
+ e.preventDefault()
198
+ exportCheck()
199
+ }
200
+ const onClickSwitch = async (e: React.MouseEvent<HTMLDivElement>) => {
201
+ e.stopPropagation()
202
+ props.onClick?.()
203
+ e.preventDefault()
204
+ setShowSwitchModal(true)
205
+ }
206
+ const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
207
+ e.stopPropagation()
208
+ props.onClick?.()
209
+ e.preventDefault()
210
+ setShowConfirmDelete(true)
211
+ }
212
+ return (
213
+ <div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
214
+ <button className={s.actionItem} onClick={onClickSettings}>
215
+ <span className={s.actionName}>{t('app.editApp')}</span>
216
+ </button>
217
+ <Divider className="!my-1" />
218
+ <button className={s.actionItem} onClick={onClickDuplicate}>
219
+ <span className={s.actionName}>{t('app.duplicate')}</span>
220
+ </button>
221
+ <button className={s.actionItem} onClick={onClickExport}>
222
+ <span className={s.actionName}>{t('app.export')}</span>
223
+ </button>
224
+ {(app.mode === 'completion' || app.mode === 'chat') && (
225
+ <>
226
+ <Divider className="!my-1" />
227
+ <div
228
+ className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
229
+ onClick={onClickSwitch}
230
+ >
231
+ <span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
232
+ </div>
233
+ </>
234
+ )}
235
+ <Divider className="!my-1" />
236
+ <div
237
+ className={cn(s.actionItem, s.deleteActionItem, 'group')}
238
+ onClick={onClickDelete}
239
+ >
240
+ <span className={cn(s.actionName, 'group-hover:text-red-500')}>
241
+ {t('common.operation.delete')}
242
+ </span>
243
+ </div>
244
+ </div>
245
+ )
246
+ }
247
+
248
+ const [tags, setTags] = useState<Tag[]>(app.tags)
249
+ useEffect(() => {
250
+ setTags(app.tags)
251
+ }, [app.tags])
252
+
253
+ return (
254
+ <>
255
+ <div
256
+ onClick={(e) => {
257
+ e.preventDefault()
258
+ getRedirection(isCurrentWorkspaceEditor, app, push)
259
+ }}
260
+ 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'
261
+ >
262
+ <div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
263
+ <div className='relative shrink-0'>
264
+ <AppIcon
265
+ size="large"
266
+ iconType={app.icon_type}
267
+ icon={app.icon}
268
+ background={app.icon_background}
269
+ imageUrl={app.icon_url}
270
+ />
271
+ <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'>
272
+ {app.mode === 'advanced-chat' && (
273
+ <ChatBot className='w-3 h-3 text-[#1570EF]' />
274
+ )}
275
+ {app.mode === 'agent-chat' && (
276
+ <CuteRobot className='w-3 h-3 text-indigo-600' />
277
+ )}
278
+ {app.mode === 'chat' && (
279
+ <ChatBot className='w-3 h-3 text-[#1570EF]' />
280
+ )}
281
+ {app.mode === 'completion' && (
282
+ <AiText className='w-3 h-3 text-[#0E9384]' />
283
+ )}
284
+ {app.mode === 'workflow' && (
285
+ <Route className='w-3 h-3 text-[#f79009]' />
286
+ )}
287
+ </span>
288
+ </div>
289
+ <div className='grow w-0 py-[1px]'>
290
+ <div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
291
+ <div className='truncate' title={app.name}>{app.name}</div>
292
+ </div>
293
+ <div className='flex items-center text-[10px] leading-[18px] text-gray-500 font-medium'>
294
+ {app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
295
+ {app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
296
+ {app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
297
+ {app.mode === 'workflow' && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
298
+ {app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
299
+ </div>
300
+ </div>
301
+ </div>
302
+ <div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-gray-500'>
303
+ <div
304
+ className={cn(tags.length ? 'line-clamp-2' : 'line-clamp-4', 'group-hover:line-clamp-2')}
305
+ title={app.description}
306
+ >
307
+ {app.description}
308
+ </div>
309
+ </div>
310
+ <div className={cn(
311
+ 'absolute bottom-1 left-0 right-0 items-center shrink-0 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
312
+ tags.length ? 'flex' : '!hidden group-hover:!flex',
313
+ )}>
314
+ {isCurrentWorkspaceEditor && (
315
+ <>
316
+ <div className={cn('grow flex items-center gap-1 w-0')} onClick={(e) => {
317
+ e.stopPropagation()
318
+ e.preventDefault()
319
+ }}>
320
+ <div className={cn(
321
+ 'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
322
+ tags.length ? '!block' : '!hidden',
323
+ )}>
324
+ <TagSelector
325
+ position='bl'
326
+ type='app'
327
+ targetID={app.id}
328
+ value={tags.map(tag => tag.id)}
329
+ selectedTags={tags}
330
+ onCacheUpdate={setTags}
331
+ onChange={onRefresh}
332
+ />
333
+ </div>
334
+ </div>
335
+ <div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200' />
336
+ <div className='!hidden group-hover:!flex shrink-0'>
337
+ <CustomPopover
338
+ htmlContent={<Operations />}
339
+ position="br"
340
+ trigger="click"
341
+ btnElement={
342
+ <div
343
+ className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
344
+ >
345
+ <RiMoreFill className='w-4 h-4 text-gray-700' />
346
+ </div>
347
+ }
348
+ btnClassName={open =>
349
+ cn(
350
+ open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
351
+ 'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
352
+ )
353
+ }
354
+ popupClassName={
355
+ (app.mode === 'completion' || app.mode === 'chat')
356
+ ? '!w-[238px] translate-x-[-110px]'
357
+ : ''
358
+ }
359
+ className={'!w-[128px] h-fit !z-20'}
360
+ />
361
+ </div>
362
+ </>
363
+ )}
364
+ </div>
365
+ </div>
366
+ {showEditModal && (
367
+ <EditAppModal
368
+ isEditModal
369
+ appName={app.name}
370
+ appIconType={app.icon_type}
371
+ appIcon={app.icon}
372
+ appIconBackground={app.icon_background}
373
+ appIconUrl={app.icon_url}
374
+ appDescription={app.description}
375
+ appMode={app.mode}
376
+ appUseIconAsAnswerIcon={app.use_icon_as_answer_icon}
377
+ show={showEditModal}
378
+ onConfirm={onEdit}
379
+ onHide={() => setShowEditModal(false)}
380
+ />
381
+ )}
382
+ {showDuplicateModal && (
383
+ <DuplicateAppModal
384
+ appName={app.name}
385
+ icon_type={app.icon_type}
386
+ icon={app.icon}
387
+ icon_background={app.icon_background}
388
+ icon_url={app.icon_url}
389
+ show={showDuplicateModal}
390
+ onConfirm={onCopy}
391
+ onHide={() => setShowDuplicateModal(false)}
392
+ />
393
+ )}
394
+ {showSwitchModal && (
395
+ <SwitchAppModal
396
+ show={showSwitchModal}
397
+ appDetail={app}
398
+ onClose={() => setShowSwitchModal(false)}
399
+ onSuccess={onSwitch}
400
+ />
401
+ )}
402
+ {showConfirmDelete && (
403
+ <Confirm
404
+ title={t('app.deleteAppConfirmTitle')}
405
+ content={t('app.deleteAppConfirmContent')}
406
+ isShow={showConfirmDelete}
407
+ onConfirm={onConfirmDelete}
408
+ onCancel={() => setShowConfirmDelete(false)}
409
+ />
410
+ )}
411
+ {secretEnvList.length > 0 && (
412
+ <DSLExportConfirmModal
413
+ envList={secretEnvList}
414
+ onConfirm={onExport}
415
+ onClose={() => setSecretEnvList([])}
416
+ />
417
+ )}
418
+ </>
419
+ )
420
+ }
421
+
422
+ export default AppCard
web/app/(commonLayout)/apps/Apps.tsx ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import useSWRInfinite from 'swr/infinite'
6
+ import { useTranslation } from 'react-i18next'
7
+ import { useDebounceFn } from 'ahooks'
8
+ import {
9
+ RiApps2Line,
10
+ RiExchange2Line,
11
+ RiMessage3Line,
12
+ RiRobot3Line,
13
+ } from '@remixicon/react'
14
+ import AppCard from './AppCard'
15
+ import NewAppCard from './NewAppCard'
16
+ import useAppsQueryState from './hooks/useAppsQueryState'
17
+ import type { AppListResponse } from '@/models/app'
18
+ import { fetchAppList } from '@/service/apps'
19
+ import { useAppContext } from '@/context/app-context'
20
+ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
21
+ import { CheckModal } from '@/hooks/use-pay'
22
+ import TabSliderNew from '@/app/components/base/tab-slider-new'
23
+ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
24
+ import Input from '@/app/components/base/input'
25
+ import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
26
+ import TagManagementModal from '@/app/components/base/tag-management'
27
+ import TagFilter from '@/app/components/base/tag-management/filter'
28
+
29
+ const getKey = (
30
+ pageIndex: number,
31
+ previousPageData: AppListResponse,
32
+ activeTab: string,
33
+ tags: string[],
34
+ keywords: string,
35
+ ) => {
36
+ if (!pageIndex || previousPageData.has_more) {
37
+ const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords } }
38
+
39
+ if (activeTab !== 'all')
40
+ params.params.mode = activeTab
41
+ else
42
+ delete params.params.mode
43
+
44
+ if (tags.length)
45
+ params.params.tag_ids = tags
46
+
47
+ return params
48
+ }
49
+ return null
50
+ }
51
+
52
+ const Apps = () => {
53
+ const { t } = useTranslation()
54
+ const router = useRouter()
55
+ const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
56
+ const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
57
+ const [activeTab, setActiveTab] = useTabSearchParams({
58
+ defaultTab: 'all',
59
+ })
60
+ const { query: { tagIDs = [], keywords = '' }, setQuery } = useAppsQueryState()
61
+ const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs)
62
+ const [searchKeywords, setSearchKeywords] = useState(keywords)
63
+ const setKeywords = useCallback((keywords: string) => {
64
+ setQuery(prev => ({ ...prev, keywords }))
65
+ }, [setQuery])
66
+ const setTagIDs = useCallback((tagIDs: string[]) => {
67
+ setQuery(prev => ({ ...prev, tagIDs }))
68
+ }, [setQuery])
69
+
70
+ const { data, isLoading, setSize, mutate } = useSWRInfinite(
71
+ (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, tagIDs, searchKeywords),
72
+ fetchAppList,
73
+ { revalidateFirstPage: true },
74
+ )
75
+
76
+ const anchorRef = useRef<HTMLDivElement>(null)
77
+ const options = [
78
+ { value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='w-[14px] h-[14px] mr-1' /> },
79
+ { value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='w-[14px] h-[14px] mr-1' /> },
80
+ { value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='w-[14px] h-[14px] mr-1' /> },
81
+ { value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='w-[14px] h-[14px] mr-1' /> },
82
+ ]
83
+
84
+ useEffect(() => {
85
+ document.title = `${t('common.menus.apps')} - Dify`
86
+ if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
87
+ localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
88
+ mutate()
89
+ }
90
+ }, [mutate, t])
91
+
92
+ useEffect(() => {
93
+ if (isCurrentWorkspaceDatasetOperator)
94
+ return router.replace('/datasets')
95
+ }, [router, isCurrentWorkspaceDatasetOperator])
96
+
97
+ useEffect(() => {
98
+ const hasMore = data?.at(-1)?.has_more ?? true
99
+ let observer: IntersectionObserver | undefined
100
+ if (anchorRef.current) {
101
+ observer = new IntersectionObserver((entries) => {
102
+ if (entries[0].isIntersecting && !isLoading && hasMore)
103
+ setSize((size: number) => size + 1)
104
+ }, { rootMargin: '100px' })
105
+ observer.observe(anchorRef.current)
106
+ }
107
+ return () => observer?.disconnect()
108
+ }, [isLoading, setSize, anchorRef, mutate, data])
109
+
110
+ const { run: handleSearch } = useDebounceFn(() => {
111
+ setSearchKeywords(keywords)
112
+ }, { wait: 500 })
113
+ const handleKeywordsChange = (value: string) => {
114
+ setKeywords(value)
115
+ handleSearch()
116
+ }
117
+
118
+ const { run: handleTagsUpdate } = useDebounceFn(() => {
119
+ setTagIDs(tagFilterValue)
120
+ }, { wait: 500 })
121
+ const handleTagsChange = (value: string[]) => {
122
+ setTagFilterValue(value)
123
+ handleTagsUpdate()
124
+ }
125
+
126
+ return (
127
+ <>
128
+ <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'>
129
+ <TabSliderNew
130
+ value={activeTab}
131
+ onChange={setActiveTab}
132
+ options={options}
133
+ />
134
+ <div className='flex items-center gap-2'>
135
+ <TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} />
136
+ <Input
137
+ showLeftIcon
138
+ showClearIcon
139
+ wrapperClassName='w-[200px]'
140
+ value={keywords}
141
+ onChange={e => handleKeywordsChange(e.target.value)}
142
+ onClear={() => handleKeywordsChange('')}
143
+ />
144
+ </div>
145
+ </div>
146
+ <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'>
147
+ {isCurrentWorkspaceEditor
148
+ && <NewAppCard onSuccess={mutate} />}
149
+ {data?.map(({ data: apps }) => apps.map(app => (
150
+ <AppCard key={app.id} app={app} onRefresh={mutate} />
151
+ )))}
152
+ <CheckModal />
153
+ </nav>
154
+ <div ref={anchorRef} className='h-0'> </div>
155
+ {showTagManagementModal && (
156
+ <TagManagementModal type='app' show={showTagManagementModal} />
157
+ )}
158
+ </>
159
+ )
160
+ }
161
+
162
+ export default Apps
web/app/(commonLayout)/apps/NewAppCard.tsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { forwardRef, useMemo, useState } from 'react'
4
+ import {
5
+ useRouter,
6
+ useSearchParams,
7
+ } from 'next/navigation'
8
+ import { useTranslation } from 'react-i18next'
9
+ import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
10
+ import CreateAppModal from '@/app/components/app/create-app-modal'
11
+ import CreateFromDSLModal, { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
12
+ import { useProviderContext } from '@/context/provider-context'
13
+ import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
14
+
15
+ export type CreateAppCardProps = {
16
+ onSuccess?: () => void
17
+ }
18
+
19
+ // eslint-disable-next-line react/display-name
20
+ const CreateAppCard = forwardRef<HTMLAnchorElement, CreateAppCardProps>(({ onSuccess }, ref) => {
21
+ const { t } = useTranslation()
22
+ const { onPlanInfoChanged } = useProviderContext()
23
+ const searchParams = useSearchParams()
24
+ const { replace } = useRouter()
25
+ const dslUrl = searchParams.get('remoteInstallUrl') || undefined
26
+
27
+ const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
28
+ const [showNewAppModal, setShowNewAppModal] = useState(false)
29
+ const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(!!dslUrl)
30
+
31
+ const activeTab = useMemo(() => {
32
+ if (dslUrl)
33
+ return CreateFromDSLModalTab.FROM_URL
34
+
35
+ return undefined
36
+ }, [dslUrl])
37
+
38
+ return (
39
+ <a
40
+ ref={ref}
41
+ className='relative col-span-1 flex flex-col justify-between min-h-[160px] bg-gray-200 rounded-xl border-[0.5px] border-black/5'
42
+ >
43
+ <div className='grow p-2 rounded-t-xl'>
44
+ <div className='px-6 pt-2 pb-1 text-xs font-medium leading-[18px] text-gray-500'>{t('app.createApp')}</div>
45
+ <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)}>
46
+ <FilePlus01 className='shrink-0 mr-2 w-4 h-4' />
47
+ {t('app.newApp.startFromBlank')}
48
+ </div>
49
+ <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)}>
50
+ <FilePlus02 className='shrink-0 mr-2 w-4 h-4' />
51
+ {t('app.newApp.startFromTemplate')}
52
+ </div>
53
+ </div>
54
+ <div
55
+ className='p-2 border-t-[0.5px] border-black/5 rounded-b-xl'
56
+ onClick={() => setShowCreateFromDSLModal(true)}
57
+ >
58
+ <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'>
59
+ <FileArrow01 className='shrink-0 mr-2 w-4 h-4' />
60
+ {t('app.importDSL')}
61
+ </div>
62
+ </div>
63
+ <CreateAppModal
64
+ show={showNewAppModal}
65
+ onClose={() => setShowNewAppModal(false)}
66
+ onSuccess={() => {
67
+ onPlanInfoChanged()
68
+ if (onSuccess)
69
+ onSuccess()
70
+ }}
71
+ />
72
+ <CreateAppTemplateDialog
73
+ show={showNewAppTemplateDialog}
74
+ onClose={() => setShowNewAppTemplateDialog(false)}
75
+ onSuccess={() => {
76
+ onPlanInfoChanged()
77
+ if (onSuccess)
78
+ onSuccess()
79
+ }}
80
+ />
81
+ <CreateFromDSLModal
82
+ show={showCreateFromDSLModal}
83
+ onClose={() => {
84
+ setShowCreateFromDSLModal(false)
85
+
86
+ if (dslUrl)
87
+ replace('/')
88
+ }}
89
+ activeTab={activeTab}
90
+ dslUrl={dslUrl}
91
+ onSuccess={() => {
92
+ onPlanInfoChanged()
93
+ if (onSuccess)
94
+ onSuccess()
95
+ }}
96
+ />
97
+ </a>
98
+ )
99
+ })
100
+
101
+ export default CreateAppCard
web/app/(commonLayout)/apps/assets/add.svg ADDED
web/app/(commonLayout)/apps/assets/chat-solid.svg ADDED
web/app/(commonLayout)/apps/assets/chat.svg ADDED
web/app/(commonLayout)/apps/assets/completion-solid.svg ADDED
web/app/(commonLayout)/apps/assets/completion.svg ADDED
web/app/(commonLayout)/apps/assets/discord.svg ADDED
web/app/(commonLayout)/apps/assets/github.svg ADDED
web/app/(commonLayout)/apps/assets/link-gray.svg ADDED
web/app/(commonLayout)/apps/assets/link.svg ADDED
web/app/(commonLayout)/apps/assets/right-arrow.svg ADDED
web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type ReadonlyURLSearchParams, usePathname, useRouter, useSearchParams } from 'next/navigation'
2
+ import { useCallback, useEffect, useMemo, useState } from 'react'
3
+
4
+ type AppsQuery = {
5
+ tagIDs?: string[]
6
+ keywords?: string
7
+ }
8
+
9
+ // Parse the query parameters from the URL search string.
10
+ function parseParams(params: ReadonlyURLSearchParams): AppsQuery {
11
+ const tagIDs = params.get('tagIDs')?.split(';')
12
+ const keywords = params.get('keywords') || undefined
13
+ return { tagIDs, keywords }
14
+ }
15
+
16
+ // Update the URL search string with the given query parameters.
17
+ function updateSearchParams(query: AppsQuery, current: URLSearchParams) {
18
+ const { tagIDs, keywords } = query || {}
19
+
20
+ if (tagIDs && tagIDs.length > 0)
21
+ current.set('tagIDs', tagIDs.join(';'))
22
+ else
23
+ current.delete('tagIDs')
24
+
25
+ if (keywords)
26
+ current.set('keywords', keywords)
27
+ else
28
+ current.delete('keywords')
29
+ }
30
+
31
+ function useAppsQueryState() {
32
+ const searchParams = useSearchParams()
33
+ const [query, setQuery] = useState<AppsQuery>(() => parseParams(searchParams))
34
+
35
+ const router = useRouter()
36
+ const pathname = usePathname()
37
+ const syncSearchParams = useCallback((params: URLSearchParams) => {
38
+ const search = params.toString()
39
+ const query = search ? `?${search}` : ''
40
+ router.push(`${pathname}${query}`)
41
+ }, [router, pathname])
42
+
43
+ // Update the URL search string whenever the query changes.
44
+ useEffect(() => {
45
+ const params = new URLSearchParams(searchParams)
46
+ updateSearchParams(query, params)
47
+ syncSearchParams(params)
48
+ }, [query, searchParams, syncSearchParams])
49
+
50
+ return useMemo(() => ({ query, setQuery }), [query])
51
+ }
52
+
53
+ export default useAppsQueryState
web/app/(commonLayout)/apps/page.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import style from '../list.module.css'
2
+ import Apps from './Apps'
3
+ import classNames from '@/utils/classnames'
4
+ import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server'
5
+
6
+ const AppList = async () => {
7
+ const locale = getLocaleOnServer()
8
+ const { t } = await translate(locale, 'app')
9
+
10
+ return (
11
+ <div className='relative flex flex-col overflow-y-auto bg-gray-100 shrink-0 h-0 grow'>
12
+ <Apps />
13
+ <footer className='px-12 py-6 grow-0 shrink-0'>
14
+ <h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3>
15
+ <p className='mt-1 text-sm font-normal leading-tight text-gray-700'>{t('communityIntro')}</p>
16
+ <div className='flex items-center gap-2 mt-3'>
17
+ <a className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://github.com/langgenius/dify'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
18
+ <a className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://discord.gg/FngNHpbcY7'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
19
+ </div>
20
+ </footer>
21
+ </div >
22
+ )
23
+ }
24
+
25
+ export default AppList
web/app/(commonLayout)/apps/style.module.css ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ .commonIcon {
3
+ @apply w-4 h-4 inline-block align-middle;
4
+ background-repeat: no-repeat;
5
+ background-position: center center;
6
+ background-size: contain;
7
+ }
8
+ .actionIcon {
9
+ @apply bg-gray-500;
10
+ mask-image: url(~@/assets/action.svg);
11
+ }
12
+ .actionItem {
13
+ @apply h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer;
14
+ width: calc(100% - 0.5rem);
15
+ }
16
+ .deleteActionItem {
17
+ @apply hover:bg-red-50 !important;
18
+ }
19
+ .actionName {
20
+ @apply text-gray-700 text-sm;
21
+ }
22
+
23
+ /* .completionPic {
24
+ background-image: url(~@/app/components/app-sidebar/completion.png)
25
+ }
26
+
27
+ .expertPic {
28
+ background-image: url(~@/app/components/app-sidebar/expert.png)
29
+ } */