Spaces:
Paused
Paused
Upload 2985 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- web/.dockerignore +24 -0
- web/.editorconfig +22 -0
- web/.env.example +27 -0
- web/.eslintignore +7 -0
- web/.eslintrc.json +31 -0
- web/.gitignore +53 -0
- web/.husky/pre-commit +82 -0
- web/.storybook/main.ts +19 -0
- web/.storybook/preview.tsx +37 -0
- web/.storybook/storybook.css +6 -0
- web/Dockerfile +72 -0
- web/__mocks__/mime.js +0 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx +15 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/configuration/page.tsx +10 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/develop/page.tsx +15 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx +161 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx +11 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx +140 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx +101 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx +24 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx +87 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx +180 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts +6 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx +41 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +179 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +291 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx +97 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx +45 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx +28 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts +16 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css +6 -0
- web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx +12 -0
- web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx +27 -0
- web/app/(commonLayout)/apps/AppCard.tsx +422 -0
- web/app/(commonLayout)/apps/Apps.tsx +162 -0
- web/app/(commonLayout)/apps/NewAppCard.tsx +101 -0
- web/app/(commonLayout)/apps/assets/add.svg +3 -0
- web/app/(commonLayout)/apps/assets/chat-solid.svg +4 -0
- web/app/(commonLayout)/apps/assets/chat.svg +3 -0
- web/app/(commonLayout)/apps/assets/completion-solid.svg +4 -0
- web/app/(commonLayout)/apps/assets/completion.svg +3 -0
- web/app/(commonLayout)/apps/assets/discord.svg +3 -0
- web/app/(commonLayout)/apps/assets/github.svg +17 -0
- web/app/(commonLayout)/apps/assets/link-gray.svg +3 -0
- web/app/(commonLayout)/apps/assets/link.svg +3 -0
- web/app/(commonLayout)/apps/assets/right-arrow.svg +3 -0
- web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts +53 -0
- web/app/(commonLayout)/apps/page.tsx +25 -0
- 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 |
+
} */
|