Commit c77029e6 authored by Jyong's avatar Jyong

Merge branch 'main' into feat/dataset-notion-import

parents ff1aadf9 f65a3ad1
#!/usr/bin/env bash
set -eo pipefail
SHA=$(git rev-parse HEAD)
REPO_NAME=langgenius/dify
API_REPO_NAME="${REPO_NAME}-api"
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
REFSPEC=$(echo "${GITHUB_HEAD_REF}" | sed 's/[^a-zA-Z0-9]/-/g' | head -c 40)
PR_NUM=$(echo "${GITHUB_REF}" | sed 's:refs/pull/::' | sed 's:/merge::')
LATEST_TAG="pr-${PR_NUM}"
CACHE_FROM_TAG="latest"
elif [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
REFSPEC=$(echo "${GITHUB_REF}" | sed 's:refs/tags/::' | head -c 40)
LATEST_TAG="${REFSPEC}"
CACHE_FROM_TAG="latest"
else
REFSPEC=$(echo "${GITHUB_REF}" | sed 's:refs/heads/::' | sed 's/[^a-zA-Z0-9]/-/g' | head -c 40)
LATEST_TAG="${REFSPEC}"
CACHE_FROM_TAG="${REFSPEC}"
fi
if [[ "${REFSPEC}" == "main" ]]; then
LATEST_TAG="latest"
CACHE_FROM_TAG="latest"
fi
echo "Pulling cache image ${API_REPO_NAME}:${CACHE_FROM_TAG}"
if docker pull "${API_REPO_NAME}:${CACHE_FROM_TAG}"; then
API_CACHE_FROM_SCRIPT="--cache-from ${API_REPO_NAME}:${CACHE_FROM_TAG}"
else
echo "WARNING: Failed to pull ${API_REPO_NAME}:${CACHE_FROM_TAG}, disable build image cache."
API_CACHE_FROM_SCRIPT=""
fi
cat<<EOF
Rolling with tags:
- ${API_REPO_NAME}:${SHA}
- ${API_REPO_NAME}:${REFSPEC}
- ${API_REPO_NAME}:${LATEST_TAG}
EOF
#
# Build image
#
cd api
docker build \
${API_CACHE_FROM_SCRIPT} \
--build-arg COMMIT_SHA=${SHA} \
-t "${API_REPO_NAME}:${SHA}" \
-t "${API_REPO_NAME}:${REFSPEC}" \
-t "${API_REPO_NAME}:${LATEST_TAG}" \
--label "sha=${SHA}" \
--label "built_at=$(date)" \
--label "build_actor=${GITHUB_ACTOR}" \
.
# push
docker push --all-tags "${API_REPO_NAME}"
......@@ -5,16 +5,19 @@ on:
branches:
- 'main'
- 'deploy/dev'
release:
types: [published]
jobs:
build-and-push:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
......@@ -22,13 +25,29 @@ jobs:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
shell: bash
env:
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
/bin/bash .github/workflows/build-api-image.sh
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: langgenius/dify-api
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: "{{defaultContext}}:api"
platforms: linux/amd64,linux/arm64
build-args: |
COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Deploy to server
if: github.ref == 'refs/heads/deploy/dev'
......
#!/usr/bin/env bash
set -eo pipefail
SHA=$(git rev-parse HEAD)
REPO_NAME=langgenius/dify
WEB_REPO_NAME="${REPO_NAME}-web"
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
REFSPEC=$(echo "${GITHUB_HEAD_REF}" | sed 's/[^a-zA-Z0-9]/-/g' | head -c 40)
PR_NUM=$(echo "${GITHUB_REF}" | sed 's:refs/pull/::' | sed 's:/merge::')
LATEST_TAG="pr-${PR_NUM}"
CACHE_FROM_TAG="latest"
elif [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
REFSPEC=$(echo "${GITHUB_REF}" | sed 's:refs/tags/::' | head -c 40)
LATEST_TAG="${REFSPEC}"
CACHE_FROM_TAG="latest"
else
REFSPEC=$(echo "${GITHUB_REF}" | sed 's:refs/heads/::' | sed 's/[^a-zA-Z0-9]/-/g' | head -c 40)
LATEST_TAG="${REFSPEC}"
CACHE_FROM_TAG="${REFSPEC}"
fi
if [[ "${REFSPEC}" == "main" ]]; then
LATEST_TAG="latest"
CACHE_FROM_TAG="latest"
fi
echo "Pulling cache image ${WEB_REPO_NAME}:${CACHE_FROM_TAG}"
if docker pull "${WEB_REPO_NAME}:${CACHE_FROM_TAG}"; then
WEB_CACHE_FROM_SCRIPT="--cache-from ${WEB_REPO_NAME}:${CACHE_FROM_TAG}"
else
echo "WARNING: Failed to pull ${WEB_REPO_NAME}:${CACHE_FROM_TAG}, disable build image cache."
WEB_CACHE_FROM_SCRIPT=""
fi
cat<<EOF
Rolling with tags:
- ${WEB_REPO_NAME}:${SHA}
- ${WEB_REPO_NAME}:${REFSPEC}
- ${WEB_REPO_NAME}:${LATEST_TAG}
EOF
#
# Build image
#
cd web
docker build \
${WEB_CACHE_FROM_SCRIPT} \
--build-arg COMMIT_SHA=${SHA} \
-t "${WEB_REPO_NAME}:${SHA}" \
-t "${WEB_REPO_NAME}:${REFSPEC}" \
-t "${WEB_REPO_NAME}:${LATEST_TAG}" \
--label "sha=${SHA}" \
--label "built_at=$(date)" \
--label "build_actor=${GITHUB_ACTOR}" \
.
docker push --all-tags "${WEB_REPO_NAME}"
\ No newline at end of file
......@@ -5,16 +5,19 @@ on:
branches:
- 'main'
- 'deploy/dev'
release:
types: [published]
jobs:
build-and-push:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
......@@ -22,13 +25,29 @@ jobs:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
shell: bash
env:
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
/bin/bash .github/workflows/build-web-image.sh
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: langgenius/dify-web
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: "{{defaultContext}}:web"
platforms: linux/amd64,linux/arm64
build-args: |
COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Deploy to server
if: github.ref == 'refs/heads/deploy/dev'
......
......@@ -130,7 +130,6 @@ dmypy.json
.idea/'
.DS_Store
.vscode
# Intellij IDEA Files
.idea/
......
import datetime
import json
import random
import string
......@@ -9,7 +8,7 @@ from libs.password import password_pattern, valid_password, hash_password
from libs.helper import email as email_validate
from extensions.ext_database import db
from models.account import InvitationCode
from models.model import Account, AppModelConfig, ApiToken, Site, App, RecommendedApp
from models.model import Account
import secrets
import base64
......@@ -131,30 +130,7 @@ def generate_upper_string():
return result
@click.command('gen-recommended-apps', help='Number of records to generate')
def generate_recommended_apps():
print('Generating recommended app data...')
apps = App.query.all()
for app in apps:
recommended_app = RecommendedApp(
app_id=app.id,
description={
'en': 'Description for ' + app.name,
'zh': '描述 ' + app.name
},
copyright='Copyright ' + str(random.randint(1990, 2020)),
privacy_policy='https://privacypolicy.example.com',
category=random.choice(['Games', 'News', 'Music', 'Sports']),
position=random.randint(1, 100),
install_count=random.randint(100, 100000)
)
db.session.add(recommended_app)
db.session.commit()
print('Done!')
def register_commands(app):
app.cli.add_command(reset_password)
app.cli.add_command(reset_email)
app.cli.add_command(generate_invitation_codes)
app.cli.add_command(generate_recommended_apps)
......@@ -78,7 +78,7 @@ class Config:
self.CONSOLE_URL = get_env('CONSOLE_URL')
self.API_URL = get_env('API_URL')
self.APP_URL = get_env('APP_URL')
self.CURRENT_VERSION = "0.2.0"
self.CURRENT_VERSION = "0.3.1"
self.COMMIT_SHA = get_env('COMMIT_SHA')
self.EDITION = "SELF_HOSTED"
self.DEPLOY_ENV = get_env('DEPLOY_ENV')
......
......@@ -5,8 +5,11 @@ from libs.external_api import ExternalApi
bp = Blueprint('console', __name__, url_prefix='/console/api')
api = ExternalApi(bp)
# Import other controllers
from . import setup, version, apikey, admin
# Import app controllers
from .app import app, site, explore, completion, model_config, statistic, conversation, message
from .app import app, site, completion, model_config, statistic, conversation, message, generator
# Import auth controllers
from .auth import login, oauth, data_source_oauth
......@@ -14,7 +17,8 @@ from .auth import login, oauth, data_source_oauth
# Import datasets controllers
from .datasets import datasets, datasets_document, datasets_segments, file, hit_testing, data_source
# Import other controllers
from . import setup, version, apikey
# Import workspace controllers
from .workspace import workspace, members, providers, account
# Import explore controllers
from .explore import installed_app, recommended_app, completion, conversation, message, parameter, saved_message
import os
from functools import wraps
from flask import request
from flask_restful import Resource, reqparse
from werkzeug.exceptions import NotFound, Unauthorized
from controllers.console import api
from controllers.console.wraps import only_edition_cloud
from extensions.ext_database import db
from models.model import RecommendedApp, App, InstalledApp
def admin_required(view):
@wraps(view)
def decorated(*args, **kwargs):
if not os.getenv('ADMIN_API_KEY'):
raise Unauthorized('API key is invalid.')
auth_header = request.headers.get('Authorization')
if auth_header is None:
raise Unauthorized('Authorization header is missing.')
if ' ' not in auth_header:
raise Unauthorized('Invalid Authorization header format. Expected \'Bearer <api-key>\' format.')
auth_scheme, auth_token = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower()
if auth_scheme != 'bearer':
raise Unauthorized('Invalid Authorization header format. Expected \'Bearer <api-key>\' format.')
if os.getenv('ADMIN_API_KEY') != auth_token:
raise Unauthorized('API key is invalid.')
return view(*args, **kwargs)
return decorated
class InsertExploreAppListApi(Resource):
@only_edition_cloud
@admin_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('app_id', type=str, required=True, nullable=False, location='json')
parser.add_argument('desc', type=str, location='json')
parser.add_argument('copyright', type=str, location='json')
parser.add_argument('privacy_policy', type=str, location='json')
parser.add_argument('language', type=str, required=True, nullable=False, choices=['en-US', 'zh-Hans'],
location='json')
parser.add_argument('category', type=str, required=True, nullable=False, location='json')
parser.add_argument('position', type=int, required=True, nullable=False, location='json')
args = parser.parse_args()
app = App.query.filter(App.id == args['app_id']).first()
if not app:
raise NotFound('App not found')
site = app.site
if not site:
desc = args['desc'] if args['desc'] else ''
copy_right = args['copyright'] if args['copyright'] else ''
privacy_policy = args['privacy_policy'] if args['privacy_policy'] else ''
else:
desc = site.description if (site.description if not args['desc'] else args['desc']) else ''
copy_right = site.copyright if (site.copyright if not args['copyright'] else args['copyright']) else ''
privacy_policy = site.privacy_policy \
if (site.privacy_policy if not args['privacy_policy'] else args['privacy_policy']) else ''
recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == args['app_id']).first()
if not recommended_app:
recommended_app = RecommendedApp(
app_id=app.id,
description=desc,
copyright=copy_right,
privacy_policy=privacy_policy,
language=args['language'],
category=args['category'],
position=args['position']
)
db.session.add(recommended_app)
app.is_public = True
db.session.commit()
return {'result': 'success'}, 201
else:
recommended_app.description = desc
recommended_app.copyright = copy_right
recommended_app.privacy_policy = privacy_policy
recommended_app.language = args['language']
recommended_app.category = args['category']
recommended_app.position = args['position']
app.is_public = True
db.session.commit()
return {'result': 'success'}, 200
class InsertExploreAppApi(Resource):
@only_edition_cloud
@admin_required
def delete(self, app_id):
recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == str(app_id)).first()
if not recommended_app:
return {'result': 'success'}, 204
app = App.query.filter(App.id == recommended_app.app_id).first()
if app:
app.is_public = False
installed_apps = InstalledApp.query.filter(
InstalledApp.app_id == recommended_app.app_id,
InstalledApp.tenant_id != InstalledApp.app_owner_tenant_id
).all()
for installed_app in installed_apps:
db.session.delete(installed_app)
db.session.delete(recommended_app)
db.session.commit()
return {'result': 'success'}, 204
api.add_resource(InsertExploreAppListApi, '/admin/insert-explore-apps')
api.add_resource(InsertExploreAppApi, '/admin/insert-explore-apps/<uuid:app_id>')
......@@ -9,18 +9,13 @@ from werkzeug.exceptions import Unauthorized, Forbidden
from constants.model_template import model_templates, demo_model_templates
from controllers.console import api
from controllers.console.app.error import AppNotFoundError, ProviderNotInitializeError, ProviderQuotaExceededError, \
CompletionRequestError, ProviderModelCurrentlyNotSupportError
from controllers.console.app.error import AppNotFoundError
from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required
from core.generator.llm_generator import LLMGenerator
from core.llm.error import ProviderTokenNotInitError, QuotaExceededError, LLMBadRequestError, LLMAPIConnectionError, \
LLMAPIUnavailableError, LLMRateLimitError, LLMAuthorizationError, ModelCurrentlyNotSupportError
from events.app_event import app_was_created, app_was_deleted
from libs.helper import TimestampField
from extensions.ext_database import db
from models.model import App, AppModelConfig, Site, InstalledApp
from services.account_service import TenantService
from models.model import App, AppModelConfig, Site
from services.app_model_config_service import AppModelConfigService
model_config_fields = {
......@@ -478,35 +473,6 @@ class AppExport(Resource):
pass
class IntroductionGenerateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('prompt_template', type=str, required=True, location='json')
args = parser.parse_args()
account = current_user
try:
answer = LLMGenerator.generate_introduction(
account.current_tenant_id,
args['prompt_template']
)
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
return {'introduction': answer}
api.add_resource(AppListApi, '/apps')
api.add_resource(AppTemplateApi, '/app-templates')
api.add_resource(AppApi, '/apps/<uuid:app_id>')
......@@ -515,4 +481,3 @@ api.add_resource(AppNameApi, '/apps/<uuid:app_id>/name')
api.add_resource(AppSiteStatus, '/apps/<uuid:app_id>/site-enable')
api.add_resource(AppApiStatus, '/apps/<uuid:app_id>/api-enable')
api.add_resource(AppRateLimit, '/apps/<uuid:app_id>/rate-limit')
api.add_resource(IntroductionGenerateApi, '/introduction-generate')
from flask_login import login_required, current_user
from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.app.error import ProviderNotInitializeError, ProviderQuotaExceededError, \
CompletionRequestError, ProviderModelCurrentlyNotSupportError
from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required
from core.generator.llm_generator import LLMGenerator
from core.llm.error import ProviderTokenNotInitError, QuotaExceededError, LLMBadRequestError, LLMAPIConnectionError, \
LLMAPIUnavailableError, LLMRateLimitError, LLMAuthorizationError, ModelCurrentlyNotSupportError
class IntroductionGenerateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('prompt_template', type=str, required=True, location='json')
args = parser.parse_args()
account = current_user
try:
answer = LLMGenerator.generate_introduction(
account.current_tenant_id,
args['prompt_template']
)
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
return {'introduction': answer}
class RuleGenerateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('audiences', type=str, required=True, nullable=False, location='json')
parser.add_argument('hoping_to_solve', type=str, required=True, nullable=False, location='json')
args = parser.parse_args()
account = current_user
try:
rules = LLMGenerator.generate_rule_config(
account.current_tenant_id,
args['audiences'],
args['hoping_to_solve']
)
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
return rules
api.add_resource(IntroductionGenerateApi, '/introduction-generate')
api.add_resource(RuleGenerateApi, '/rule-generate')
# -*- coding:utf-8 -*-
from decimal import Decimal
from datetime import datetime
import pytz
......@@ -59,18 +60,20 @@ class DailyConversationStatistic(Resource):
arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date'
rs = db.session.execute(sql_query, arg_dict)
response_date = []
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs:
response_date.append({
response_data.append({
'date': str(i.date),
'conversation_count': i.conversation_count
})
return jsonify({
'data': response_date
'data': response_data
})
......@@ -119,18 +122,20 @@ class DailyTerminalsStatistic(Resource):
arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date'
rs = db.session.execute(sql_query, arg_dict)
response_date = []
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs:
response_date.append({
response_data.append({
'date': str(i.date),
'terminal_count': i.terminal_count
})
return jsonify({
'data': response_date
'data': response_data
})
......@@ -180,12 +185,14 @@ class DailyTokenCostStatistic(Resource):
arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date'
rs = db.session.execute(sql_query, arg_dict)
response_date = []
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs:
response_date.append({
response_data.append({
'date': str(i.date),
'token_count': i.token_count,
'total_price': i.total_price,
......@@ -193,10 +200,207 @@ class DailyTokenCostStatistic(Resource):
})
return jsonify({
'data': response_date
'data': response_data
})
class AverageSessionInteractionStatistic(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self, app_id):
account = current_user
app_id = str(app_id)
app_model = _get_app(app_id, 'chat')
parser = reqparse.RequestParser()
parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
parser.add_argument('end', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
args = parser.parse_args()
sql_query = """SELECT date(DATE_TRUNC('day', c.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
AVG(subquery.message_count) AS interactions
FROM (SELECT m.conversation_id, COUNT(m.id) AS message_count
FROM conversations c
JOIN messages m ON c.id = m.conversation_id
WHERE c.override_model_configs IS NULL AND c.app_id = :app_id"""
arg_dict = {'tz': account.timezone, 'app_id': app_model.id}
timezone = pytz.timezone(account.timezone)
utc_timezone = pytz.utc
if args['start']:
start_datetime = datetime.strptime(args['start'], '%Y-%m-%d %H:%M')
start_datetime = start_datetime.replace(second=0)
start_datetime_timezone = timezone.localize(start_datetime)
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and c.created_at >= :start'
arg_dict['start'] = start_datetime_utc
if args['end']:
end_datetime = datetime.strptime(args['end'], '%Y-%m-%d %H:%M')
end_datetime = end_datetime.replace(second=0)
end_datetime_timezone = timezone.localize(end_datetime)
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and c.created_at < :end'
arg_dict['end'] = end_datetime_utc
sql_query += """
GROUP BY m.conversation_id) subquery
LEFT JOIN conversations c on c.id=subquery.conversation_id
GROUP BY date
ORDER BY date"""
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs:
response_data.append({
'date': str(i.date),
'interactions': float(i.interactions.quantize(Decimal('0.01')))
})
return jsonify({
'data': response_data
})
class UserSatisfactionRateStatistic(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self, app_id):
account = current_user
app_id = str(app_id)
app_model = _get_app(app_id)
parser = reqparse.RequestParser()
parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
parser.add_argument('end', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
args = parser.parse_args()
sql_query = '''
SELECT date(DATE_TRUNC('day', m.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
COUNT(m.id) as message_count, COUNT(mf.id) as feedback_count
FROM messages m
LEFT JOIN message_feedbacks mf on mf.message_id=m.id
WHERE m.app_id = :app_id
'''
arg_dict = {'tz': account.timezone, 'app_id': app_model.id}
timezone = pytz.timezone(account.timezone)
utc_timezone = pytz.utc
if args['start']:
start_datetime = datetime.strptime(args['start'], '%Y-%m-%d %H:%M')
start_datetime = start_datetime.replace(second=0)
start_datetime_timezone = timezone.localize(start_datetime)
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and m.created_at >= :start'
arg_dict['start'] = start_datetime_utc
if args['end']:
end_datetime = datetime.strptime(args['end'], '%Y-%m-%d %H:%M')
end_datetime = end_datetime.replace(second=0)
end_datetime_timezone = timezone.localize(end_datetime)
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and m.created_at < :end'
arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date'
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs:
response_data.append({
'date': str(i.date),
'rate': round((i.feedback_count * 1000 / i.message_count) if i.message_count > 0 else 0, 2),
})
return jsonify({
'data': response_data
})
class AverageResponseTimeStatistic(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self, app_id):
account = current_user
app_id = str(app_id)
app_model = _get_app(app_id, 'completion')
parser = reqparse.RequestParser()
parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
parser.add_argument('end', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
args = parser.parse_args()
sql_query = '''
SELECT date(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
AVG(provider_response_latency) as latency
FROM messages
WHERE app_id = :app_id
'''
arg_dict = {'tz': account.timezone, 'app_id': app_model.id}
timezone = pytz.timezone(account.timezone)
utc_timezone = pytz.utc
if args['start']:
start_datetime = datetime.strptime(args['start'], '%Y-%m-%d %H:%M')
start_datetime = start_datetime.replace(second=0)
start_datetime_timezone = timezone.localize(start_datetime)
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and created_at >= :start'
arg_dict['start'] = start_datetime_utc
if args['end']:
end_datetime = datetime.strptime(args['end'], '%Y-%m-%d %H:%M')
end_datetime = end_datetime.replace(second=0)
end_datetime_timezone = timezone.localize(end_datetime)
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and created_at < :end'
arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date'
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs:
response_data.append({
'date': str(i.date),
'latency': round(i.latency * 1000, 4)
})
return jsonify({
'data': response_data
})
api.add_resource(DailyConversationStatistic, '/apps/<uuid:app_id>/statistics/daily-conversations')
api.add_resource(DailyTerminalsStatistic, '/apps/<uuid:app_id>/statistics/daily-end-users')
api.add_resource(DailyTokenCostStatistic, '/apps/<uuid:app_id>/statistics/token-costs')
api.add_resource(AverageSessionInteractionStatistic, '/apps/<uuid:app_id>/statistics/average-session-interactions')
api.add_resource(UserSatisfactionRateStatistic, '/apps/<uuid:app_id>/statistics/user-satisfaction-rate')
api.add_resource(AverageResponseTimeStatistic, '/apps/<uuid:app_id>/statistics/average-response-time')
# -*- coding:utf-8 -*-
import json
import logging
from typing import Generator, Union
from flask import Response, stream_with_context
from flask_login import current_user
from flask_restful import reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
from controllers.console import api
from controllers.console.app.error import ConversationCompletedError, AppUnavailableError, ProviderNotInitializeError, \
ProviderQuotaExceededError, ProviderModelCurrentlyNotSupportError, CompletionRequestError
from controllers.console.explore.error import NotCompletionAppError, NotChatAppError
from controllers.console.explore.wraps import InstalledAppResource
from core.conversation_message_task import PubHandler
from core.llm.error import LLMBadRequestError, LLMAPIUnavailableError, LLMAuthorizationError, LLMAPIConnectionError, \
LLMRateLimitError, ProviderTokenNotInitError, QuotaExceededError, ModelCurrentlyNotSupportError
from libs.helper import uuid_value
from services.completion_service import CompletionService
# define completion api for user
class CompletionApi(InstalledAppResource):
def post(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
parser = reqparse.RequestParser()
parser.add_argument('inputs', type=dict, required=True, location='json')
parser.add_argument('query', type=str, location='json')
parser.add_argument('response_mode', type=str, choices=['blocking', 'streaming'], location='json')
args = parser.parse_args()
streaming = args['response_mode'] == 'streaming'
try:
response = CompletionService.completion(
app_model=app_model,
user=current_user,
args=args,
from_source='console',
streaming=streaming
)
return compact_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
raise InternalServerError()
class CompletionStopApi(InstalledAppResource):
def post(self, installed_app, task_id):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
PubHandler.stop(current_user, task_id)
return {'result': 'success'}, 200
class ChatApi(InstalledAppResource):
def post(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
parser = reqparse.RequestParser()
parser.add_argument('inputs', type=dict, required=True, location='json')
parser.add_argument('query', type=str, required=True, location='json')
parser.add_argument('response_mode', type=str, choices=['blocking', 'streaming'], location='json')
parser.add_argument('conversation_id', type=uuid_value, location='json')
args = parser.parse_args()
streaming = args['response_mode'] == 'streaming'
try:
response = CompletionService.completion(
app_model=app_model,
user=current_user,
args=args,
from_source='console',
streaming=streaming
)
return compact_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
raise InternalServerError()
class ChatStopApi(InstalledAppResource):
def post(self, installed_app, task_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
PubHandler.stop(current_user, task_id)
return {'result': 'success'}, 200
def compact_response(response: Union[dict | Generator]) -> Response:
if isinstance(response, dict):
return Response(response=json.dumps(response), status=200, mimetype='application/json')
else:
def generate() -> Generator:
try:
for chunk in response:
yield chunk
except services.errors.conversation.ConversationNotExistsError:
yield "data: " + json.dumps(api.handle_error(NotFound("Conversation Not Exists.")).get_json()) + "\n\n"
except services.errors.conversation.ConversationCompletedError:
yield "data: " + json.dumps(api.handle_error(ConversationCompletedError()).get_json()) + "\n\n"
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
yield "data: " + json.dumps(api.handle_error(AppUnavailableError()).get_json()) + "\n\n"
except ProviderTokenNotInitError:
yield "data: " + json.dumps(api.handle_error(ProviderNotInitializeError()).get_json()) + "\n\n"
except QuotaExceededError:
yield "data: " + json.dumps(api.handle_error(ProviderQuotaExceededError()).get_json()) + "\n\n"
except ModelCurrentlyNotSupportError:
yield "data: " + json.dumps(api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
yield "data: " + json.dumps(api.handle_error(CompletionRequestError(str(e))).get_json()) + "\n\n"
except ValueError as e:
yield "data: " + json.dumps(api.handle_error(e).get_json()) + "\n\n"
except Exception:
logging.exception("internal server error.")
yield "data: " + json.dumps(api.handle_error(InternalServerError()).get_json()) + "\n\n"
return Response(stream_with_context(generate()), status=200,
mimetype='text/event-stream')
api.add_resource(CompletionApi, '/installed-apps/<uuid:installed_app_id>/completion-messages', endpoint='installed_app_completion')
api.add_resource(CompletionStopApi, '/installed-apps/<uuid:installed_app_id>/completion-messages/<string:task_id>/stop', endpoint='installed_app_stop_completion')
api.add_resource(ChatApi, '/installed-apps/<uuid:installed_app_id>/chat-messages', endpoint='installed_app_chat_completion')
api.add_resource(ChatStopApi, '/installed-apps/<uuid:installed_app_id>/chat-messages/<string:task_id>/stop', endpoint='installed_app_stop_chat_completion')
# -*- coding:utf-8 -*-
from flask_login import current_user
from flask_restful import fields, reqparse, marshal_with
from flask_restful.inputs import int_range
from werkzeug.exceptions import NotFound
from controllers.console import api
from controllers.console.explore.error import NotChatAppError
from controllers.console.explore.wraps import InstalledAppResource
from libs.helper import TimestampField, uuid_value
from services.conversation_service import ConversationService
from services.errors.conversation import LastConversationNotExistsError, ConversationNotExistsError
from services.web_conversation_service import WebConversationService
conversation_fields = {
'id': fields.String,
'name': fields.String,
'inputs': fields.Raw,
'status': fields.String,
'introduction': fields.String,
'created_at': TimestampField
}
conversation_infinite_scroll_pagination_fields = {
'limit': fields.Integer,
'has_more': fields.Boolean,
'data': fields.List(fields.Nested(conversation_fields))
}
class ConversationListApi(InstalledAppResource):
@marshal_with(conversation_infinite_scroll_pagination_fields)
def get(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
parser = reqparse.RequestParser()
parser.add_argument('last_id', type=uuid_value, location='args')
parser.add_argument('limit', type=int_range(1, 100), required=False, default=20, location='args')
parser.add_argument('pinned', type=str, choices=['true', 'false', None], location='args')
args = parser.parse_args()
pinned = None
if 'pinned' in args and args['pinned'] is not None:
pinned = True if args['pinned'] == 'true' else False
try:
return WebConversationService.pagination_by_last_id(
app_model=app_model,
user=current_user,
last_id=args['last_id'],
limit=args['limit'],
pinned=pinned
)
except LastConversationNotExistsError:
raise NotFound("Last Conversation Not Exists.")
class ConversationApi(InstalledAppResource):
def delete(self, installed_app, c_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
conversation_id = str(c_id)
ConversationService.delete(app_model, conversation_id, current_user)
WebConversationService.unpin(app_model, conversation_id, current_user)
return {"result": "success"}, 204
class ConversationRenameApi(InstalledAppResource):
@marshal_with(conversation_fields)
def post(self, installed_app, c_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
conversation_id = str(c_id)
parser = reqparse.RequestParser()
parser.add_argument('name', type=str, required=True, location='json')
args = parser.parse_args()
try:
return ConversationService.rename(app_model, conversation_id, current_user, args['name'])
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
class ConversationPinApi(InstalledAppResource):
def patch(self, installed_app, c_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
conversation_id = str(c_id)
try:
WebConversationService.pin(app_model, conversation_id, current_user)
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
return {"result": "success"}
class ConversationUnPinApi(InstalledAppResource):
def patch(self, installed_app, c_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
conversation_id = str(c_id)
WebConversationService.unpin(app_model, conversation_id, current_user)
return {"result": "success"}
api.add_resource(ConversationRenameApi, '/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/name', endpoint='installed_app_conversation_rename')
api.add_resource(ConversationListApi, '/installed-apps/<uuid:installed_app_id>/conversations', endpoint='installed_app_conversations')
api.add_resource(ConversationApi, '/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>', endpoint='installed_app_conversation')
api.add_resource(ConversationPinApi, '/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/pin', endpoint='installed_app_conversation_pin')
api.add_resource(ConversationUnPinApi, '/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/unpin', endpoint='installed_app_conversation_unpin')
# -*- coding:utf-8 -*-
from libs.exception import BaseHTTPException
class NotCompletionAppError(BaseHTTPException):
error_code = 'not_completion_app'
description = "Not Completion App"
code = 400
class NotChatAppError(BaseHTTPException):
error_code = 'not_chat_app'
description = "Not Chat App"
code = 400
class AppSuggestedQuestionsAfterAnswerDisabledError(BaseHTTPException):
error_code = 'app_suggested_questions_after_answer_disabled'
description = "Function Suggested questions after answer disabled."
code = 403
......@@ -2,12 +2,16 @@
from datetime import datetime
from flask_login import login_required, current_user
from flask_restful import Resource, reqparse, fields, marshal_with, abort, inputs
from flask_restful import Resource, reqparse, fields, marshal_with, inputs
from sqlalchemy import and_
from werkzeug.exceptions import NotFound, Forbidden, BadRequest
from controllers.console import api
from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db
from models.model import Tenant, App, InstalledApp, RecommendedApp
from libs.helper import TimestampField
from models.model import App, InstalledApp, RecommendedApp
from services.account_service import TenantService
app_fields = {
......@@ -20,42 +24,25 @@ app_fields = {
installed_app_fields = {
'id': fields.String,
'app': fields.Nested(app_fields, attribute='app'),
'app': fields.Nested(app_fields),
'app_owner_tenant_id': fields.String,
'is_pinned': fields.Boolean,
'last_used_at': fields.DateTime,
'editable': fields.Boolean
'last_used_at': TimestampField,
'editable': fields.Boolean,
'uninstallable': fields.Boolean,
}
installed_app_list_fields = {
'installed_apps': fields.List(fields.Nested(installed_app_fields))
}
recommended_app_fields = {
'app': fields.Nested(app_fields, attribute='app'),
'app_id': fields.String,
'description': fields.String(attribute='description'),
'copyright': fields.String,
'privacy_policy': fields.String,
'category': fields.String,
'position': fields.Integer,
'is_listed': fields.Boolean,
'install_count': fields.Integer,
'installed': fields.Boolean,
'editable': fields.Boolean
}
recommended_app_list_fields = {
'recommended_apps': fields.List(fields.Nested(recommended_app_fields)),
'categories': fields.List(fields.String)
}
class InstalledAppsListResource(Resource):
class InstalledAppsListApi(Resource):
@login_required
@account_initialization_required
@marshal_with(installed_app_list_fields)
def get(self):
current_tenant_id = Tenant.query.first().id
current_tenant_id = current_user.current_tenant_id
installed_apps = db.session.query(InstalledApp).filter(
InstalledApp.tenant_id == current_tenant_id
).all()
......@@ -63,30 +50,42 @@ class InstalledAppsListResource(Resource):
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
installed_apps = [
{
**installed_app,
'id': installed_app.id,
'app': installed_app.app,
'app_owner_tenant_id': installed_app.app_owner_tenant_id,
'is_pinned': installed_app.is_pinned,
'last_used_at': installed_app.last_used_at,
"editable": current_user.role in ["owner", "admin"],
"uninstallable": current_tenant_id == installed_app.app_owner_tenant_id
}
for installed_app in installed_apps
]
installed_apps.sort(key=lambda app: (-app.is_pinned, app.last_used_at))
installed_apps.sort(key=lambda app: (-app['is_pinned'], app['last_used_at']
if app['last_used_at'] is not None else datetime.min))
return {'installed_apps': installed_apps}
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('app_id', type=str, required=True, help='Invalid app_id')
args = parser.parse_args()
current_tenant_id = Tenant.query.first().id
app = App.query.get(args['app_id'])
if app is None:
abort(404, message='App not found')
recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == args['app_id']).first()
if recommended_app is None:
abort(404, message='App not found')
raise NotFound('App not found')
current_tenant_id = current_user.current_tenant_id
app = db.session.query(App).filter(
App.id == args['app_id']
).first()
if app is None:
raise NotFound('App not found')
if not app.is_public:
abort(403, message="You can't install a non-public app")
raise Forbidden('You can\'t install a non-public app')
installed_app = InstalledApp.query.filter(and_(
InstalledApp.app_id == args['app_id'],
......@@ -100,6 +99,7 @@ class InstalledAppsListResource(Resource):
new_installed_app = InstalledApp(
app_id=args['app_id'],
tenant_id=current_tenant_id,
app_owner_tenant_id=app.tenant_id,
is_pinned=False,
last_used_at=datetime.utcnow()
)
......@@ -109,42 +109,25 @@ class InstalledAppsListResource(Resource):
return {'message': 'App installed successfully'}
class InstalledAppResource(Resource):
@login_required
def delete(self, installed_app_id):
installed_app = InstalledApp.query.filter(and_(
InstalledApp.id == str(installed_app_id),
InstalledApp.tenant_id == current_user.current_tenant_id
)).first()
if installed_app is None:
abort(404, message='App not found')
class InstalledAppApi(InstalledAppResource):
"""
update and delete an installed app
use InstalledAppResource to apply default decorators and get installed_app
"""
def delete(self, installed_app):
if installed_app.app_owner_tenant_id == current_user.current_tenant_id:
abort(400, message="You can't uninstall an app owned by the current tenant")
raise BadRequest('You can\'t uninstall an app owned by the current tenant')
db.session.delete(installed_app)
db.session.commit()
return {'result': 'success', 'message': 'App uninstalled successfully'}
@login_required
def patch(self, installed_app_id):
def patch(self, installed_app):
parser = reqparse.RequestParser()
parser.add_argument('is_pinned', type=inputs.boolean)
args = parser.parse_args()
current_tenant_id = Tenant.query.first().id
installed_app = InstalledApp.query.filter(and_(
InstalledApp.id == str(installed_app_id),
InstalledApp.tenant_id == current_tenant_id
)).first()
if installed_app is None:
abort(404, message='Installed app not found')
commit_args = False
if 'is_pinned' in args:
installed_app.is_pinned = args['is_pinned']
......@@ -156,54 +139,5 @@ class InstalledAppResource(Resource):
return {'result': 'success', 'message': 'App info updated successfully'}
class RecommendedAppsResource(Resource):
@login_required
@marshal_with(recommended_app_list_fields)
def get(self):
recommended_apps = db.session.query(RecommendedApp).filter(
RecommendedApp.is_listed == True
).all()
categories = set()
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
recommended_apps_result = []
for recommended_app in recommended_apps:
installed = db.session.query(InstalledApp).filter(
and_(
InstalledApp.app_id == recommended_app.app_id,
InstalledApp.tenant_id == current_user.current_tenant_id
)
).first() is not None
language_prefix = current_user.interface_language.split('-')[0]
desc = None
if recommended_app.description:
if language_prefix in recommended_app.description:
desc = recommended_app.description[language_prefix]
elif 'en' in recommended_app.description:
desc = recommended_app.description['en']
recommended_app_result = {
'id': recommended_app.id,
'app': recommended_app.app,
'app_id': recommended_app.app_id,
'description': desc,
'copyright': recommended_app.copyright,
'privacy_policy': recommended_app.privacy_policy,
'category': recommended_app.category,
'position': recommended_app.position,
'is_listed': recommended_app.is_listed,
'install_count': recommended_app.install_count,
'installed': installed,
'editable': current_user.role in ['owner', 'admin'],
}
recommended_apps_result.append(recommended_app_result)
categories.add(recommended_app.category) # add category to categories
return {'recommended_apps': recommended_apps_result, 'categories': list(categories)}
api.add_resource(InstalledAppsListResource, '/installed-apps')
api.add_resource(InstalledAppResource, '/installed-apps/<uuid:installed_app_id>')
api.add_resource(RecommendedAppsResource, '/explore/apps')
api.add_resource(InstalledAppsListApi, '/installed-apps')
api.add_resource(InstalledAppApi, '/installed-apps/<uuid:installed_app_id>')
# -*- coding:utf-8 -*-
import json
import logging
from typing import Generator, Union
from flask import stream_with_context, Response
from flask_login import current_user
from flask_restful import reqparse, fields, marshal_with
from flask_restful.inputs import int_range
from werkzeug.exceptions import NotFound, InternalServerError
import services
from controllers.console import api
from controllers.console.app.error import AppMoreLikeThisDisabledError, ProviderNotInitializeError, \
ProviderQuotaExceededError, ProviderModelCurrentlyNotSupportError, CompletionRequestError
from controllers.console.explore.error import NotCompletionAppError, AppSuggestedQuestionsAfterAnswerDisabledError
from controllers.console.explore.wraps import InstalledAppResource
from core.llm.error import LLMRateLimitError, LLMBadRequestError, LLMAuthorizationError, LLMAPIConnectionError, \
ProviderTokenNotInitError, LLMAPIUnavailableError, QuotaExceededError, ModelCurrentlyNotSupportError
from libs.helper import uuid_value, TimestampField
from services.completion_service import CompletionService
from services.errors.app import MoreLikeThisDisabledError
from services.errors.conversation import ConversationNotExistsError
from services.errors.message import MessageNotExistsError, SuggestedQuestionsAfterAnswerDisabledError
from services.message_service import MessageService
class MessageListApi(InstalledAppResource):
feedback_fields = {
'rating': fields.String
}
message_fields = {
'id': fields.String,
'conversation_id': fields.String,
'inputs': fields.Raw,
'query': fields.String,
'answer': fields.String,
'feedback': fields.Nested(feedback_fields, attribute='user_feedback', allow_null=True),
'created_at': TimestampField
}
message_infinite_scroll_pagination_fields = {
'limit': fields.Integer,
'has_more': fields.Boolean,
'data': fields.List(fields.Nested(message_fields))
}
@marshal_with(message_infinite_scroll_pagination_fields)
def get(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
parser = reqparse.RequestParser()
parser.add_argument('conversation_id', required=True, type=uuid_value, location='args')
parser.add_argument('first_id', type=uuid_value, location='args')
parser.add_argument('limit', type=int_range(1, 100), required=False, default=20, location='args')
args = parser.parse_args()
try:
return MessageService.pagination_by_first_id(app_model, current_user,
args['conversation_id'], args['first_id'], args['limit'])
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
except services.errors.message.FirstMessageNotExistsError:
raise NotFound("First Message Not Exists.")
class MessageFeedbackApi(InstalledAppResource):
def post(self, installed_app, message_id):
app_model = installed_app.app
message_id = str(message_id)
parser = reqparse.RequestParser()
parser.add_argument('rating', type=str, choices=['like', 'dislike', None], location='json')
args = parser.parse_args()
try:
MessageService.create_feedback(app_model, message_id, current_user, args['rating'])
except services.errors.message.MessageNotExistsError:
raise NotFound("Message Not Exists.")
return {'result': 'success'}
class MessageMoreLikeThisApi(InstalledAppResource):
def get(self, installed_app, message_id):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
message_id = str(message_id)
parser = reqparse.RequestParser()
parser.add_argument('response_mode', type=str, required=True, choices=['blocking', 'streaming'], location='args')
args = parser.parse_args()
streaming = args['response_mode'] == 'streaming'
try:
response = CompletionService.generate_more_like_this(app_model, current_user, message_id, streaming)
return compact_response(response)
except MessageNotExistsError:
raise NotFound("Message Not Exists.")
except MoreLikeThisDisabledError:
raise AppMoreLikeThisDisabledError()
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
raise InternalServerError()
def compact_response(response: Union[dict | Generator]) -> Response:
if isinstance(response, dict):
return Response(response=json.dumps(response), status=200, mimetype='application/json')
else:
def generate() -> Generator:
try:
for chunk in response:
yield chunk
except MessageNotExistsError:
yield "data: " + json.dumps(api.handle_error(NotFound("Message Not Exists.")).get_json()) + "\n\n"
except MoreLikeThisDisabledError:
yield "data: " + json.dumps(api.handle_error(AppMoreLikeThisDisabledError()).get_json()) + "\n\n"
except ProviderTokenNotInitError:
yield "data: " + json.dumps(api.handle_error(ProviderNotInitializeError()).get_json()) + "\n\n"
except QuotaExceededError:
yield "data: " + json.dumps(api.handle_error(ProviderQuotaExceededError()).get_json()) + "\n\n"
except ModelCurrentlyNotSupportError:
yield "data: " + json.dumps(api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
yield "data: " + json.dumps(api.handle_error(CompletionRequestError(str(e))).get_json()) + "\n\n"
except ValueError as e:
yield "data: " + json.dumps(api.handle_error(e).get_json()) + "\n\n"
except Exception:
logging.exception("internal server error.")
yield "data: " + json.dumps(api.handle_error(InternalServerError()).get_json()) + "\n\n"
return Response(stream_with_context(generate()), status=200,
mimetype='text/event-stream')
class MessageSuggestedQuestionApi(InstalledAppResource):
def get(self, installed_app, message_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotCompletionAppError()
message_id = str(message_id)
try:
questions = MessageService.get_suggested_questions_after_answer(
app_model=app_model,
user=current_user,
message_id=message_id
)
except MessageNotExistsError:
raise NotFound("Message not found")
except ConversationNotExistsError:
raise NotFound("Conversation not found")
except SuggestedQuestionsAfterAnswerDisabledError:
raise AppSuggestedQuestionsAfterAnswerDisabledError()
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
except Exception:
logging.exception("internal server error.")
raise InternalServerError()
return {'data': questions}
api.add_resource(MessageListApi, '/installed-apps/<uuid:installed_app_id>/messages', endpoint='installed_app_messages')
api.add_resource(MessageFeedbackApi, '/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/feedbacks', endpoint='installed_app_message_feedback')
api.add_resource(MessageMoreLikeThisApi, '/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/more-like-this', endpoint='installed_app_more_like_this')
api.add_resource(MessageSuggestedQuestionApi, '/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/suggested-questions', endpoint='installed_app_suggested_question')
# -*- coding:utf-8 -*-
from flask_restful import marshal_with, fields
from controllers.console import api
from controllers.console.explore.wraps import InstalledAppResource
class AppParameterApi(InstalledAppResource):
"""Resource for app variables."""
variable_fields = {
'key': fields.String,
'name': fields.String,
'description': fields.String,
'type': fields.String,
'default': fields.String,
'max_length': fields.Integer,
'options': fields.List(fields.String)
}
parameters_fields = {
'opening_statement': fields.String,
'suggested_questions': fields.Raw,
'suggested_questions_after_answer': fields.Raw,
'more_like_this': fields.Raw,
'user_input_form': fields.Raw,
}
@marshal_with(parameters_fields)
def get(self, installed_app):
"""Retrieve app parameters."""
app_model = installed_app.app
app_model_config = app_model.app_model_config
return {
'opening_statement': app_model_config.opening_statement,
'suggested_questions': app_model_config.suggested_questions_list,
'suggested_questions_after_answer': app_model_config.suggested_questions_after_answer_dict,
'more_like_this': app_model_config.more_like_this_dict,
'user_input_form': app_model_config.user_input_form_list
}
api.add_resource(AppParameterApi, '/installed-apps/<uuid:installed_app_id>/parameters', endpoint='installed_app_parameters')
# -*- coding:utf-8 -*-
from flask_login import login_required, current_user
from flask_restful import Resource, fields, marshal_with
from sqlalchemy import and_
from controllers.console import api
from controllers.console.app.error import AppNotFoundError
from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db
from models.model import App, InstalledApp, RecommendedApp
from services.account_service import TenantService
app_fields = {
'id': fields.String,
'name': fields.String,
'mode': fields.String,
'icon': fields.String,
'icon_background': fields.String
}
recommended_app_fields = {
'app': fields.Nested(app_fields, attribute='app'),
'app_id': fields.String,
'description': fields.String(attribute='description'),
'copyright': fields.String,
'privacy_policy': fields.String,
'category': fields.String,
'position': fields.Integer,
'is_listed': fields.Boolean,
'install_count': fields.Integer,
'installed': fields.Boolean,
'editable': fields.Boolean
}
recommended_app_list_fields = {
'recommended_apps': fields.List(fields.Nested(recommended_app_fields)),
'categories': fields.List(fields.String)
}
class RecommendedAppListApi(Resource):
@login_required
@account_initialization_required
@marshal_with(recommended_app_list_fields)
def get(self):
language_prefix = current_user.interface_language if current_user.interface_language else 'en-US'
recommended_apps = db.session.query(RecommendedApp).filter(
RecommendedApp.is_listed == True,
RecommendedApp.language == language_prefix
).all()
categories = set()
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
recommended_apps_result = []
for recommended_app in recommended_apps:
installed = db.session.query(InstalledApp).filter(
and_(
InstalledApp.app_id == recommended_app.app_id,
InstalledApp.tenant_id == current_user.current_tenant_id
)
).first() is not None
app = recommended_app.app
if not app or not app.is_public:
continue
site = app.site
if not site:
continue
recommended_app_result = {
'id': recommended_app.id,
'app': app,
'app_id': recommended_app.app_id,
'description': site.description,
'copyright': site.copyright,
'privacy_policy': site.privacy_policy,
'category': recommended_app.category,
'position': recommended_app.position,
'is_listed': recommended_app.is_listed,
'install_count': recommended_app.install_count,
'installed': installed,
'editable': current_user.role in ['owner', 'admin'],
}
recommended_apps_result.append(recommended_app_result)
categories.add(recommended_app.category) # add category to categories
return {'recommended_apps': recommended_apps_result, 'categories': list(categories)}
class RecommendedAppApi(Resource):
model_config_fields = {
'opening_statement': fields.String,
'suggested_questions': fields.Raw(attribute='suggested_questions_list'),
'suggested_questions_after_answer': fields.Raw(attribute='suggested_questions_after_answer_dict'),
'more_like_this': fields.Raw(attribute='more_like_this_dict'),
'model': fields.Raw(attribute='model_dict'),
'user_input_form': fields.Raw(attribute='user_input_form_list'),
'pre_prompt': fields.String,
'agent_mode': fields.Raw(attribute='agent_mode_dict'),
}
app_simple_detail_fields = {
'id': fields.String,
'name': fields.String,
'icon': fields.String,
'icon_background': fields.String,
'mode': fields.String,
'app_model_config': fields.Nested(model_config_fields),
}
@login_required
@account_initialization_required
@marshal_with(app_simple_detail_fields)
def get(self, app_id):
app_id = str(app_id)
# is in public recommended list
recommended_app = db.session.query(RecommendedApp).filter(
RecommendedApp.is_listed == True,
RecommendedApp.app_id == app_id
).first()
if not recommended_app:
raise AppNotFoundError
# get app detail
app = db.session.query(App).filter(App.id == app_id).first()
if not app or not app.is_public:
raise AppNotFoundError
return app
api.add_resource(RecommendedAppListApi, '/explore/apps')
api.add_resource(RecommendedAppApi, '/explore/apps/<uuid:app_id>')
from flask_login import current_user
from flask_restful import reqparse, marshal_with, fields
from flask_restful.inputs import int_range
from werkzeug.exceptions import NotFound
from controllers.console import api
from controllers.console.explore.error import NotCompletionAppError
from controllers.console.explore.wraps import InstalledAppResource
from libs.helper import uuid_value, TimestampField
from services.errors.message import MessageNotExistsError
from services.saved_message_service import SavedMessageService
feedback_fields = {
'rating': fields.String
}
message_fields = {
'id': fields.String,
'inputs': fields.Raw,
'query': fields.String,
'answer': fields.String,
'feedback': fields.Nested(feedback_fields, attribute='user_feedback', allow_null=True),
'created_at': TimestampField
}
class SavedMessageListApi(InstalledAppResource):
saved_message_infinite_scroll_pagination_fields = {
'limit': fields.Integer,
'has_more': fields.Boolean,
'data': fields.List(fields.Nested(message_fields))
}
@marshal_with(saved_message_infinite_scroll_pagination_fields)
def get(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
parser = reqparse.RequestParser()
parser.add_argument('last_id', type=uuid_value, location='args')
parser.add_argument('limit', type=int_range(1, 100), required=False, default=20, location='args')
args = parser.parse_args()
return SavedMessageService.pagination_by_last_id(app_model, current_user, args['last_id'], args['limit'])
def post(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
parser = reqparse.RequestParser()
parser.add_argument('message_id', type=uuid_value, required=True, location='json')
args = parser.parse_args()
try:
SavedMessageService.save(app_model, current_user, args['message_id'])
except MessageNotExistsError:
raise NotFound("Message Not Exists.")
return {'result': 'success'}
class SavedMessageApi(InstalledAppResource):
def delete(self, installed_app, message_id):
app_model = installed_app.app
message_id = str(message_id)
if app_model.mode != 'completion':
raise NotCompletionAppError()
SavedMessageService.delete(app_model, current_user, message_id)
return {'result': 'success'}
api.add_resource(SavedMessageListApi, '/installed-apps/<uuid:installed_app_id>/saved-messages', endpoint='installed_app_saved_messages')
api.add_resource(SavedMessageApi, '/installed-apps/<uuid:installed_app_id>/saved-messages/<uuid:message_id>', endpoint='installed_app_saved_message')
from flask_login import login_required, current_user
from flask_restful import Resource
from functools import wraps
from werkzeug.exceptions import NotFound
from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db
from models.model import InstalledApp
def installed_app_required(view=None):
def decorator(view):
@wraps(view)
def decorated(*args, **kwargs):
if not kwargs.get('installed_app_id'):
raise ValueError('missing installed_app_id in path parameters')
installed_app_id = kwargs.get('installed_app_id')
installed_app_id = str(installed_app_id)
del kwargs['installed_app_id']
installed_app = db.session.query(InstalledApp).filter(
InstalledApp.id == str(installed_app_id),
InstalledApp.tenant_id == current_user.current_tenant_id
).first()
if installed_app is None:
raise NotFound('Installed app not found')
if not installed_app.app:
db.session.delete(installed_app)
db.session.commit()
raise NotFound('Installed app not found')
return view(installed_app, *args, **kwargs)
return decorated
if view:
return decorator(view)
return decorator
class InstalledAppResource(Resource):
# must be reversed if there are multiple decorators
method_decorators = [installed_app_required, account_initialization_required, login_required]
......@@ -47,7 +47,7 @@ class ConversationListApi(WebApiResource):
try:
return WebConversationService.pagination_by_last_id(
app_model=app_model,
end_user=end_user,
user=end_user,
last_id=args['last_id'],
limit=args['limit'],
pinned=pinned
......
......@@ -16,7 +16,7 @@ def validate_token(view=None):
def decorated(*args, **kwargs):
site = validate_and_get_site()
app_model = db.session.query(App).get(site.app_id)
app_model = db.session.query(App).filter(App.id == site.app_id).first()
if not app_model:
raise NotFound()
......@@ -42,13 +42,16 @@ def validate_and_get_site():
"""
auth_header = request.headers.get('Authorization')
if auth_header is None:
raise Unauthorized()
raise Unauthorized('Authorization header is missing.')
if ' ' not in auth_header:
raise Unauthorized('Invalid Authorization header format. Expected \'Bearer <api-key>\' format.')
auth_scheme, auth_token = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower()
if auth_scheme != 'bearer':
raise Unauthorized()
raise Unauthorized('Invalid Authorization header format. Expected \'Bearer <api-key>\' format.')
site = db.session.query(Site).filter(
Site.code == auth_token,
......
......@@ -34,5 +34,9 @@ class DatasetIndexToolCallbackHandler(IndexToolCallbackHandler):
db.session.query(DocumentSegment).filter(
DocumentSegment.dataset_id == self.dataset_id,
DocumentSegment.index_node_id == index_node_id
).update({DocumentSegment.hit_count: DocumentSegment.hit_count + 1}, synchronize_session=False)
).update(
{DocumentSegment.hit_count: DocumentSegment.hit_count + 1},
synchronize_session=False
)
db.session.commit()
"""Base classes for LLM-powered router chains."""
from __future__ import annotations
import json
from typing import Any, Dict, List, Optional, Type, cast, NamedTuple
from langchain.chains.base import Chain
from pydantic import root_validator
from langchain.chains import LLMChain
from langchain.prompts import BasePromptTemplate
from langchain.schema import BaseOutputParser, OutputParserException, BaseLanguageModel
from libs.json_in_md_parser import parse_and_check_json_markdown
class Route(NamedTuple):
destination: Optional[str]
next_inputs: Dict[str, Any]
class LLMRouterChain(Chain):
"""A router chain that uses an LLM chain to perform routing."""
llm_chain: LLMChain
"""LLM chain used to perform routing"""
@root_validator()
def validate_prompt(cls, values: dict) -> dict:
prompt = values["llm_chain"].prompt
if prompt.output_parser is None:
raise ValueError(
"LLMRouterChain requires base llm_chain prompt to have an output"
" parser that converts LLM text output to a dictionary with keys"
" 'destination' and 'next_inputs'. Received a prompt with no output"
" parser."
)
return values
@property
def input_keys(self) -> List[str]:
"""Will be whatever keys the LLM chain prompt expects.
:meta private:
"""
return self.llm_chain.input_keys
def _validate_outputs(self, outputs: Dict[str, Any]) -> None:
super()._validate_outputs(outputs)
if not isinstance(outputs["next_inputs"], dict):
raise ValueError
def _call(
self,
inputs: Dict[str, Any]
) -> Dict[str, Any]:
output = cast(
Dict[str, Any],
self.llm_chain.predict_and_parse(**inputs),
)
return output
@classmethod
def from_llm(
cls, llm: BaseLanguageModel, prompt: BasePromptTemplate, **kwargs: Any
) -> LLMRouterChain:
"""Convenience constructor."""
llm_chain = LLMChain(llm=llm, prompt=prompt)
return cls(llm_chain=llm_chain, **kwargs)
@property
def output_keys(self) -> List[str]:
return ["destination", "next_inputs"]
def route(self, inputs: Dict[str, Any]) -> Route:
result = self(inputs)
return Route(result["destination"], result["next_inputs"])
class RouterOutputParser(BaseOutputParser[Dict[str, str]]):
"""Parser for output of router chain int he multi-prompt chain."""
default_destination: str = "DEFAULT"
next_inputs_type: Type = str
next_inputs_inner_key: str = "input"
def parse(self, text: str) -> Dict[str, Any]:
try:
expected_keys = ["destination", "next_inputs"]
parsed = parse_and_check_json_markdown(text, expected_keys)
if not isinstance(parsed["destination"], str):
raise ValueError("Expected 'destination' to be a string.")
if not isinstance(parsed["next_inputs"], self.next_inputs_type):
raise ValueError(
f"Expected 'next_inputs' to be {self.next_inputs_type}."
)
parsed["next_inputs"] = {self.next_inputs_inner_key: parsed["next_inputs"]}
if (
parsed["destination"].strip().lower()
== self.default_destination.lower()
):
parsed["destination"] = None
else:
parsed["destination"] = parsed["destination"].strip()
return parsed
except Exception as e:
raise OutputParserException(
f"Parsing text\n{text}\n of llm router raised following error:\n{e}"
)
from typing import Optional, List
from langchain.callbacks import SharedCallbackManager
from langchain.callbacks import SharedCallbackManager, CallbackManager
from langchain.chains import SequentialChain
from langchain.chains.base import Chain
from langchain.memory.chat_memory import BaseChatMemory
from core.agent.agent_builder import AgentBuilder
from core.callback_handler.agent_loop_gather_callback_handler import AgentLoopGatherCallbackHandler
from core.callback_handler.dataset_tool_callback_handler import DatasetToolCallbackHandler
from core.callback_handler.main_chain_gather_callback_handler import MainChainGatherCallbackHandler
from core.callback_handler.std_out_callback_handler import DifyStdOutCallbackHandler
from core.chain.chain_builder import ChainBuilder
from core.constant import llm_constant
from core.chain.multi_dataset_router_chain import MultiDatasetRouterChain
from core.conversation_message_task import ConversationMessageTask
from core.tool.dataset_tool_builder import DatasetToolBuilder
from extensions.ext_database import db
from models.dataset import Dataset
class MainChainBuilder:
......@@ -31,8 +31,7 @@ class MainChainBuilder:
tenant_id=tenant_id,
agent_mode=agent_mode,
memory=memory,
dataset_tool_callback_handler=DatasetToolCallbackHandler(conversation_message_task),
agent_loop_gather_callback_handler=chain_callback_handler.agent_loop_gather_callback_handler
conversation_message_task=conversation_message_task
)
chains += tool_chains
......@@ -59,15 +58,15 @@ class MainChainBuilder:
@classmethod
def get_agent_chains(cls, tenant_id: str, agent_mode: dict, memory: Optional[BaseChatMemory],
dataset_tool_callback_handler: DatasetToolCallbackHandler,
agent_loop_gather_callback_handler: AgentLoopGatherCallbackHandler):
conversation_message_task: ConversationMessageTask):
# agent mode
chains = []
if agent_mode and agent_mode.get('enabled'):
tools = agent_mode.get('tools', [])
pre_fixed_chains = []
agent_tools = []
# agent_tools = []
datasets = []
for tool in tools:
tool_type = list(tool.keys())[0]
tool_config = list(tool.values())[0]
......@@ -76,34 +75,27 @@ class MainChainBuilder:
if chain:
pre_fixed_chains.append(chain)
elif tool_type == "dataset":
dataset_tool = DatasetToolBuilder.build_dataset_tool(
tenant_id=tenant_id,
dataset_id=tool_config.get("id"),
response_mode='no_synthesizer', # "compact"
callback_handler=dataset_tool_callback_handler
)
# get dataset from dataset id
dataset = db.session.query(Dataset).filter(
Dataset.tenant_id == tenant_id,
Dataset.id == tool_config.get("id")
).first()
if dataset_tool:
agent_tools.append(dataset_tool)
if dataset:
datasets.append(dataset)
# add pre-fixed chains
chains += pre_fixed_chains
if len(agent_tools) == 1:
if len(datasets) > 0:
# tool to chain
tool_chain = ChainBuilder.to_tool_chain(tool=agent_tools[0], output_key='tool_output')
chains.append(tool_chain)
elif len(agent_tools) > 1:
# build agent config
agent_chain = AgentBuilder.to_agent_chain(
multi_dataset_router_chain = MultiDatasetRouterChain.from_datasets(
tenant_id=tenant_id,
tools=agent_tools,
memory=memory,
dataset_tool_callback_handler=dataset_tool_callback_handler,
agent_loop_gather_callback_handler=agent_loop_gather_callback_handler
datasets=datasets,
conversation_message_task=conversation_message_task,
callback_manager=CallbackManager([DifyStdOutCallbackHandler()])
)
chains.append(agent_chain)
chains.append(multi_dataset_router_chain)
final_output_key = cls.get_chains_output_key(chains)
......
from typing import Mapping, List, Dict, Any, Optional
from langchain import LLMChain, PromptTemplate, ConversationChain
from langchain.callbacks import CallbackManager
from langchain.chains.base import Chain
from langchain.schema import BaseLanguageModel
from pydantic import Extra
from core.callback_handler.dataset_tool_callback_handler import DatasetToolCallbackHandler
from core.callback_handler.std_out_callback_handler import DifyStdOutCallbackHandler
from core.chain.llm_router_chain import LLMRouterChain, RouterOutputParser
from core.conversation_message_task import ConversationMessageTask
from core.llm.llm_builder import LLMBuilder
from core.tool.dataset_tool_builder import DatasetToolBuilder
from core.tool.llama_index_tool import EnhanceLlamaIndexTool
from models.dataset import Dataset
MULTI_PROMPT_ROUTER_TEMPLATE = """
Given a raw text input to a language model select the model prompt best suited for \
the input. You will be given the names of the available prompts and a description of \
what the prompt is best suited for. You may also revise the original input if you \
think that revising it will ultimately lead to a better response from the language \
model.
<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like, \
no any other string out of markdown code snippet:
```json
{{{{
"destination": string \\ name of the prompt to use or "DEFAULT"
"next_inputs": string \\ a potentially modified version of the original input
}}}}
```
REMEMBER: "destination" MUST be one of the candidate prompt names specified below OR \
it can be "DEFAULT" if the input is not well suited for any of the candidate prompts.
REMEMBER: "next_inputs" can just be the original input if you don't think any \
modifications are needed.
<< CANDIDATE PROMPTS >>
{destinations}
<< INPUT >>
{{input}}
<< OUTPUT >>
"""
class MultiDatasetRouterChain(Chain):
"""Use a single chain to route an input to one of multiple candidate chains."""
router_chain: LLMRouterChain
"""Chain for deciding a destination chain and the input to it."""
dataset_tools: Mapping[str, EnhanceLlamaIndexTool]
"""Map of name to candidate chains that inputs can be routed to."""
class Config:
"""Configuration for this pydantic object."""
extra = Extra.forbid
arbitrary_types_allowed = True
@property
def input_keys(self) -> List[str]:
"""Will be whatever keys the router chain prompt expects.
:meta private:
"""
return self.router_chain.input_keys
@property
def output_keys(self) -> List[str]:
return ["text"]
@classmethod
def from_datasets(
cls,
tenant_id: str,
datasets: List[Dataset],
conversation_message_task: ConversationMessageTask,
**kwargs: Any,
):
"""Convenience constructor for instantiating from destination prompts."""
llm_callback_manager = CallbackManager([DifyStdOutCallbackHandler()])
llm = LLMBuilder.to_llm(
tenant_id=tenant_id,
model_name='gpt-3.5-turbo',
temperature=0,
max_tokens=1024,
callback_manager=llm_callback_manager
)
destinations = ["{}: {}".format(d.id, d.description.replace('\n', ' ') if d.description
else ('useful for when you want to answer queries about the ' + d.name))
for d in datasets]
destinations_str = "\n".join(destinations)
router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(
destinations=destinations_str
)
router_prompt = PromptTemplate(
template=router_template,
input_variables=["input"],
output_parser=RouterOutputParser(),
)
router_chain = LLMRouterChain.from_llm(llm, router_prompt)
dataset_tools = {}
for dataset in datasets:
dataset_tool = DatasetToolBuilder.build_dataset_tool(
dataset=dataset,
response_mode='no_synthesizer', # "compact"
callback_handler=DatasetToolCallbackHandler(conversation_message_task)
)
if dataset_tool:
dataset_tools[dataset.id] = dataset_tool
return cls(
router_chain=router_chain,
dataset_tools=dataset_tools,
**kwargs,
)
def _call(
self,
inputs: Dict[str, Any]
) -> Dict[str, Any]:
if len(self.dataset_tools) == 0:
return {"text": ''}
elif len(self.dataset_tools) == 1:
return {"text": next(iter(self.dataset_tools.values())).run(inputs['input'])}
route = self.router_chain.route(inputs)
if not route.destination:
return {"text": ''}
elif route.destination in self.dataset_tools:
return {"text": self.dataset_tools[route.destination].run(
route.next_inputs['input']
)}
else:
raise ValueError(
f"Received invalid destination chain name '{route.destination}'"
)
import logging
from typing import Optional, List, Union, Tuple
from langchain.callbacks import CallbackManager
from langchain.chat_models.base import BaseChatModel
from langchain.llms import BaseLLM
from langchain.schema import BaseMessage, BaseLanguageModel, HumanMessage
from requests.exceptions import ChunkedEncodingError
from core.constant import llm_constant
from core.callback_handler.llm_callback_handler import LLMCallbackHandler
from core.callback_handler.std_out_callback_handler import DifyStreamingStdOutCallbackHandler, \
DifyStdOutCallbackHandler
from core.conversation_message_task import ConversationMessageTask, ConversationTaskStoppedException
from core.conversation_message_task import ConversationMessageTask, ConversationTaskStoppedException, PubHandler
from core.llm.error import LLMBadRequestError
from core.llm.llm_builder import LLMBuilder
from core.chain.main_chain_builder import MainChainBuilder
......@@ -84,6 +87,11 @@ class Completion:
)
except ConversationTaskStoppedException:
return
except ChunkedEncodingError as e:
# Interrupt by LLM (like OpenAI), handle it.
logging.warning(f'ChunkedEncodingError: {e}')
conversation_message_task.end()
return
@classmethod
def run_final_llm(cls, tenant_id: str, mode: str, app_model_config: AppModelConfig, query: str, inputs: dict,
......
......@@ -80,7 +80,10 @@ class ConversationMessageTask:
if introduction:
prompt_template = OutLinePromptTemplate.from_template(template=PromptBuilder.process_template(introduction))
prompt_inputs = {k: self.inputs[k] for k in prompt_template.input_variables if k in self.inputs}
try:
introduction = prompt_template.format(**prompt_inputs)
except KeyError:
pass
if self.app_model_config.pre_prompt:
pre_prompt = PromptBuilder.process_template(self.app_model_config.pre_prompt)
......@@ -171,7 +174,7 @@ class ConversationMessageTask:
)
if not by_stopped:
self._pub_handler.pub_end()
self.end()
def update_provider_quota(self):
llm_provider_service = LLMProviderService(
......@@ -268,6 +271,9 @@ class ConversationMessageTask:
total_price = message_tokens_per_1k * message_unit_price + answer_tokens_per_1k * answer_unit_price
return total_price.quantize(decimal.Decimal('0.0000001'), rounding=decimal.ROUND_HALF_UP)
def end(self):
self._pub_handler.pub_end()
class PubHandler:
def __init__(self, user: Union[Account | EndUser], task_id: str,
......
......@@ -173,6 +173,13 @@ class OpenAIEmbedding(BaseEmbedding):
Can be overriden for batch queries.
"""
if self.openai_api_type and self.openai_api_type == 'azure':
embeddings = []
for text in texts:
embeddings.append(self._get_text_embedding(text))
return embeddings
if self.deployment_name is not None:
engine = self.deployment_name
else:
......@@ -187,6 +194,13 @@ class OpenAIEmbedding(BaseEmbedding):
async def _aget_text_embeddings(self, texts: List[str]) -> List[List[float]]:
"""Asynchronously get text embeddings."""
if self.openai_api_type and self.openai_api_type == 'azure':
embeddings = []
for text in texts:
embeddings.append(await self._aget_text_embedding(text))
return embeddings
if self.deployment_name is not None:
engine = self.deployment_name
else:
......
......@@ -7,6 +7,7 @@ from core.constant import llm_constant
from core.llm.llm_builder import LLMBuilder
from core.llm.streamable_open_ai import StreamableOpenAI
from core.llm.token_calculator import TokenCalculator
from core.prompt.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser
from core.prompt.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser
from core.prompt.prompt_template import OutLinePromptTemplate
......@@ -118,3 +119,46 @@ class LLMGenerator:
questions = []
return questions
@classmethod
def generate_rule_config(cls, tenant_id: str, audiences: str, hoping_to_solve: str) -> dict:
output_parser = RuleConfigGeneratorOutputParser()
prompt = OutLinePromptTemplate(
template=output_parser.get_format_instructions(),
input_variables=["audiences", "hoping_to_solve"],
partial_variables={
"variable": '{variable}',
"lanA": '{lanA}',
"lanB": '{lanB}',
"topic": '{topic}'
},
validate_template=False
)
_input = prompt.format_prompt(audiences=audiences, hoping_to_solve=hoping_to_solve)
llm: StreamableOpenAI = LLMBuilder.to_llm(
tenant_id=tenant_id,
model_name=generate_base_model,
temperature=0,
max_tokens=512
)
if isinstance(llm, BaseChatModel):
query = [HumanMessage(content=_input.to_string())]
else:
query = _input.to_string()
try:
output = llm(query)
rule_config = output_parser.parse(output)
except Exception:
logging.exception("Error generating prompt")
rule_config = {
"prompt": "",
"variables": [],
"opening_statement": ""
}
return rule_config
......@@ -110,6 +110,8 @@ class AzureProvider(BaseProvider):
if missing_model_ids:
raise ValidateFailedError("Please add deployments for '{}'.".format(", ".join(missing_model_ids)))
except ValidateFailedError as e:
raise e
except AzureAuthenticationError:
raise ValidateFailedError('Validation failed, please check your API Key.')
except (requests.ConnectionError, requests.RequestException):
......
from typing import Any
from langchain.schema import BaseOutputParser, OutputParserException
from core.prompt.prompts import RULE_CONFIG_GENERATE_TEMPLATE
from libs.json_in_md_parser import parse_and_check_json_markdown
class RuleConfigGeneratorOutputParser(BaseOutputParser):
def get_format_instructions(self) -> str:
return RULE_CONFIG_GENERATE_TEMPLATE
def parse(self, text: str) -> Any:
try:
expected_keys = ["prompt", "variables", "opening_statement"]
parsed = parse_and_check_json_markdown(text, expected_keys)
if not isinstance(parsed["prompt"], str):
raise ValueError("Expected 'prompt' to be a string.")
if not isinstance(parsed["variables"], list):
raise ValueError(
f"Expected 'variables' to be a list."
)
if not isinstance(parsed["opening_statement"], str):
raise ValueError(
f"Expected 'opening_statement' to be a str."
)
return parsed
except Exception as e:
raise OutputParserException(
f"Parsing text\n{text}\n of rule config generator raised following error:\n{e}"
)
......@@ -32,6 +32,6 @@ class PromptBuilder:
@classmethod
def process_template(cls, template: str):
processed_template = re.sub(r'\{(.+?)\}', r'\1', template)
processed_template = re.sub(r'\{\{(.+?)\}\}', r'{\1}', processed_template)
processed_template = re.sub(r'\{([a-zA-Z_]\w+?)\}', r'\1', template)
processed_template = re.sub(r'\{\{([a-zA-Z_]\w+?)\}\}', r'{\1}', processed_template)
return processed_template
......@@ -61,3 +61,60 @@ QUERY_KEYWORD_EXTRACT_TEMPLATE_TMPL = (
QUERY_KEYWORD_EXTRACT_TEMPLATE = QueryKeywordExtractPrompt(
QUERY_KEYWORD_EXTRACT_TEMPLATE_TMPL
)
RULE_CONFIG_GENERATE_TEMPLATE = """Given MY INTENDED AUDIENCES and HOPING TO SOLVE using a language model, please select \
the model prompt that best suits the input.
You will be provided with the prompt, variables, and an opening statement.
Only the content enclosed in double curly braces, such as {{variable}}, in the prompt can be considered as a variable; \
otherwise, it cannot exist as a variable in the variables.
If you believe revising the original input will result in a better response from the language model, you may \
suggest revisions.
<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like, \
no any other string out of markdown code snippet:
```json
{{{{
"prompt": string \\ generated prompt
"variables": list of string \\ variables
"opening_statement": string \\ an opening statement to guide users on how to ask questions with generated prompt \
and fill in variables, with a welcome sentence, and keep TLDR.
}}}}
```
<< EXAMPLES >>
[EXAMPLE A]
```json
{
"prompt": "Write a letter about love",
"variables": [],
"opening_statement": "Hi! I'm your love letter writer AI."
}
```
[EXAMPLE B]
```json
{
"prompt": "Translate from {{lanA}} to {{lanB}}",
"variables": ["lanA", "lanB"],
"opening_statement": "Welcome to use translate app"
}
```
[EXAMPLE C]
```json
{
"prompt": "Write a story about {{topic}}",
"variables": ["topic"],
"opening_statement": "I'm your story writer"
}
```
<< MY INTENDED AUDIENCES >>
{audiences}
<< HOPING TO SOLVE >>
{hoping_to_solve}
<< OUTPUT >>
"""
\ No newline at end of file
......@@ -10,24 +10,14 @@ from core.index.keyword_table_index import KeywordTableIndex
from core.index.vector_index import VectorIndex
from core.prompt.prompts import QUERY_KEYWORD_EXTRACT_TEMPLATE
from core.tool.llama_index_tool import EnhanceLlamaIndexTool
from extensions.ext_database import db
from models.dataset import Dataset
class DatasetToolBuilder:
@classmethod
def build_dataset_tool(cls, tenant_id: str, dataset_id: str,
def build_dataset_tool(cls, dataset: Dataset,
response_mode: str = "no_synthesizer",
callback_handler: Optional[DatasetToolCallbackHandler] = None):
# get dataset from dataset id
dataset = db.session.query(Dataset).filter(
Dataset.tenant_id == tenant_id,
Dataset.id == dataset_id
).first()
if not dataset:
return None
if dataset.indexing_technique == "economy":
# use keyword table query
index = KeywordTableIndex(dataset=dataset).query_index
......@@ -65,7 +55,7 @@ class DatasetToolBuilder:
index_tool_config = IndexToolConfig(
index=index,
name=f"dataset-{dataset_id}",
name=f"dataset-{dataset.id}",
description=description,
index_query_kwargs=query_kwargs,
tool_kwargs={
......@@ -75,7 +65,7 @@ class DatasetToolBuilder:
# return_direct: Whether to return LLM results directly or process the output data with an Output Parser
)
index_callback_handler = DatasetIndexToolCallbackHandler(dataset_id=dataset_id)
index_callback_handler = DatasetIndexToolCallbackHandler(dataset_id=dataset.id)
return EnhanceLlamaIndexTool.from_tool_config(
tool_config=index_tool_config,
......
import json
from typing import List
from langchain.schema import OutputParserException
def parse_json_markdown(json_string: str) -> dict:
# Remove the triple backticks if present
json_string = json_string.strip()
start_index = json_string.find("```json")
end_index = json_string.find("```", start_index + len("```json"))
if start_index != -1 and end_index != -1:
extracted_content = json_string[start_index + len("```json"):end_index].strip()
# Parse the JSON string into a Python dictionary
parsed = json.loads(extracted_content)
elif json_string.startswith("{"):
# Parse the JSON string into a Python dictionary
parsed = json.loads(json_string)
else:
raise Exception("Could not find JSON block in the output.")
return parsed
def parse_and_check_json_markdown(text: str, expected_keys: List[str]) -> dict:
try:
json_obj = parse_json_markdown(text)
except json.JSONDecodeError as e:
raise OutputParserException(f"Got invalid JSON object. Error: {e}")
for key in expected_keys:
if key not in json_obj:
raise OutputParserException(
f"Got invalid return object. Expected key `{key}` "
f"to be present, but got {json_obj}"
)
return json_obj
"""add created by role
Revision ID: 9f4e3427ea84
Revises: 64b051264f32
Create Date: 2023-05-17 17:29:01.060435
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9f4e3427ea84'
down_revision = '64b051264f32'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('pinned_conversations', schema=None) as batch_op:
batch_op.add_column(sa.Column('created_by_role', sa.String(length=255), server_default=sa.text("'end_user'::character varying"), nullable=False))
batch_op.drop_index('pinned_conversation_conversation_idx')
batch_op.create_index('pinned_conversation_conversation_idx', ['app_id', 'conversation_id', 'created_by_role', 'created_by'], unique=False)
with op.batch_alter_table('saved_messages', schema=None) as batch_op:
batch_op.add_column(sa.Column('created_by_role', sa.String(length=255), server_default=sa.text("'end_user'::character varying"), nullable=False))
batch_op.drop_index('saved_message_message_idx')
batch_op.create_index('saved_message_message_idx', ['app_id', 'message_id', 'created_by_role', 'created_by'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('saved_messages', schema=None) as batch_op:
batch_op.drop_index('saved_message_message_idx')
batch_op.create_index('saved_message_message_idx', ['app_id', 'message_id', 'created_by'], unique=False)
batch_op.drop_column('created_by_role')
with op.batch_alter_table('pinned_conversations', schema=None) as batch_op:
batch_op.drop_index('pinned_conversation_conversation_idx')
batch_op.create_index('pinned_conversation_conversation_idx', ['app_id', 'conversation_id', 'created_by'], unique=False)
batch_op.drop_column('created_by_role')
# ### end Alembic commands ###
"""add language to recommend apps
Revision ID: a45f4dfde53b
Revises: 9f4e3427ea84
Create Date: 2023-05-25 17:50:32.052335
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a45f4dfde53b'
down_revision = '9f4e3427ea84'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('recommended_apps', schema=None) as batch_op:
batch_op.add_column(sa.Column('language', sa.String(length=255), server_default=sa.text("'en-US'::character varying"), nullable=False))
batch_op.drop_index('recommended_app_is_listed_idx')
batch_op.create_index('recommended_app_is_listed_idx', ['is_listed', 'language'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('recommended_apps', schema=None) as batch_op:
batch_op.drop_index('recommended_app_is_listed_idx')
batch_op.create_index('recommended_app_is_listed_idx', ['is_listed'], unique=False)
batch_op.drop_column('language')
# ### end Alembic commands ###
......@@ -123,7 +123,7 @@ class RecommendedApp(db.Model):
__table_args__ = (
db.PrimaryKeyConstraint('id', name='recommended_app_pkey'),
db.Index('recommended_app_app_id_idx', 'app_id'),
db.Index('recommended_app_is_listed_idx', 'is_listed')
db.Index('recommended_app_is_listed_idx', 'is_listed', 'language')
)
id = db.Column(UUID, primary_key=True, server_default=db.text('uuid_generate_v4()'))
......@@ -135,6 +135,7 @@ class RecommendedApp(db.Model):
position = db.Column(db.Integer, nullable=False, default=0)
is_listed = db.Column(db.Boolean, nullable=False, default=True)
install_count = db.Column(db.Integer, nullable=False, default=0)
language = db.Column(db.String(255), nullable=False, server_default=db.text("'en-US'::character varying"))
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
......@@ -143,17 +144,6 @@ class RecommendedApp(db.Model):
app = db.session.query(App).filter(App.id == self.app_id).first()
return app
# def set_description(self, lang, desc):
# if self.description is None:
# self.description = {}
# self.description[lang] = desc
def get_description(self, lang):
if self.description and lang in self.description:
return self.description[lang]
else:
return self.description.get('en')
class InstalledApp(db.Model):
__tablename__ = 'installed_apps'
......@@ -314,6 +304,10 @@ class Conversation(db.Model):
def app(self):
return db.session.query(App).filter(App.id == self.app_id).first()
@property
def in_debug_mode(self):
return self.override_model_configs is not None
class Message(db.Model):
__tablename__ = 'messages'
......@@ -380,6 +374,10 @@ class Message(db.Model):
return None
@property
def in_debug_mode(self):
return self.override_model_configs is not None
class MessageFeedback(db.Model):
__tablename__ = 'message_feedbacks'
......
......@@ -8,12 +8,13 @@ class SavedMessage(db.Model):
__tablename__ = 'saved_messages'
__table_args__ = (
db.PrimaryKeyConstraint('id', name='saved_message_pkey'),
db.Index('saved_message_message_idx', 'app_id', 'message_id', 'created_by'),
db.Index('saved_message_message_idx', 'app_id', 'message_id', 'created_by_role', 'created_by'),
)
id = db.Column(UUID, server_default=db.text('uuid_generate_v4()'))
app_id = db.Column(UUID, nullable=False)
message_id = db.Column(UUID, nullable=False)
created_by_role = db.Column(db.String(255), nullable=False, server_default=db.text("'end_user'::character varying"))
created_by = db.Column(UUID, nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
......@@ -26,11 +27,12 @@ class PinnedConversation(db.Model):
__tablename__ = 'pinned_conversations'
__table_args__ = (
db.PrimaryKeyConstraint('id', name='pinned_conversation_pkey'),
db.Index('pinned_conversation_conversation_idx', 'app_id', 'conversation_id', 'created_by'),
db.Index('pinned_conversation_conversation_idx', 'app_id', 'conversation_id', 'created_by_role', 'created_by'),
)
id = db.Column(UUID, server_default=db.text('uuid_generate_v4()'))
app_id = db.Column(UUID, nullable=False)
conversation_id = db.Column(UUID, nullable=False)
created_by_role = db.Column(db.String(255), nullable=False, server_default=db.text("'end_user'::character varying"))
created_by = db.Column(UUID, nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
......@@ -33,6 +33,10 @@ class CompletionService:
# is streaming mode
inputs = args['inputs']
query = args['query']
if not query:
raise ValueError('query is required')
conversation_id = args['conversation_id'] if 'conversation_id' in args else None
conversation = None
......
......@@ -127,7 +127,7 @@ class MessageService:
message_id=message_id
)
feedback = message.user_feedback
feedback = message.user_feedback if isinstance(user, EndUser) else message.admin_feedback
if not rating and feedback:
db.session.delete(feedback)
......
from typing import Optional
from typing import Optional, Union
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from extensions.ext_database import db
from models.account import Account
from models.model import App, EndUser
from models.web import SavedMessage
from services.message_service import MessageService
......@@ -9,27 +10,29 @@ from services.message_service import MessageService
class SavedMessageService:
@classmethod
def pagination_by_last_id(cls, app_model: App, end_user: Optional[EndUser],
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account | EndUser]],
last_id: Optional[str], limit: int) -> InfiniteScrollPagination:
saved_messages = db.session.query(SavedMessage).filter(
SavedMessage.app_id == app_model.id,
SavedMessage.created_by == end_user.id
SavedMessage.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
SavedMessage.created_by == user.id
).order_by(SavedMessage.created_at.desc()).all()
message_ids = [sm.message_id for sm in saved_messages]
return MessageService.pagination_by_last_id(
app_model=app_model,
user=end_user,
user=user,
last_id=last_id,
limit=limit,
include_ids=message_ids
)
@classmethod
def save(cls, app_model: App, user: Optional[EndUser], message_id: str):
def save(cls, app_model: App, user: Optional[Union[Account | EndUser]], message_id: str):
saved_message = db.session.query(SavedMessage).filter(
SavedMessage.app_id == app_model.id,
SavedMessage.message_id == message_id,
SavedMessage.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
SavedMessage.created_by == user.id
).first()
......@@ -45,6 +48,7 @@ class SavedMessageService:
saved_message = SavedMessage(
app_id=app_model.id,
message_id=message.id,
created_by_role='account' if isinstance(user, Account) else 'end_user',
created_by=user.id
)
......@@ -52,10 +56,11 @@ class SavedMessageService:
db.session.commit()
@classmethod
def delete(cls, app_model: App, user: Optional[EndUser], message_id: str):
def delete(cls, app_model: App, user: Optional[Union[Account | EndUser]], message_id: str):
saved_message = db.session.query(SavedMessage).filter(
SavedMessage.app_id == app_model.id,
SavedMessage.message_id == message_id,
SavedMessage.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
SavedMessage.created_by == user.id
).first()
......
......@@ -2,6 +2,7 @@ from typing import Optional, Union
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from extensions.ext_database import db
from models.account import Account
from models.model import App, EndUser
from models.web import PinnedConversation
from services.conversation_service import ConversationService
......@@ -9,14 +10,15 @@ from services.conversation_service import ConversationService
class WebConversationService:
@classmethod
def pagination_by_last_id(cls, app_model: App, end_user: Optional[EndUser],
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account | EndUser]],
last_id: Optional[str], limit: int, pinned: Optional[bool] = None) -> InfiniteScrollPagination:
include_ids = None
exclude_ids = None
if pinned is not None:
pinned_conversations = db.session.query(PinnedConversation).filter(
PinnedConversation.app_id == app_model.id,
PinnedConversation.created_by == end_user.id
PinnedConversation.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
PinnedConversation.created_by == user.id
).order_by(PinnedConversation.created_at.desc()).all()
pinned_conversation_ids = [pc.conversation_id for pc in pinned_conversations]
if pinned:
......@@ -26,7 +28,7 @@ class WebConversationService:
return ConversationService.pagination_by_last_id(
app_model=app_model,
user=end_user,
user=user,
last_id=last_id,
limit=limit,
include_ids=include_ids,
......@@ -34,10 +36,11 @@ class WebConversationService:
)
@classmethod
def pin(cls, app_model: App, conversation_id: str, user: Optional[EndUser]):
def pin(cls, app_model: App, conversation_id: str, user: Optional[Union[Account | EndUser]]):
pinned_conversation = db.session.query(PinnedConversation).filter(
PinnedConversation.app_id == app_model.id,
PinnedConversation.conversation_id == conversation_id,
PinnedConversation.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
PinnedConversation.created_by == user.id
).first()
......@@ -53,6 +56,7 @@ class WebConversationService:
pinned_conversation = PinnedConversation(
app_id=app_model.id,
conversation_id=conversation.id,
created_by_role='account' if isinstance(user, Account) else 'end_user',
created_by=user.id
)
......@@ -60,10 +64,11 @@ class WebConversationService:
db.session.commit()
@classmethod
def unpin(cls, app_model: App, conversation_id: str, user: Optional[EndUser]):
def unpin(cls, app_model: App, conversation_id: str, user: Optional[Union[Account | EndUser]]):
pinned_conversation = db.session.query(PinnedConversation).filter(
PinnedConversation.app_id == app_model.id,
PinnedConversation.conversation_id == conversation_id,
PinnedConversation.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
PinnedConversation.created_by == user.id
).first()
......
......@@ -2,7 +2,7 @@ version: '3.1'
services:
# API service
api:
image: langgenius/dify-api:latest
image: langgenius/dify-api:0.3.1
restart: always
environment:
# Startup mode, 'api' starts the API server.
......@@ -110,7 +110,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:latest
image: langgenius/dify-api:0.3.1
restart: always
environment:
# Startup mode, 'worker' starts the Celery worker for processing the queue.
......@@ -156,7 +156,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:latest
image: langgenius/dify-web:0.3.1
restart: always
environment:
EDITION: SELF_HOSTED
......
......@@ -11,7 +11,7 @@ class DifyClient {
public function __construct($api_key) {
$this->api_key = $api_key;
$this->base_url = "https://api.dify.ai/v1";
$this->base_url = "https://api.dify.ai/v1/";
$this->client = new Client([
'base_uri' => $this->base_url,
'headers' => [
......@@ -37,12 +37,12 @@ class DifyClient {
'rating' => $rating,
'user' => $user,
];
return $this->send_request('POST', "/messages/{$message_id}/feedbacks", $data);
return $this->send_request('POST', "messages/{$message_id}/feedbacks", $data);
}
public function get_application_parameters($user) {
$params = ['user' => $user];
return $this->send_request('GET', '/parameters', null, $params);
return $this->send_request('GET', 'parameters', null, $params);
}
}
......@@ -54,7 +54,7 @@ class CompletionClient extends DifyClient {
'response_mode' => $response_mode,
'user' => $user,
];
return $this->send_request('POST', '/completion-messages', $data, null, $response_mode === 'streaming');
return $this->send_request('POST', 'completion-messages', $data, null, $response_mode === 'streaming');
}
}
......@@ -70,7 +70,7 @@ class ChatClient extends DifyClient {
$data['conversation_id'] = $conversation_id;
}
return $this->send_request('POST', '/chat-messages', $data, null, $response_mode === 'streaming');
return $this->send_request('POST', 'chat-messages', $data, null, $response_mode === 'streaming');
}
public function get_conversation_messages($user, $conversation_id = null, $first_id = null, $limit = null) {
......@@ -86,7 +86,7 @@ class ChatClient extends DifyClient {
$params['limit'] = $limit;
}
return $this->send_request('GET', '/messages', null, $params);
return $this->send_request('GET', 'messages', null, $params);
}
public function get_conversations($user, $first_id = null, $limit = null, $pinned = null) {
......@@ -96,7 +96,7 @@ class ChatClient extends DifyClient {
'limit' => $limit,
'pinned'=> $pinned,
];
return $this->send_request('GET', '/conversations', null, $params);
return $this->send_request('GET', 'conversations', null, $params);
}
public function rename_conversation($conversation_id, $name, $user) {
......@@ -104,6 +104,6 @@ class ChatClient extends DifyClient {
'name' => $name,
'user' => $user,
];
return $this->send_request('PATCH', "/conversations/{$conversation_id}", $data);
return $this->send_request('PATCH', "conversations/{$conversation_id}", $data);
}
}
/**/node_modules/*
node_modules/
dist/
build/
out/
.next/
\ No newline at end of file
......@@ -23,6 +23,6 @@
]
}
],
"react-hooks/exhaustive-deps": "warning"
"react-hooks/exhaustive-deps": "warn"
}
}
\ No newline at end of file
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
cd ./web && npx lint-staged
{
"prettier.enable": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.format.enable": true,
"[python]": {
"editor.formatOnType": true
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[javascriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
\ No newline at end of file
......@@ -71,6 +71,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
<AppCard
className='mr-3 flex-1'
appInfo={response}
cardType='webapp'
onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode}
onSaveSiteConfig={onSaveSiteConfig} />
......
......@@ -3,8 +3,10 @@ import React, { useState } from 'react'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { fetchAppDetail } from '@/service/apps'
import type { PeriodParams } from '@/app/components/app/overview/appChart'
import { ConversationsChart, CostChart, EndUsersChart } from '@/app/components/app/overview/appChart'
import { AvgResponseTime, AvgSessionInteractions, ConversationsChart, CostChart, EndUsersChart, UserSatisfactionRate } from '@/app/components/app/overview/appChart'
import type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select'
import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter'
......@@ -20,6 +22,9 @@ export type IChartViewProps = {
}
export default function ChartView({ appId }: IChartViewProps) {
const detailParams = { url: '/apps', id: appId }
const { data: response } = useSWR(detailParams, fetchAppDetail)
const isChatApp = response?.mode === 'chat'
const { t } = useTranslation()
const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
......@@ -27,6 +32,9 @@ export default function ChartView({ appId }: IChartViewProps) {
setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
}
if (!response)
return null
return (
<div>
<div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
......@@ -46,6 +54,20 @@ export default function ChartView({ appId }: IChartViewProps) {
<EndUsersChart period={period} id={appId} />
</div>
</div>
<div className='flex flex-row w-full mb-6'>
<div className='flex-1 mr-3'>
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
)
: (
<AvgResponseTime period={period} id={appId} />
)}
</div>
<div className='flex-1 ml-3'>
<UserSatisfactionRate period={period} id={appId} />
</div>
</div>
<CostChart period={period} id={appId} />
</div>
)
......
......@@ -19,16 +19,16 @@ import I18n from '@/context/i18n'
type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill'
const STATUS_COLOR_MAP = {
normal: { color: '', bgColor: 'bg-primary-50', borderColor: 'border-primary-100' },
error: { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
verified: { color: '', bgColor: 'bg-green-50', borderColor: 'border-green-100' },
'normal': { color: '', bgColor: 'bg-primary-50', borderColor: 'border-primary-100' },
'error': { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
'verified': { color: '', bgColor: 'bg-green-50', borderColor: 'border-green-100' },
'error-api-key-exceed-bill': { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
}
const CheckCircleIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<rect width="20" height="20" rx="10" fill="#DEF7EC" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6947 6.70495C14.8259 6.83622 14.8996 7.01424 14.8996 7.19985C14.8996 7.38547 14.8259 7.56348 14.6947 7.69475L9.0947 13.2948C8.96343 13.426 8.78541 13.4997 8.5998 13.4997C8.41418 13.4997 8.23617 13.426 8.1049 13.2948L5.3049 10.4948C5.17739 10.3627 5.10683 10.1859 5.10842 10.0024C5.11002 9.81883 5.18364 9.64326 5.31342 9.51348C5.44321 9.38369 5.61878 9.31007 5.80232 9.30848C5.98585 9.30688 6.16268 9.37744 6.2947 9.50495L8.5998 11.8101L13.7049 6.70495C13.8362 6.57372 14.0142 6.5 14.1998 6.5C14.3854 6.5 14.5634 6.57372 14.6947 6.70495Z" fill="#046C4E" />
<path fillRule="evenodd" clipRule="evenodd" d="M14.6947 6.70495C14.8259 6.83622 14.8996 7.01424 14.8996 7.19985C14.8996 7.38547 14.8259 7.56348 14.6947 7.69475L9.0947 13.2948C8.96343 13.426 8.78541 13.4997 8.5998 13.4997C8.41418 13.4997 8.23617 13.426 8.1049 13.2948L5.3049 10.4948C5.17739 10.3627 5.10683 10.1859 5.10842 10.0024C5.11002 9.81883 5.18364 9.64326 5.31342 9.51348C5.44321 9.38369 5.61878 9.31007 5.80232 9.30848C5.98585 9.30688 6.16268 9.37744 6.2947 9.50495L8.5998 11.8101L13.7049 6.70495C13.8362 6.57372 14.0142 6.5 14.1998 6.5C14.3854 6.5 14.5634 6.57372 14.6947 6.70495Z" fill="#046C4E" />
</svg>
}
......@@ -81,11 +81,11 @@ const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, on
catch (err: any) {
if (err.status === 400) {
err.json().then(({ code }: any) => {
if (code === 'provider_request_failed') {
if (code === 'provider_request_failed')
setEditStatus('error-api-key-exceed-bill')
}
})
} else {
}
else {
setEditStatus('error')
}
}
......@@ -96,14 +96,14 @@ const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, on
const renderErrorMessage = () => {
if (validating) {
return (
<div className={`text-primary-600 mt-2 text-xs`}>
<div className={'text-primary-600 mt-2 text-xs'}>
{t('common.provider.validating')}
</div>
)
}
if (editStatus === 'error-api-key-exceed-bill') {
return (
<div className={`text-[#D92D20] mt-2 text-xs`}>
<div className={'text-[#D92D20] mt-2 text-xs'}>
{t('common.provider.apiKeyExceedBill')}
{locale === 'en' ? ' ' : ''}
<Link
......@@ -117,7 +117,7 @@ const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, on
}
if (editStatus === 'error') {
return (
<div className={`text-[#D92D20] mt-2 text-xs`}>
<div className={'text-[#D92D20] mt-2 text-xs'}>
{t('common.provider.invalidKey')}
</div>
)
......
......@@ -8,6 +8,8 @@ import NewAppCard from './NewAppCard'
import { AppListResponse } from '@/models/app'
import { fetchAppList } from '@/service/apps'
import { useSelector } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useTranslation } from 'react-i18next'
const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
if (!pageIndex || previousPageData.has_more)
......@@ -16,11 +18,20 @@ const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
}
const Apps = () => {
const { t } = useTranslation()
const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false })
const loadingStateRef = useRef(false)
const pageContainerRef = useSelector(state => state.pageContainerRef)
const anchorRef = useRef<HTMLAnchorElement>(null)
useEffect(() => {
document.title = `${t('app.title')} - Dify`;
if(localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate()
}
}, [])
useEffect(() => {
loadingStateRef.current = isLoading
}, [isLoading])
......
......@@ -37,7 +37,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
// Emoji Picker
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon: '🍌', icon_background: '#FFEAD5' })
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
......@@ -102,7 +102,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🍌', icon_background: '#FFEAD5' })
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}
......
......@@ -14,23 +14,19 @@ const AppList = async () => {
<footer className='px-12 py-6 grow-0 shrink-0'>
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3>
<p className='mt-1 text-sm font-normal leading-tight text-gray-700'>{t('communityIntro')}</p>
{/*<p className='mt-3 text-sm'>*/}
{/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}>*/}
{/* {t('roadmap')}*/}
{/* <span className={style.linkIcon} />*/}
{/* </a>*/}
{/*</p>*/}
{/* <p className='mt-3 text-sm'> */}
{/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}> */}
{/* {t('roadmap')} */}
{/* <span className={style.linkIcon} /> */}
{/* </a> */}
{/* </p> */}
<div className='flex items-center gap-2 mt-3'>
<a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
<a className={style.socialMediaLink} target='_blank' href='https://discord.gg/AhzKf7dNgk'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
<a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius/dify'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
<a className={style.socialMediaLink} target='_blank' href='https://discord.gg/FngNHpbcY7'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
</div>
</footer>
</div >
)
}
export const metadata = {
title: 'Apps - Dify',
}
export default AppList
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import { usePathname, useSelectedLayoutSegments } from 'next/navigation'
import { usePathname } from 'next/navigation'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { getLocaleOnClient } from '@/i18n/client'
import {
Cog8ToothIcon,
// CommandLineIcon,
Squares2X2Icon,
// eslint-disable-next-line sort-imports
PuzzlePieceIcon,
DocumentTextIcon,
} from '@heroicons/react/24/outline'
......@@ -18,9 +18,10 @@ import {
DocumentTextIcon as DocumentTextSolidIcon,
} from '@heroicons/react/24/solid'
import Link from 'next/link'
import s from './style.module.css'
import { fetchDataDetail, fetchDatasetRelatedApps } from '@/service/datasets'
import type { RelatedApp } from '@/models/datasets'
import s from './style.module.css'
import { getLocaleOnClient } from '@/i18n/client'
import AppSideBar from '@/app/components/app-sidebar'
import Divider from '@/app/components/base/divider'
import Indicator from '@/app/components/header/indicator'
......@@ -38,10 +39,10 @@ export type IAppDetailLayoutProps = {
const LikedItem: FC<{ type?: 'plugin' | 'app'; appStatus?: boolean; detail: RelatedApp }> = ({
type = 'app',
appStatus = true,
detail
detail,
}) => {
return (
<Link prefetch className={s.itemWrapper} href={`/app/${detail?.id}/overview`}>
<Link className={s.itemWrapper} href={`/app/${detail?.id}/overview`}>
<div className={s.iconWrapper}>
<AppIcon size='tiny' />
{type === 'app' && (
......@@ -58,7 +59,7 @@ const LikedItem: FC<{ type?: 'plugin' | 'app'; appStatus?: boolean; detail: Rela
const TargetIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<g clip-path="url(#clip0_4610_6951)">
<path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" />
<path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_4610_6951">
......@@ -70,7 +71,7 @@ const TargetIcon: FC<{ className?: string }> = ({ className }) => {
const TargetSolidIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
<path fillRule="evenodd" clipRule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
<path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
<path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
</svg>
......@@ -79,7 +80,7 @@ const TargetSolidIcon: FC<{ className?: string }> = ({ className }) => {
const BookOpenIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path opacity="0.12" d="M1 3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7V10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1Z" fill="#155EEF" />
<path d="M6 10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7M6 10.5V4.7M6 10.5L6.05003 10.425C6.39735 9.90398 6.57101 9.64349 6.80045 9.45491C7.00357 9.28796 7.23762 9.1627 7.4892 9.0863C7.77337 9 8.08645 9 8.71259 9H9.4C9.96005 9 10.2401 9 10.454 8.89101C10.6422 8.79513 10.7951 8.64215 10.891 8.45399C11 8.24008 11 7.96005 11 7.4V3.1C11 2.53995 11 2.25992 10.891 2.04601C10.7951 1.85785 10.6422 1.70487 10.454 1.60899C10.2401 1.5 9.96005 1.5 9.4 1.5H9.2C8.07989 1.5 7.51984 1.5 7.09202 1.71799C6.71569 1.90973 6.40973 2.21569 6.21799 2.59202C6 3.01984 6 3.5799 6 4.7" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round" />
<path d="M6 10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7M6 10.5V4.7M6 10.5L6.05003 10.425C6.39735 9.90398 6.57101 9.64349 6.80045 9.45491C7.00357 9.28796 7.23762 9.1627 7.4892 9.0863C7.77337 9 8.08645 9 8.71259 9H9.4C9.96005 9 10.2401 9 10.454 8.89101C10.6422 8.79513 10.7951 8.64215 10.891 8.45399C11 8.24008 11 7.96005 11 7.4V3.1C11 2.53995 11 2.25992 10.891 2.04601C10.7951 1.85785 10.6422 1.70487 10.454 1.60899C10.2401 1.5 9.96005 1.5 9.4 1.5H9.2C8.07989 1.5 7.51984 1.5 7.09202 1.71799C6.71569 1.90973 6.40973 2.21569 6.21799 2.59202C6 3.01984 6 3.5799 6 4.7" stroke="#155EEF" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
......@@ -109,9 +110,8 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
]
useEffect(() => {
if (datasetRes) {
if (datasetRes)
document.title = `${datasetRes.name || 'Dataset'} - Dify`
}
}, [datasetRes])
const ExtraInfo: FC = () => {
......@@ -119,12 +119,14 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
return <div className='w-full'>
<Divider className='mt-5' />
{relatedApps?.data?.length ? (
{relatedApps?.data?.length
? (
<>
<div className={s.subTitle}>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>
{relatedApps?.data?.map((item) => (<LikedItem detail={item} />))}
{relatedApps?.data?.map(item => (<LikedItem detail={item} />))}
</>
) : (
)
: (
<div className='mt-5 p-3'>
<div className='flex items-center justify-start gap-2'>
<div className={s.emptyIconDiv}>
......
......@@ -3,7 +3,13 @@ import { getLocaleOnServer } from '@/i18n/server'
import { useTranslation } from '@/i18n/i18next-serverside-config'
import Form from '@/app/components/datasets/settings/form'
const Settings = async () => {
type Props = {
params: { datasetId: string }
}
const Settings = async ({
params: { datasetId },
}: Props) => {
const locale = getLocaleOnServer()
const { t } = await useTranslation(locale, 'dataset-settings')
......@@ -14,7 +20,7 @@ const Settings = async () => {
<div className='text-sm text-gray-500'>{t('desc')}</div>
</div>
<div>
<Form />
<Form datasetId={datasetId} />
</div>
</div>
)
......
import AppList from "@/app/components/explore/app-list"
import React from 'react'
const Apps = ({ }) => {
return <AppList />
}
export default React.memo(Apps)
import React, { FC } from 'react'
import Main from '@/app/components/explore/installed-app'
export interface IInstalledAppProps {
params: {
appId: string
}
}
const InstalledApp: FC<IInstalledAppProps> = ({ params: {appId} }) => {
return (
<Main id={appId} />
)
}
export default React.memo(InstalledApp)
import type { FC } from 'react'
import React from 'react'
import ExploreClient from '@/app/components/explore'
export type IAppDetail = {
children: React.ReactNode
}
const AppDetail: FC<IAppDetail> = ({ children }) => {
return (
<ExploreClient>
{children}
</ExploreClient>
)
}
export default React.memo(AppDetail)
......@@ -4,12 +4,10 @@ import React from 'react'
import type { IMainProps } from '@/app/components/share/chat'
import Main from '@/app/components/share/chat'
const Chat: FC<IMainProps> = ({
params,
}: any) => {
const Chat: FC<IMainProps> = () => {
return (
<Main params={params} />
<Main />
)
}
......
......@@ -14,32 +14,37 @@ export function randomString(length: number) {
}
export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset'
icon?: string,
icon_background?: string,
iconType?: 'app' | 'api' | 'dataset' | 'webapp'
icon?: string
icon_background?: string
name: string
type: string | React.ReactNode
hoverTip?: string
textStyle?: { main?: string; extra?: string }
}
const AlgorithmSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12.5 9H1.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
const ApiSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9H1.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
<path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
</svg>
const WebappSvg = <svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.375 5.45825L7.99998 8.99992M7.99998 8.99992L1.62498 5.45825M7.99998 8.99992L8 16.1249M14.75 12.0439V5.95603C14.75 5.69904 14.75 5.57055 14.7121 5.45595C14.6786 5.35457 14.6239 5.26151 14.5515 5.18299C14.4697 5.09424 14.3574 5.03184 14.1328 4.90704L8.58277 1.8237C8.37007 1.70553 8.26372 1.64645 8.15109 1.62329C8.05141 1.60278 7.9486 1.60278 7.84891 1.62329C7.73628 1.64645 7.62993 1.70553 7.41723 1.8237L1.86723 4.90704C1.64259 5.03184 1.53026 5.09424 1.44847 5.18299C1.37612 5.26151 1.32136 5.35457 1.28786 5.45595C1.25 5.57055 1.25 5.69904 1.25 5.95603V12.0439C1.25 12.3008 1.25 12.4293 1.28786 12.5439C1.32136 12.6453 1.37612 12.7384 1.44847 12.8169C1.53026 12.9056 1.64259 12.968 1.86723 13.0928L7.41723 16.1762C7.62993 16.2943 7.73628 16.3534 7.84891 16.3766C7.9486 16.3971 8.05141 16.3971 8.15109 16.3766C8.26372 16.3534 8.37007 16.2943 8.58277 16.1762L14.1328 13.0928C14.3574 12.968 14.4697 12.9056 14.5515 12.8169C14.6239 12.7384 14.6786 12.6453 14.7121 12.5439C14.75 12.4293 14.75 12.3008 14.75 12.0439Z" stroke="#155EEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
const ICON_MAP = {
'app': <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
'api': <AppIcon innerIcon={AlgorithmSvg} className='border !bg-purple-50 !border-purple-200' />,
'dataset': <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />
app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
api: <AppIcon innerIcon={ApiSvg} className='border !bg-purple-50 !border-purple-200' />,
dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />,
webapp: <AppIcon innerIcon={WebappSvg} className='border !bg-primary-100 !border-primary-200' />,
}
export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, iconType = 'app' }: IAppBasicProps) {
......@@ -50,8 +55,8 @@ export default function AppBasic({ icon, icon_background, name, type, hoverTip,
<AppIcon icon={icon} background={icon_background} />
</div>
)}
{iconType !== 'app' &&
<div className='flex-shrink-0 mr-3'>
{iconType !== 'app'
&& <div className='flex-shrink-0 mr-3'>
{ICON_MAP[iconType]}
</div>
......
......@@ -18,7 +18,6 @@ export default function NavLink({
return (
<Link
prefetch
key={name}
href={href}
className={classNames(
......
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useContext } from 'use-context-selector'
import cn from 'classnames'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
......@@ -8,6 +8,8 @@ import { UserCircleIcon } from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next'
import { randomString } from '../../app-sidebar/basic'
import s from './style.module.css'
import LoadingAnim from './loading-anim'
import CopyBtn from './copy-btn'
import Tooltip from '@/app/components/base/tooltip'
import { ToastContext } from '@/app/components/base/toast'
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
......@@ -15,13 +17,12 @@ import Button from '@/app/components/base/button'
import type { Annotation, MessageRating } from '@/models/log'
import AppContext from '@/context/app-context'
import { Markdown } from '@/app/components/base/markdown'
import LoadingAnim from './loading-anim'
import { formatNumber } from '@/utils/format'
import CopyBtn from './copy-btn'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
const stopIcon = (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00004 0.583313C3.45621 0.583313 0.583374 3.45615 0.583374 6.99998C0.583374 10.5438 3.45621 13.4166 7.00004 13.4166C10.5439 13.4166 13.4167 10.5438 13.4167 6.99998C13.4167 3.45615 10.5439 0.583313 7.00004 0.583313ZM4.73029 4.98515C4.66671 5.10993 4.66671 5.27328 4.66671 5.59998V8.39998C4.66671 8.72668 4.66671 8.89003 4.73029 9.01481C4.78621 9.12457 4.87545 9.21381 4.98521 9.26973C5.10999 9.33331 5.27334 9.33331 5.60004 9.33331H8.40004C8.72674 9.33331 8.89009 9.33331 9.01487 9.26973C9.12463 9.21381 9.21387 9.12457 9.2698 9.01481C9.33337 8.89003 9.33337 8.72668 9.33337 8.39998V5.59998C9.33337 5.27328 9.33337 5.10993 9.2698 4.98515C9.21387 4.87539 9.12463 4.78615 9.01487 4.73023C8.89009 4.66665 8.72674 4.66665 8.40004 4.66665H5.60004C5.27334 4.66665 5.10999 4.66665 4.98521 4.73023C4.87545 4.78615 4.78621 4.87539 4.73029 4.98515Z" fill="#667085" />
<path fillRule="evenodd" clipRule="evenodd" d="M7.00004 0.583313C3.45621 0.583313 0.583374 3.45615 0.583374 6.99998C0.583374 10.5438 3.45621 13.4166 7.00004 13.4166C10.5439 13.4166 13.4167 10.5438 13.4167 6.99998C13.4167 3.45615 10.5439 0.583313 7.00004 0.583313ZM4.73029 4.98515C4.66671 5.10993 4.66671 5.27328 4.66671 5.59998V8.39998C4.66671 8.72668 4.66671 8.89003 4.73029 9.01481C4.78621 9.12457 4.87545 9.21381 4.98521 9.26973C5.10999 9.33331 5.27334 9.33331 5.60004 9.33331H8.40004C8.72674 9.33331 8.89009 9.33331 9.01487 9.26973C9.12463 9.21381 9.21387 9.12457 9.2698 9.01481C9.33337 8.89003 9.33337 8.72668 9.33337 8.39998V5.59998C9.33337 5.27328 9.33337 5.10993 9.2698 4.98515C9.21387 4.87539 9.12463 4.78615 9.01487 4.73023C8.89009 4.66665 8.72674 4.66665 8.40004 4.66665H5.60004C5.27334 4.66665 5.10999 4.66665 4.98521 4.73023C4.87545 4.78615 4.78621 4.87539 4.73029 4.98515Z" fill="#667085" />
</svg>
)
export type Feedbacktype = {
......@@ -131,8 +132,8 @@ const EditIcon: FC<{ className?: string }> = ({ className }) => {
export const EditIconSolid: FC<{ className?: string }> = ({ className }) => {
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path fill-rule="evenodd" clipRule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" />
<path fill-rule="evenodd" clipRule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" />
<path fillRule="evenodd" clipRule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" />
<path fillRule="evenodd" clipRule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" />
</svg>
}
......@@ -285,8 +286,8 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
<div key={id}>
<div className='flex items-start'>
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
{isResponsing &&
<div className={s.typeingIcon}>
{isResponsing
&& <div className={s.typeingIcon}>
<LoadingAnim type='avatar' />
</div>
}
......@@ -301,11 +302,13 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
<div className='text-xs text-gray-500'>{t('appDebug.openingStatement.title')}</div>
</div>
)}
{(isResponsing && !content) ? (
{(isResponsing && !content)
? (
<div className='flex items-center justify-center w-6 h-5'>
<LoadingAnim type='text' />
</div>
) : (
)
: (
<Markdown content={content} />
)}
{!showEdit
......@@ -384,11 +387,13 @@ const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar
</div>
{more && <MoreInfo more={more} isQuestion={true} />}
</div>
{useCurrentUserAvatar ? (
{useCurrentUserAvatar
? (
<div className='w-10 h-10 shrink-0 leading-10 text-center mr-2 rounded-full bg-primary-600 text-white'>
{userName?.[0].toLocaleUpperCase()}
</div>
) : (
)
: (
<div className={`${s.questionIcon} w-10 h-10 shrink-0 `}></div>
)}
</div>
......@@ -411,7 +416,7 @@ const Chat: FC<IChatProps> = ({
controlClearQuery,
controlFocus,
isShowSuggestion,
suggestionList
suggestionList,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
......@@ -436,29 +441,26 @@ const Chat: FC<IChatProps> = ({
}
useEffect(() => {
if (controlClearQuery) {
if (controlClearQuery)
setQuery('')
}
}, [controlClearQuery])
const handleSend = () => {
if (!valid() || (checkCanSend && !checkCanSend()))
return
onSend(query)
if (!isResponsing) {
if (!isResponsing)
setQuery('')
}
}
const handleKeyUp = (e: any) => {
if (e.code === 'Enter') {
e.preventDefault()
// prevent send message when using input method enter
if (!e.shiftKey && !isUseInputMethod.current) {
if (!e.shiftKey && !isUseInputMethod.current)
handleSend()
}
}
}
const haneleKeyDown = (e: any) => {
isUseInputMethod.current = e.nativeEvent.isComposing
......@@ -468,6 +470,20 @@ const Chat: FC<IChatProps> = ({
}
}
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const sendBtn = <div className={cn(!(!query || query.trim() === '') && s.sendBtnActive, `${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`)} onClick={handleSend}></div>
const suggestionListRef = useRef<HTMLDivElement>(null)
const [hasScrollbar, setHasScrollbar] = useState(false)
useLayoutEffect(() => {
if (suggestionListRef.current) {
const listDom = suggestionListRef.current
const hasScrollbar = listDom.scrollWidth > listDom.clientWidth
setHasScrollbar(hasScrollbar)
}
}, [suggestionList])
return (
<div className={cn(!feedbackDisabled && 'px-3.5', 'h-full')}>
{/* Chat List */}
......@@ -506,7 +522,7 @@ const Chat: FC<IChatProps> = ({
<div className='flex items-center justify-center mb-2.5'>
<div className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)'
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
}}></div>
<div className='shrink-0 flex items-center px-3 space-x-1'>
{TryToAskIcon}
......@@ -514,10 +530,11 @@ const Chat: FC<IChatProps> = ({
</div>
<div className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)'
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
}}></div>
</div>
<div className='flex justify-center overflow-x-scroll pb-2'>
{/* has scrollbar would hide part of first item */}
<div ref={suggestionListRef} className={cn(!hasScrollbar && 'justify-center', 'flex overflow-x-auto pb-2')}>
{suggestionList?.map((item, index) => (
<div className='shrink-0 flex justify-center mr-2'>
<Button
......@@ -544,6 +561,9 @@ const Chat: FC<IChatProps> = ({
/>
<div className="absolute top-0 right-2 flex items-center h-[48px]">
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
{isMobile
? sendBtn
: (
<Tooltip
selector='send-tip'
htmlContent={
......@@ -553,8 +573,9 @@ const Chat: FC<IChatProps> = ({
</div>
}
>
<div className={`${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`} onClick={handleSend}></div>
{sendBtn}
</Tooltip>
)}
</div>
</div>
</div>
......
......@@ -102,6 +102,10 @@
background: url(./icons/send.svg) center center no-repeat;
}
.sendBtnActive {
background-image: url(./icons/send-active.svg);
}
.sendBtn:hover {
background-image: url(./icons/send-active.svg);
background-color: #EBF5FF;
......
'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
const SuggestedQuestionsAfterAnswerIcon: FC = () => {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8275 1.33325H5.17245C4.63581 1.33324 4.19289 1.33324 3.8321 1.36272C3.45737 1.39333 3.1129 1.45904 2.78934 1.6239C2.28758 1.87956 1.87963 2.28751 1.62397 2.78928C1.45911 3.11284 1.3934 3.4573 1.36278 3.83204C1.3333 4.19283 1.33331 4.63574 1.33332 5.17239L1.33328 9.42497C1.333 9.95523 1.33278 10.349 1.42418 10.6901C1.67076 11.6103 2.38955 12.3291 3.3098 12.5757C3.51478 12.6306 3.73878 12.6525 3.99998 12.6611L3.99998 13.5806C3.99995 13.7374 3.99992 13.8973 4.01182 14.0283C4.0232 14.1536 4.05333 14.3901 4.21844 14.5969C4.40843 14.8349 4.69652 14.9734 5.00106 14.973C5.26572 14.9728 5.46921 14.8486 5.57416 14.7792C5.6839 14.7066 5.80872 14.6067 5.93117 14.5087L7.53992 13.2217C7.88564 12.9451 7.98829 12.8671 8.09494 12.8126C8.20192 12.7579 8.3158 12.718 8.43349 12.6938C8.55081 12.6697 8.67974 12.6666 9.12248 12.6666H10.8275C11.3642 12.6666 11.8071 12.6666 12.1679 12.6371C12.5426 12.6065 12.8871 12.5408 13.2106 12.3759C13.7124 12.1203 14.1203 11.7123 14.376 11.2106C14.5409 10.887 14.6066 10.5425 14.6372 10.1678C14.6667 9.80701 14.6667 9.36411 14.6667 8.82747V5.17237C14.6667 4.63573 14.6667 4.19283 14.6372 3.83204C14.6066 3.4573 14.5409 3.11284 14.376 2.78928C14.1203 2.28751 13.7124 1.87956 13.2106 1.6239C12.8871 1.45904 12.5426 1.39333 12.1679 1.36272C11.8071 1.33324 11.3642 1.33324 10.8275 1.33325ZM8.99504 4.99992C8.99504 4.44763 9.44275 3.99992 9.99504 3.99992C10.5473 3.99992 10.995 4.44763 10.995 4.99992C10.995 5.5522 10.5473 5.99992 9.99504 5.99992C9.44275 5.99992 8.99504 5.5522 8.99504 4.99992ZM4.92837 7.79996C5.222 7.57974 5.63816 7.63837 5.85961 7.93051C5.90071 7.98295 5.94593 8.03229 5.99199 8.08035C6.09019 8.18282 6.23775 8.32184 6.42882 8.4608C6.81353 8.74059 7.3454 8.99996 7.99504 8.99996C8.64469 8.99996 9.17655 8.74059 9.56126 8.4608C9.75233 8.32184 9.89989 8.18282 9.99809 8.08035C10.0441 8.0323 10.0894 7.98294 10.1305 7.93051C10.3519 7.63837 10.7681 7.57974 11.0617 7.79996C11.3563 8.02087 11.416 8.43874 11.195 8.73329C11.1967 8.73112 11.1928 8.7361 11.186 8.74466C11.1697 8.7651 11.1372 8.80597 11.1261 8.81916C11.087 8.86575 11.0317 8.92884 10.9607 9.00289C10.8194 9.15043 10.6128 9.34474 10.3455 9.53912C9.81353 9.92599 9.01206 10.3333 7.99504 10.3333C6.97802 10.3333 6.17655 9.92599 5.64459 9.53912C5.37733 9.34474 5.17072 9.15043 5.02934 9.00289C4.95837 8.92884 4.90305 8.86575 4.86395 8.81916C4.84438 8.79585 4.82881 8.77659 4.81731 8.76207C4.58702 8.46455 4.61798 8.03275 4.92837 7.79996ZM5.99504 3.99992C5.44275 3.99992 4.99504 4.44763 4.99504 4.99992C4.99504 5.5522 5.44275 5.99992 5.99504 5.99992C6.54732 5.99992 6.99504 5.5522 6.99504 4.99992C6.99504 4.44763 6.54732 3.99992 5.99504 3.99992Z" fill="#06AED4" />
<path fillRule="evenodd" clipRule="evenodd" d="M10.8275 1.33325H5.17245C4.63581 1.33324 4.19289 1.33324 3.8321 1.36272C3.45737 1.39333 3.1129 1.45904 2.78934 1.6239C2.28758 1.87956 1.87963 2.28751 1.62397 2.78928C1.45911 3.11284 1.3934 3.4573 1.36278 3.83204C1.3333 4.19283 1.33331 4.63574 1.33332 5.17239L1.33328 9.42497C1.333 9.95523 1.33278 10.349 1.42418 10.6901C1.67076 11.6103 2.38955 12.3291 3.3098 12.5757C3.51478 12.6306 3.73878 12.6525 3.99998 12.6611L3.99998 13.5806C3.99995 13.7374 3.99992 13.8973 4.01182 14.0283C4.0232 14.1536 4.05333 14.3901 4.21844 14.5969C4.40843 14.8349 4.69652 14.9734 5.00106 14.973C5.26572 14.9728 5.46921 14.8486 5.57416 14.7792C5.6839 14.7066 5.80872 14.6067 5.93117 14.5087L7.53992 13.2217C7.88564 12.9451 7.98829 12.8671 8.09494 12.8126C8.20192 12.7579 8.3158 12.718 8.43349 12.6938C8.55081 12.6697 8.67974 12.6666 9.12248 12.6666H10.8275C11.3642 12.6666 11.8071 12.6666 12.1679 12.6371C12.5426 12.6065 12.8871 12.5408 13.2106 12.3759C13.7124 12.1203 14.1203 11.7123 14.376 11.2106C14.5409 10.887 14.6066 10.5425 14.6372 10.1678C14.6667 9.80701 14.6667 9.36411 14.6667 8.82747V5.17237C14.6667 4.63573 14.6667 4.19283 14.6372 3.83204C14.6066 3.4573 14.5409 3.11284 14.376 2.78928C14.1203 2.28751 13.7124 1.87956 13.2106 1.6239C12.8871 1.45904 12.5426 1.39333 12.1679 1.36272C11.8071 1.33324 11.3642 1.33324 10.8275 1.33325ZM8.99504 4.99992C8.99504 4.44763 9.44275 3.99992 9.99504 3.99992C10.5473 3.99992 10.995 4.44763 10.995 4.99992C10.995 5.5522 10.5473 5.99992 9.99504 5.99992C9.44275 5.99992 8.99504 5.5522 8.99504 4.99992ZM4.92837 7.79996C5.222 7.57974 5.63816 7.63837 5.85961 7.93051C5.90071 7.98295 5.94593 8.03229 5.99199 8.08035C6.09019 8.18282 6.23775 8.32184 6.42882 8.4608C6.81353 8.74059 7.3454 8.99996 7.99504 8.99996C8.64469 8.99996 9.17655 8.74059 9.56126 8.4608C9.75233 8.32184 9.89989 8.18282 9.99809 8.08035C10.0441 8.0323 10.0894 7.98294 10.1305 7.93051C10.3519 7.63837 10.7681 7.57974 11.0617 7.79996C11.3563 8.02087 11.416 8.43874 11.195 8.73329C11.1967 8.73112 11.1928 8.7361 11.186 8.74466C11.1697 8.7651 11.1372 8.80597 11.1261 8.81916C11.087 8.86575 11.0317 8.92884 10.9607 9.00289C10.8194 9.15043 10.6128 9.34474 10.3455 9.53912C9.81353 9.92599 9.01206 10.3333 7.99504 10.3333C6.97802 10.3333 6.17655 9.92599 5.64459 9.53912C5.37733 9.34474 5.17072 9.15043 5.02934 9.00289C4.95837 8.92884 4.90305 8.86575 4.86395 8.81916C4.84438 8.79585 4.82881 8.77659 4.81731 8.76207C4.58702 8.46455 4.61798 8.03275 4.92837 7.79996ZM5.99504 3.99992C5.44275 3.99992 4.99504 4.44763 4.99504 4.99992C4.99504 5.5522 5.44275 5.99992 5.99504 5.99992C6.54732 5.99992 6.99504 5.5522 6.99504 4.99992C6.99504 4.44763 6.54732 3.99992 5.99504 3.99992Z" fill="#06AED4" />
</svg>
)
}
......
......@@ -29,6 +29,7 @@ const options = [
{ id: 'gpt-4', name: 'gpt-4', type: AppType.chat }, // 8k version
{ id: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo', type: AppType.completion },
{ id: 'text-davinci-003', name: 'text-davinci-003', type: AppType.completion },
{ id: 'gpt-4', name: 'gpt-4', type: AppType.completion }, // 8k version
]
const ModelIcon = ({ className }: { className?: string }) => (
......@@ -205,14 +206,14 @@ const ConifgModel: FC<IConifgModelProps> = ({
<div className="flex items-center justify-between my-5 h-9">
<div>{t('appDebug.modelConfig.model')}</div>
{/* model selector */}
<div className="relative">
<div className="relative" style={{zIndex: 30}}>
<div ref={triggerRef} onClick={() => !selectModelDisabled && toogleOption()} className={cn(selectModelDisabled ? 'cursor-not-allowed' : 'cursor-pointer', "flex items-center h-9 px-3 space-x-2 rounded-lg bg-gray-50 ")}>
<ModelIcon />
<div className="text-sm gray-900">{selectedModel?.name}</div>
{!selectModelDisabled && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />}
</div>
{isShowOption && (
<div className={cn(isChatApp ? 'w-[159px]' : 'w-[179px]', "absolute right-0 bg-gray-50 rounded-lg")}>
<div className={cn(isChatApp ? 'w-[159px]' : 'w-[179px]', "absolute right-0 bg-gray-50 rounded-lg shadow")}>
{availableModels.map(item => (
<div key={item.id} onClick={handleSelectModel(item.id)} className="flex items-center h-9 px-3 rounded-lg cursor-pointer hover:bg-gray-100">
<ModelIcon className='mr-2' />
......
'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import s from './style.module.css'
import Switch from '@/app/components/base/switch'
export interface IFeatureItemProps {
export type IFeatureItemProps = {
icon: React.ReactNode
previewImgClassName?: string
title: string
description: string
value: boolean
......@@ -12,13 +16,14 @@ export interface IFeatureItemProps {
const FeatureItem: FC<IFeatureItemProps> = ({
icon,
previewImgClassName,
title,
description,
value,
onChange
onChange,
}) => {
return (
<div className='flex justify-between p-3 rounded-xl border border-transparent bg-gray-50 hover:border-gray-200 cursor-pointer'>
<div className={cn(s.wrap, 'relative flex justify-between p-3 rounded-xl border border-transparent bg-gray-50 hover:border-gray-200 cursor-pointer')}>
<div className='flex space-x-3 mr-2'>
{/* icon */}
<div
......@@ -36,6 +41,11 @@ const FeatureItem: FC<IFeatureItemProps> = ({
</div>
<Switch onChange={onChange} defaultValue={value} />
{
previewImgClassName && (
<div className={cn(s.preview, s[previewImgClassName])}>
</div>)
}
</div>
)
}
......
<svg width="304" height="384" viewBox="0 0 304 384" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g filter="url(#filter0_dd_3472_38727)">
<g clip-path="url(#clip0_3472_38727)">
<rect x="12" width="280" height="360" rx="12" fill="white"/>
<mask id="path-2-inside-1_3472_38727" fill="white">
<path d="M12 0H292V32H12V0Z"/>
</mask>
<path d="M12 0H292V32H12V0Z" fill="#F9FAFB"/>
<rect x="118" y="8" width="16" height="16" rx="4" fill="#FFE4E8"/>
<path d="M120.833 21.7467H131.5V11.08H120.833V21.7467Z" fill="url(#pattern0)"/>
<rect x="118.167" y="8.16667" width="15.6667" height="15.6667" rx="3.83333" stroke="black" stroke-opacity="0.05" stroke-width="0.333333"/>
<g opacity="0.12">
<rect x="142" y="14" width="44" height="4" rx="2" fill="#101828"/>
</g>
<path d="M292 31H12V33H292V31Z" fill="#F2F4F7" mask="url(#path-2-inside-1_3472_38727)"/>
<rect width="280" height="328" transform="translate(12 32)" fill="#F9FAFB"/>
<g filter="url(#filter1_d_3472_38727)">
<g clip-path="url(#clip1_3472_38727)">
<rect x="30" y="48" width="244" height="100" rx="9" fill="white"/>
<path d="M50.0625 64.375L48.9375 71.125M53.0625 64.375L51.9375 71.125M54.1875 66.25H47.8125M53.8125 69.25H47.4375" stroke="#98A2B3" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M59.2764 70.8496C58.8135 70.8496 58.4385 70.7285 58.1514 70.4863C57.8643 70.2422 57.6729 69.8887 57.5771 69.4258C57.4814 68.9629 57.4863 68.4053 57.5918 67.7529C57.6992 67.1025 57.8779 66.5469 58.1279 66.0859C58.3799 65.625 58.6895 65.2734 59.0566 65.0313C59.4258 64.7891 59.8398 64.668 60.2988 64.668C60.7559 64.668 61.1289 64.79 61.418 65.0342C61.707 65.2764 61.9014 65.627 62.001 66.0859C62.1025 66.5449 62.0986 67.1006 61.9893 67.7529C61.8799 68.4072 61.7002 68.9648 61.4502 69.4258C61.2002 69.8867 60.8906 70.2393 60.5215 70.4834C60.1523 70.7275 59.7373 70.8496 59.2764 70.8496ZM59.4053 70.0674C59.8135 70.0674 60.165 69.8691 60.46 69.4727C60.7549 69.0762 60.9648 68.5029 61.0898 67.7529C61.1719 67.2549 61.1885 66.834 61.1396 66.4902C61.0928 66.1445 60.9863 65.8828 60.8203 65.7051C60.6563 65.5254 60.4395 65.4355 60.1699 65.4355C59.7676 65.4355 59.418 65.6357 59.1211 66.0361C58.8262 66.4346 58.6172 67.0068 58.4941 67.7529C58.4102 68.2529 58.3916 68.6758 58.4385 69.0215C58.4854 69.3652 58.5908 69.626 58.7549 69.8037C58.9189 69.9795 59.1357 70.0674 59.4053 70.0674ZM65.8 64.75L64.804 70.75H63.8958L64.7395 65.6582H64.7043L63.1135 66.5957L63.26 65.7285L64.9182 64.75H65.8Z" fill="#667085"/>
<rect x="42.375" y="60.375" width="28.75" height="14.75" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
<g opacity="0.1">
<rect x="42" y="81.5" width="220" height="4" rx="2" fill="#101828"/>
</g>
<g opacity="0.1">
<rect x="42" y="91.5" width="220" height="4" rx="2" fill="#101828"/>
</g>
<g opacity="0.1">
<rect x="42" y="101.5" width="92" height="4" rx="2" fill="#101828"/>
</g>
<rect x="42.375" y="115.125" width="20.25" height="20.25" rx="4.125" fill="white"/>
<g clip-path="url(#clip2_3472_38727)">
<path d="M54.5 121.25H55.5C55.7652 121.25 56.0196 121.355 56.2071 121.543C56.3946 121.73 56.5 121.985 56.5 122.25V129.25C56.5 129.515 56.3946 129.77 56.2071 129.957C56.0196 130.145 55.7652 130.25 55.5 130.25H49.5C49.2348 130.25 48.9804 130.145 48.7929 129.957C48.6054 129.77 48.5 129.515 48.5 129.25V122.25C48.5 121.985 48.6054 121.73 48.7929 121.543C48.9804 121.355 49.2348 121.25 49.5 121.25H50.5M51 120.25H54C54.2761 120.25 54.5 120.474 54.5 120.75V121.75C54.5 122.026 54.2761 122.25 54 122.25H51C50.7239 122.25 50.5 122.026 50.5 121.75V120.75C50.5 120.474 50.7239 120.25 51 120.25Z" stroke="#344054" stroke-width="0.94" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<rect x="42.375" y="115.125" width="20.25" height="20.25" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
<rect x="69.375" y="115.125" width="20.25" height="20.25" rx="4.125" fill="white"/>
<path d="M83 129.75L79.5 127.25L76 129.75V121.75C76 121.485 76.1054 121.23 76.2929 121.043C76.4804 120.855 76.7348 120.75 77 120.75H82C82.2652 120.75 82.5196 120.855 82.7071 121.043C82.8946 121.23 83 121.485 83 121.75V129.75Z" stroke="#344054" stroke-width="0.94" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="69.375" y="115.125" width="20.25" height="20.25" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
<rect x="96.375" y="114.875" width="89.5" height="20.75" rx="4.125" fill="white"/>
<g clip-path="url(#clip3_3472_38727)">
<path d="M106.969 129.625V127.438M106.969 123.062V120.875M105.875 121.969H108.062M105.875 128.531H108.062M110.688 121.312L109.929 123.285C109.805 123.606 109.744 123.766 109.648 123.901C109.563 124.021 109.458 124.125 109.339 124.21C109.204 124.306 109.043 124.368 108.723 124.491L106.75 125.25L108.723 126.009C109.043 126.132 109.204 126.194 109.339 126.29C109.458 126.375 109.563 126.479 109.648 126.599C109.744 126.734 109.805 126.894 109.929 127.215L110.688 129.188L111.446 127.215C111.57 126.894 111.631 126.734 111.727 126.599C111.812 126.479 111.917 126.375 112.036 126.29C112.171 126.194 112.332 126.132 112.652 126.009L114.625 125.25L112.652 124.491C112.332 124.368 112.171 124.306 112.036 124.21C111.917 124.125 111.812 124.021 111.727 123.901C111.631 123.766 111.57 123.606 111.446 123.285L110.688 121.312Z" stroke="#344054" stroke-width="0.9375" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<path d="M119.969 121.705H121.168L123.251 126.793H123.328L125.412 121.705H126.61V128.25H125.671V123.513H125.61L123.68 128.24H122.9L120.969 123.51H120.909V128.25H119.969V121.705ZM130.037 128.349C129.577 128.349 129.176 128.244 128.832 128.033C128.489 127.822 128.223 127.527 128.033 127.147C127.844 126.768 127.749 126.325 127.749 125.818C127.749 125.309 127.844 124.863 128.033 124.482C128.223 124.1 128.489 123.804 128.832 123.593C129.176 123.382 129.577 123.277 130.037 123.277C130.498 123.277 130.899 123.382 131.242 123.593C131.585 123.804 131.852 124.1 132.041 124.482C132.231 124.863 132.326 125.309 132.326 125.818C132.326 126.325 132.231 126.768 132.041 127.147C131.852 127.527 131.585 127.822 131.242 128.033C130.899 128.244 130.498 128.349 130.037 128.349ZM130.041 127.547C130.339 127.547 130.586 127.468 130.782 127.31C130.978 127.153 131.123 126.943 131.217 126.681C131.313 126.419 131.361 126.13 131.361 125.815C131.361 125.501 131.313 125.214 131.217 124.952C131.123 124.687 130.978 124.475 130.782 124.316C130.586 124.156 130.339 124.076 130.041 124.076C129.74 124.076 129.491 124.156 129.293 124.316C129.097 124.475 128.951 124.687 128.855 124.952C128.761 125.214 128.714 125.501 128.714 125.815C128.714 126.13 128.761 126.419 128.855 126.681C128.951 126.943 129.097 127.153 129.293 127.31C129.491 127.468 129.74 127.547 130.041 127.547ZM133.392 128.25V123.341H134.316V124.121H134.367C134.457 123.857 134.614 123.649 134.84 123.498C135.068 123.344 135.326 123.267 135.614 123.267C135.673 123.267 135.744 123.27 135.825 123.274C135.908 123.278 135.973 123.283 136.02 123.29V124.204C135.981 124.193 135.913 124.181 135.815 124.169C135.717 124.154 135.619 124.146 135.521 124.146C135.295 124.146 135.094 124.194 134.917 124.29C134.742 124.384 134.604 124.515 134.501 124.683C134.399 124.849 134.348 125.039 134.348 125.252V128.25H133.392ZM138.786 128.349C138.303 128.349 137.886 128.246 137.537 128.039C137.19 127.83 136.921 127.537 136.731 127.16C136.544 126.781 136.45 126.337 136.45 125.827C136.45 125.325 136.544 124.881 136.731 124.498C136.921 124.114 137.185 123.815 137.524 123.6C137.865 123.385 138.263 123.277 138.719 123.277C138.996 123.277 139.265 123.323 139.525 123.414C139.785 123.506 140.018 123.65 140.225 123.846C140.431 124.042 140.594 124.297 140.714 124.61C140.833 124.921 140.893 125.299 140.893 125.744V126.083H136.99V125.367H139.956C139.956 125.116 139.905 124.893 139.803 124.699C139.701 124.503 139.557 124.349 139.371 124.236C139.188 124.123 138.973 124.066 138.726 124.066C138.457 124.066 138.223 124.132 138.023 124.265C137.824 124.395 137.671 124.565 137.562 124.776C137.456 124.985 137.403 125.212 137.403 125.457V126.016C137.403 126.344 137.46 126.623 137.575 126.853C137.692 127.083 137.855 127.259 138.064 127.381C138.273 127.5 138.517 127.56 138.796 127.56C138.977 127.56 139.142 127.534 139.291 127.483C139.441 127.43 139.57 127.351 139.678 127.246C139.787 127.142 139.87 127.013 139.927 126.86L140.832 127.023C140.759 127.289 140.63 127.522 140.442 127.723C140.257 127.921 140.023 128.075 139.742 128.186C139.463 128.295 139.144 128.349 138.786 128.349ZM145.29 121.705V128.25H144.335V121.705H145.29ZM146.576 128.25V123.341H147.532V128.25H146.576ZM147.059 122.583C146.892 122.583 146.75 122.528 146.63 122.417C146.513 122.304 146.455 122.17 146.455 122.015C146.455 121.857 146.513 121.723 146.63 121.612C146.75 121.499 146.892 121.442 147.059 121.442C147.225 121.442 147.366 121.499 147.484 121.612C147.603 121.723 147.663 121.857 147.663 122.015C147.663 122.17 147.603 122.304 147.484 122.417C147.366 122.528 147.225 122.583 147.059 122.583ZM149.696 126.585L149.69 125.418H149.856L151.812 123.341H152.956L150.725 125.706H150.575L149.696 126.585ZM148.817 128.25V121.705H149.773V128.25H148.817ZM151.917 128.25L150.16 125.917L150.818 125.249L153.09 128.25H151.917ZM155.741 128.349C155.257 128.349 154.84 128.246 154.491 128.039C154.144 127.83 153.875 127.537 153.686 127.16C153.498 126.781 153.404 126.337 153.404 125.827C153.404 125.325 153.498 124.881 153.686 124.498C153.875 124.114 154.139 123.815 154.478 123.6C154.819 123.385 155.218 123.277 155.673 123.277C155.95 123.277 156.219 123.323 156.479 123.414C156.739 123.506 156.972 123.65 157.179 123.846C157.385 124.042 157.548 124.297 157.668 124.61C157.787 124.921 157.847 125.299 157.847 125.744V126.083H153.944V125.367H156.91C156.91 125.116 156.859 124.893 156.757 124.699C156.655 124.503 156.511 124.349 156.325 124.236C156.142 124.123 155.927 124.066 155.68 124.066C155.411 124.066 155.177 124.132 154.977 124.265C154.779 124.395 154.625 124.565 154.517 124.776C154.41 124.985 154.357 125.212 154.357 125.457V126.016C154.357 126.344 154.414 126.623 154.529 126.853C154.646 127.083 154.809 127.259 155.018 127.381C155.227 127.5 155.471 127.56 155.75 127.56C155.931 127.56 156.096 127.534 156.246 127.483C156.395 127.43 156.524 127.351 156.632 127.246C156.741 127.142 156.824 127.013 156.882 126.86L157.786 127.023C157.714 127.289 157.584 127.522 157.396 127.723C157.211 127.921 156.977 128.075 156.696 128.186C156.417 128.295 156.099 128.349 155.741 128.349ZM163.58 123.341V124.108H160.899V123.341H163.58ZM161.618 122.165H162.574V126.809C162.574 126.994 162.601 127.134 162.657 127.227C162.712 127.319 162.784 127.382 162.871 127.416C162.96 127.448 163.057 127.464 163.162 127.464C163.238 127.464 163.306 127.458 163.363 127.448C163.421 127.437 163.465 127.429 163.497 127.422L163.67 128.212C163.615 128.233 163.536 128.254 163.433 128.276C163.331 128.299 163.203 128.312 163.05 128.314C162.798 128.318 162.564 128.273 162.347 128.18C162.129 128.086 161.954 127.941 161.819 127.745C161.685 127.549 161.618 127.303 161.618 127.007V122.165ZM165.699 125.335V128.25H164.743V121.705H165.686V124.14H165.747C165.862 123.876 166.037 123.666 166.274 123.51C166.51 123.355 166.819 123.277 167.201 123.277C167.537 123.277 167.831 123.346 168.083 123.485C168.336 123.623 168.532 123.83 168.671 124.105C168.812 124.377 168.882 124.718 168.882 125.127V128.25H167.926V125.243C167.926 124.882 167.834 124.603 167.648 124.405C167.463 124.205 167.205 124.105 166.875 124.105C166.649 124.105 166.446 124.153 166.267 124.249C166.091 124.344 165.951 124.485 165.849 124.67C165.749 124.854 165.699 125.075 165.699 125.335ZM170.157 128.25V123.341H171.113V128.25H170.157ZM170.64 122.583C170.473 122.583 170.331 122.528 170.211 122.417C170.094 122.304 170.036 122.17 170.036 122.015C170.036 121.857 170.094 121.723 170.211 121.612C170.331 121.499 170.473 121.442 170.64 121.442C170.806 121.442 170.948 121.499 171.065 121.612C171.184 121.723 171.244 121.857 171.244 122.015C171.244 122.17 171.184 122.304 171.065 122.417C170.948 122.528 170.806 122.583 170.64 122.583ZM176.077 124.539L175.211 124.693C175.175 124.582 175.117 124.477 175.038 124.376C174.961 124.276 174.857 124.194 174.725 124.13C174.593 124.066 174.428 124.034 174.23 124.034C173.959 124.034 173.733 124.095 173.552 124.217C173.371 124.336 173.28 124.49 173.28 124.68C173.28 124.844 173.341 124.976 173.463 125.076C173.584 125.176 173.78 125.259 174.051 125.322L174.83 125.501C175.282 125.606 175.619 125.767 175.84 125.984C176.062 126.201 176.173 126.484 176.173 126.831C176.173 127.125 176.088 127.387 175.917 127.617C175.749 127.845 175.513 128.024 175.211 128.154C174.91 128.284 174.562 128.349 174.166 128.349C173.616 128.349 173.167 128.232 172.82 127.998C172.473 127.761 172.26 127.425 172.181 126.991L173.105 126.85C173.162 127.091 173.28 127.273 173.459 127.397C173.638 127.518 173.872 127.579 174.159 127.579C174.472 127.579 174.723 127.514 174.91 127.384C175.098 127.252 175.192 127.091 175.192 126.901C175.192 126.748 175.134 126.619 175.019 126.515C174.906 126.41 174.732 126.331 174.498 126.278L173.667 126.096C173.209 125.991 172.87 125.825 172.651 125.597C172.433 125.369 172.325 125.081 172.325 124.731C172.325 124.441 172.406 124.188 172.568 123.971C172.73 123.753 172.953 123.584 173.239 123.462C173.524 123.339 173.851 123.277 174.22 123.277C174.751 123.277 175.168 123.392 175.473 123.622C175.778 123.85 175.979 124.156 176.077 124.539Z" fill="#344054"/>
<rect x="96.375" y="114.875" width="89.5" height="20.75" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
</g>
<rect x="30.1875" y="48.1875" width="243.625" height="99.625" rx="8.8125" stroke="#EAECF0" stroke-width="0.375"/>
</g>
<g filter="url(#filter2_d_3472_38727)">
<g clip-path="url(#clip4_3472_38727)">
<rect x="30" y="154" width="244" height="90" rx="9" fill="white"/>
<path d="M50.0625 170.375L48.9375 177.125M53.0625 170.375L51.9375 177.125M54.1875 172.25H47.8125M53.8125 175.25H47.4375" stroke="#98A2B3" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M59.2764 176.85C58.8135 176.85 58.4385 176.729 58.1514 176.486C57.8643 176.242 57.6729 175.889 57.5771 175.426C57.4814 174.963 57.4863 174.405 57.5918 173.753C57.6992 173.103 57.8779 172.547 58.1279 172.086C58.3799 171.625 58.6895 171.273 59.0566 171.031C59.4258 170.789 59.8398 170.668 60.2988 170.668C60.7559 170.668 61.1289 170.79 61.418 171.034C61.707 171.276 61.9014 171.627 62.001 172.086C62.1025 172.545 62.0986 173.101 61.9893 173.753C61.8799 174.407 61.7002 174.965 61.4502 175.426C61.2002 175.887 60.8906 176.239 60.5215 176.483C60.1523 176.728 59.7373 176.85 59.2764 176.85ZM59.4053 176.067C59.8135 176.067 60.165 175.869 60.46 175.473C60.7549 175.076 60.9648 174.503 61.0898 173.753C61.1719 173.255 61.1885 172.834 61.1396 172.49C61.0928 172.145 60.9863 171.883 60.8203 171.705C60.6563 171.525 60.4395 171.436 60.1699 171.436C59.7676 171.436 59.418 171.636 59.1211 172.036C58.8262 172.435 58.6172 173.007 58.4941 173.753C58.4102 174.253 58.3916 174.676 58.4385 175.021C58.4854 175.365 58.5908 175.626 58.7549 175.804C58.9189 175.979 59.1357 176.067 59.4053 176.067ZM62.5276 176.75L62.6418 176.094L65.0149 173.99C65.2688 173.762 65.4807 173.562 65.6506 173.39C65.8225 173.216 65.9563 173.051 66.052 172.895C66.1497 172.738 66.2141 172.572 66.2454 172.396C66.2786 172.197 66.261 172.025 66.1926 171.881C66.1262 171.734 66.0198 171.622 65.8733 171.544C65.7268 171.464 65.551 171.424 65.3459 171.424C65.1311 171.424 64.9358 171.468 64.76 171.556C64.5842 171.644 64.4377 171.768 64.3206 171.928C64.2034 172.088 64.1262 172.275 64.0891 172.49H63.2278C63.2883 172.125 63.427 171.806 63.6438 171.532C63.8606 171.259 64.1292 171.047 64.4495 170.896C64.7698 170.744 65.1165 170.668 65.4895 170.668C65.8684 170.668 66.1877 170.743 66.4475 170.894C66.7092 171.042 66.8987 171.245 67.0159 171.503C67.1331 171.759 67.1643 172.048 67.1096 172.37C67.0706 172.595 66.9915 172.813 66.8723 173.026C66.7532 173.237 66.5696 173.474 66.3215 173.735C66.0735 173.995 65.7356 174.311 65.3079 174.682L63.9133 175.93L63.9045 175.974H66.6028L66.4768 176.75H62.5276Z" fill="#667085"/>
<rect x="42.375" y="166.375" width="29.75" height="14.75" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
<g opacity="0.1">
<rect x="42" y="187.5" width="220" height="4" rx="2" fill="#101828"/>
</g>
<g opacity="0.1">
<rect x="42" y="197.5" width="92" height="4" rx="2" fill="#101828"/>
</g>
<rect x="42.375" y="211.125" width="20.25" height="20.25" rx="4.125" fill="white"/>
<g clip-path="url(#clip5_3472_38727)">
<path d="M54.5 217.25H55.5C55.7652 217.25 56.0196 217.355 56.2071 217.543C56.3946 217.73 56.5 217.985 56.5 218.25V225.25C56.5 225.515 56.3946 225.77 56.2071 225.957C56.0196 226.145 55.7652 226.25 55.5 226.25H49.5C49.2348 226.25 48.9804 226.145 48.7929 225.957C48.6054 225.77 48.5 225.515 48.5 225.25V218.25C48.5 217.985 48.6054 217.73 48.7929 217.543C48.9804 217.355 49.2348 217.25 49.5 217.25H50.5M51 216.25H54C54.2761 216.25 54.5 216.474 54.5 216.75V217.75C54.5 218.026 54.2761 218.25 54 218.25H51C50.7239 218.25 50.5 218.026 50.5 217.75V216.75C50.5 216.474 50.7239 216.25 51 216.25Z" stroke="#344054" stroke-width="0.94" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<rect x="42.375" y="211.125" width="20.25" height="20.25" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
<rect x="69.375" y="211.125" width="20.25" height="20.25" rx="4.125" fill="white"/>
<path d="M83 225.75L79.5 223.25L76 225.75V217.75C76 217.485 76.1054 217.23 76.2929 217.043C76.4804 216.855 76.7348 216.75 77 216.75H82C82.2652 216.75 82.5196 216.855 82.7071 217.043C82.8946 217.23 83 217.485 83 217.75V225.75Z" stroke="#344054" stroke-width="0.94" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="69.375" y="211.125" width="20.25" height="20.25" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
<rect x="96.375" y="210.875" width="89.5" height="20.75" rx="4.125" fill="white"/>
<g clip-path="url(#clip6_3472_38727)">
<path d="M106.969 225.625V223.438M106.969 219.062V216.875M105.875 217.969H108.062M105.875 224.531H108.062M110.688 217.312L109.929 219.285C109.805 219.606 109.744 219.766 109.648 219.901C109.563 220.021 109.458 220.125 109.339 220.21C109.204 220.306 109.043 220.368 108.723 220.491L106.75 221.25L108.723 222.009C109.043 222.132 109.204 222.194 109.339 222.29C109.458 222.375 109.563 222.479 109.648 222.599C109.744 222.734 109.805 222.894 109.929 223.215L110.688 225.188L111.446 223.215C111.57 222.894 111.631 222.734 111.727 222.599C111.812 222.479 111.917 222.375 112.036 222.29C112.171 222.194 112.332 222.132 112.652 222.009L114.625 221.25L112.652 220.491C112.332 220.368 112.171 220.306 112.036 220.21C111.917 220.125 111.812 220.021 111.727 219.901C111.631 219.766 111.57 219.606 111.446 219.285L110.688 217.312Z" stroke="#344054" stroke-width="0.9375" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<path d="M119.969 217.705H121.168L123.251 222.793H123.328L125.412 217.705H126.61V224.25H125.671V219.513H125.61L123.68 224.24H122.9L120.969 219.51H120.909V224.25H119.969V217.705ZM130.037 224.349C129.577 224.349 129.176 224.244 128.832 224.033C128.489 223.822 128.223 223.527 128.033 223.147C127.844 222.768 127.749 222.325 127.749 221.818C127.749 221.309 127.844 220.863 128.033 220.482C128.223 220.1 128.489 219.804 128.832 219.593C129.176 219.382 129.577 219.277 130.037 219.277C130.498 219.277 130.899 219.382 131.242 219.593C131.585 219.804 131.852 220.1 132.041 220.482C132.231 220.863 132.326 221.309 132.326 221.818C132.326 222.325 132.231 222.768 132.041 223.147C131.852 223.527 131.585 223.822 131.242 224.033C130.899 224.244 130.498 224.349 130.037 224.349ZM130.041 223.547C130.339 223.547 130.586 223.468 130.782 223.31C130.978 223.153 131.123 222.943 131.217 222.681C131.313 222.419 131.361 222.13 131.361 221.815C131.361 221.501 131.313 221.214 131.217 220.952C131.123 220.687 130.978 220.475 130.782 220.316C130.586 220.156 130.339 220.076 130.041 220.076C129.74 220.076 129.491 220.156 129.293 220.316C129.097 220.475 128.951 220.687 128.855 220.952C128.761 221.214 128.714 221.501 128.714 221.815C128.714 222.13 128.761 222.419 128.855 222.681C128.951 222.943 129.097 223.153 129.293 223.31C129.491 223.468 129.74 223.547 130.041 223.547ZM133.392 224.25V219.341H134.316V220.121H134.367C134.457 219.857 134.614 219.649 134.84 219.498C135.068 219.344 135.326 219.267 135.614 219.267C135.673 219.267 135.744 219.27 135.825 219.274C135.908 219.278 135.973 219.283 136.02 219.29V220.204C135.981 220.193 135.913 220.181 135.815 220.169C135.717 220.154 135.619 220.146 135.521 220.146C135.295 220.146 135.094 220.194 134.917 220.29C134.742 220.384 134.604 220.515 134.501 220.683C134.399 220.849 134.348 221.039 134.348 221.252V224.25H133.392ZM138.786 224.349C138.303 224.349 137.886 224.246 137.537 224.039C137.19 223.83 136.921 223.537 136.731 223.16C136.544 222.781 136.45 222.337 136.45 221.827C136.45 221.325 136.544 220.881 136.731 220.498C136.921 220.114 137.185 219.815 137.524 219.6C137.865 219.385 138.263 219.277 138.719 219.277C138.996 219.277 139.265 219.323 139.525 219.414C139.785 219.506 140.018 219.65 140.225 219.846C140.431 220.042 140.594 220.297 140.714 220.61C140.833 220.921 140.893 221.299 140.893 221.744V222.083H136.99V221.367H139.956C139.956 221.116 139.905 220.893 139.803 220.699C139.701 220.503 139.557 220.349 139.371 220.236C139.188 220.123 138.973 220.066 138.726 220.066C138.457 220.066 138.223 220.132 138.023 220.265C137.824 220.395 137.671 220.565 137.562 220.776C137.456 220.985 137.403 221.212 137.403 221.457V222.016C137.403 222.344 137.46 222.623 137.575 222.853C137.692 223.083 137.855 223.259 138.064 223.381C138.273 223.5 138.517 223.56 138.796 223.56C138.977 223.56 139.142 223.534 139.291 223.483C139.441 223.43 139.57 223.351 139.678 223.246C139.787 223.142 139.87 223.013 139.927 222.86L140.832 223.023C140.759 223.289 140.63 223.522 140.442 223.723C140.257 223.921 140.023 224.075 139.742 224.186C139.463 224.295 139.144 224.349 138.786 224.349ZM145.29 217.705V224.25H144.335V217.705H145.29ZM146.576 224.25V219.341H147.532V224.25H146.576ZM147.059 218.583C146.892 218.583 146.75 218.528 146.63 218.417C146.513 218.304 146.455 218.17 146.455 218.015C146.455 217.857 146.513 217.723 146.63 217.612C146.75 217.499 146.892 217.442 147.059 217.442C147.225 217.442 147.366 217.499 147.484 217.612C147.603 217.723 147.663 217.857 147.663 218.015C147.663 218.17 147.603 218.304 147.484 218.417C147.366 218.528 147.225 218.583 147.059 218.583ZM149.696 222.585L149.69 221.418H149.856L151.812 219.341H152.956L150.725 221.706H150.575L149.696 222.585ZM148.817 224.25V217.705H149.773V224.25H148.817ZM151.917 224.25L150.16 221.917L150.818 221.249L153.09 224.25H151.917ZM155.741 224.349C155.257 224.349 154.84 224.246 154.491 224.039C154.144 223.83 153.875 223.537 153.686 223.16C153.498 222.781 153.404 222.337 153.404 221.827C153.404 221.325 153.498 220.881 153.686 220.498C153.875 220.114 154.139 219.815 154.478 219.6C154.819 219.385 155.218 219.277 155.673 219.277C155.95 219.277 156.219 219.323 156.479 219.414C156.739 219.506 156.972 219.65 157.179 219.846C157.385 220.042 157.548 220.297 157.668 220.61C157.787 220.921 157.847 221.299 157.847 221.744V222.083H153.944V221.367H156.91C156.91 221.116 156.859 220.893 156.757 220.699C156.655 220.503 156.511 220.349 156.325 220.236C156.142 220.123 155.927 220.066 155.68 220.066C155.411 220.066 155.177 220.132 154.977 220.265C154.779 220.395 154.625 220.565 154.517 220.776C154.41 220.985 154.357 221.212 154.357 221.457V222.016C154.357 222.344 154.414 222.623 154.529 222.853C154.646 223.083 154.809 223.259 155.018 223.381C155.227 223.5 155.471 223.56 155.75 223.56C155.931 223.56 156.096 223.534 156.246 223.483C156.395 223.43 156.524 223.351 156.632 223.246C156.741 223.142 156.824 223.013 156.882 222.86L157.786 223.023C157.714 223.289 157.584 223.522 157.396 223.723C157.211 223.921 156.977 224.075 156.696 224.186C156.417 224.295 156.099 224.349 155.741 224.349ZM163.58 219.341V220.108H160.899V219.341H163.58ZM161.618 218.165H162.574V222.809C162.574 222.994 162.601 223.134 162.657 223.227C162.712 223.319 162.784 223.382 162.871 223.416C162.96 223.448 163.057 223.464 163.162 223.464C163.238 223.464 163.306 223.458 163.363 223.448C163.421 223.437 163.465 223.429 163.497 223.422L163.67 224.212C163.615 224.233 163.536 224.254 163.433 224.276C163.331 224.299 163.203 224.312 163.05 224.314C162.798 224.318 162.564 224.273 162.347 224.18C162.129 224.086 161.954 223.941 161.819 223.745C161.685 223.549 161.618 223.303 161.618 223.007V218.165ZM165.699 221.335V224.25H164.743V217.705H165.686V220.14H165.747C165.862 219.876 166.037 219.666 166.274 219.51C166.51 219.355 166.819 219.277 167.201 219.277C167.537 219.277 167.831 219.346 168.083 219.485C168.336 219.623 168.532 219.83 168.671 220.105C168.812 220.377 168.882 220.718 168.882 221.127V224.25H167.926V221.243C167.926 220.882 167.834 220.603 167.648 220.405C167.463 220.205 167.205 220.105 166.875 220.105C166.649 220.105 166.446 220.153 166.267 220.249C166.091 220.344 165.951 220.485 165.849 220.67C165.749 220.854 165.699 221.075 165.699 221.335ZM170.157 224.25V219.341H171.113V224.25H170.157ZM170.64 218.583C170.473 218.583 170.331 218.528 170.211 218.417C170.094 218.304 170.036 218.17 170.036 218.015C170.036 217.857 170.094 217.723 170.211 217.612C170.331 217.499 170.473 217.442 170.64 217.442C170.806 217.442 170.948 217.499 171.065 217.612C171.184 217.723 171.244 217.857 171.244 218.015C171.244 218.17 171.184 218.304 171.065 218.417C170.948 218.528 170.806 218.583 170.64 218.583ZM176.077 220.539L175.211 220.693C175.175 220.582 175.117 220.477 175.038 220.376C174.961 220.276 174.857 220.194 174.725 220.13C174.593 220.066 174.428 220.034 174.23 220.034C173.959 220.034 173.733 220.095 173.552 220.217C173.371 220.336 173.28 220.49 173.28 220.68C173.28 220.844 173.341 220.976 173.463 221.076C173.584 221.176 173.78 221.259 174.051 221.322L174.83 221.501C175.282 221.606 175.619 221.767 175.84 221.984C176.062 222.201 176.173 222.484 176.173 222.831C176.173 223.125 176.088 223.387 175.917 223.617C175.749 223.845 175.513 224.024 175.211 224.154C174.91 224.284 174.562 224.349 174.166 224.349C173.616 224.349 173.167 224.232 172.82 223.998C172.473 223.761 172.26 223.425 172.181 222.991L173.105 222.85C173.162 223.091 173.28 223.273 173.459 223.397C173.638 223.518 173.872 223.579 174.159 223.579C174.472 223.579 174.723 223.514 174.91 223.384C175.098 223.252 175.192 223.091 175.192 222.901C175.192 222.748 175.134 222.619 175.019 222.515C174.906 222.41 174.732 222.331 174.498 222.278L173.667 222.096C173.209 221.991 172.87 221.825 172.651 221.597C172.433 221.369 172.325 221.081 172.325 220.731C172.325 220.441 172.406 220.188 172.568 219.971C172.73 219.753 172.953 219.584 173.239 219.462C173.524 219.339 173.851 219.277 174.22 219.277C174.751 219.277 175.168 219.392 175.473 219.622C175.778 219.85 175.979 220.156 176.077 220.539Z" fill="#344054"/>
<rect x="96.375" y="210.875" width="89.5" height="20.75" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
</g>
<rect x="30.1875" y="154.188" width="243.625" height="89.625" rx="8.8125" stroke="#EAECF0" stroke-width="0.375"/>
</g>
<g filter="url(#filter3_d_3472_38727)">
<g clip-path="url(#clip7_3472_38727)">
<rect x="30" y="250" width="244" height="90" rx="9" fill="white"/>
<path d="M50.0625 266.375L48.9375 273.125M53.0625 266.375L51.9375 273.125M54.1875 268.25H47.8125M53.8125 271.25H47.4375" stroke="#98A2B3" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M59.2764 272.85C58.8135 272.85 58.4385 272.729 58.1514 272.486C57.8643 272.242 57.6729 271.889 57.5771 271.426C57.4814 270.963 57.4863 270.405 57.5918 269.753C57.6992 269.103 57.8779 268.547 58.1279 268.086C58.3799 267.625 58.6895 267.273 59.0566 267.031C59.4258 266.789 59.8398 266.668 60.2988 266.668C60.7559 266.668 61.1289 266.79 61.418 267.034C61.707 267.276 61.9014 267.627 62.001 268.086C62.1025 268.545 62.0986 269.101 61.9893 269.753C61.8799 270.407 61.7002 270.965 61.4502 271.426C61.2002 271.887 60.8906 272.239 60.5215 272.483C60.1523 272.728 59.7373 272.85 59.2764 272.85ZM59.4053 272.067C59.8135 272.067 60.165 271.869 60.46 271.473C60.7549 271.076 60.9648 270.503 61.0898 269.753C61.1719 269.255 61.1885 268.834 61.1396 268.49C61.0928 268.145 60.9863 267.883 60.8203 267.705C60.6563 267.525 60.4395 267.436 60.1699 267.436C59.7676 267.436 59.418 267.636 59.1211 268.036C58.8262 268.435 58.6172 269.007 58.4941 269.753C58.4102 270.253 58.3916 270.676 58.4385 271.021C58.4854 271.365 58.5908 271.626 58.7549 271.804C58.9189 271.979 59.1357 272.067 59.4053 272.067ZM64.6516 272.832C64.2415 272.832 63.887 272.764 63.5881 272.627C63.2893 272.488 63.0667 272.295 62.9202 272.047C62.7756 271.799 62.7258 271.512 62.7708 271.186H63.676C63.6565 271.361 63.6858 271.515 63.7639 271.646C63.844 271.776 63.9622 271.877 64.1184 271.947C64.2747 272.018 64.4612 272.053 64.678 272.053C64.9143 272.053 65.134 272.011 65.3372 271.927C65.5422 271.843 65.7131 271.724 65.8499 271.569C65.9885 271.413 66.0735 271.23 66.1047 271.021C66.134 270.828 66.1116 270.659 66.0374 270.515C65.9631 270.368 65.8372 270.254 65.6594 270.172C65.4817 270.09 65.2542 270.049 64.9768 270.049H64.4524L64.5754 269.311H65.0559C65.2805 269.311 65.4856 269.269 65.6711 269.185C65.8586 269.101 66.0149 268.982 66.1399 268.83C66.2649 268.678 66.343 268.5 66.3743 268.297C66.4055 268.121 66.3938 267.969 66.3391 267.84C66.2844 267.709 66.1907 267.607 66.0579 267.535C65.927 267.461 65.759 267.424 65.554 267.424C65.3567 267.424 65.1633 267.46 64.9739 267.532C64.7844 267.603 64.6233 267.704 64.4905 267.837C64.3577 267.968 64.2747 268.125 64.2415 268.309H63.3684C63.429 267.984 63.5676 267.699 63.7844 267.453C64.0012 267.207 64.2678 267.015 64.5842 266.876C64.9026 266.737 65.2415 266.668 65.6008 266.668C65.9934 266.668 66.3196 266.747 66.5793 266.905C66.8391 267.063 67.0256 267.271 67.1389 267.526C67.2542 267.782 67.2883 268.059 67.2415 268.355C67.1868 268.689 67.0461 268.967 66.8196 269.187C66.593 269.406 66.3147 269.557 65.9846 269.639V269.677C66.3655 269.739 66.6458 269.906 66.8254 270.178C67.0051 270.449 67.0637 270.775 67.0012 271.156C66.9465 271.494 66.8108 271.789 66.594 272.041C66.3772 272.291 66.1018 272.485 65.7678 272.624C65.4338 272.763 65.0618 272.832 64.6516 272.832Z" fill="#667085"/>
<rect x="42.375" y="262.375" width="29.75" height="14.75" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
<g opacity="0.1">
<rect x="42" y="283.5" width="220" height="4" rx="2" fill="#101828"/>
</g>
<g opacity="0.1">
<rect x="42" y="293.5" width="180" height="4" rx="2" fill="#101828"/>
</g>
<rect x="42.375" y="307.125" width="20.25" height="20.25" rx="4.125" fill="white"/>
<g clip-path="url(#clip8_3472_38727)">
<path d="M54.5 313.25H55.5C55.7652 313.25 56.0196 313.355 56.2071 313.543C56.3946 313.73 56.5 313.985 56.5 314.25V321.25C56.5 321.515 56.3946 321.77 56.2071 321.957C56.0196 322.145 55.7652 322.25 55.5 322.25H49.5C49.2348 322.25 48.9804 322.145 48.7929 321.957C48.6054 321.77 48.5 321.515 48.5 321.25V314.25C48.5 313.985 48.6054 313.73 48.7929 313.543C48.9804 313.355 49.2348 313.25 49.5 313.25H50.5M51 312.25H54C54.2761 312.25 54.5 312.474 54.5 312.75V313.75C54.5 314.026 54.2761 314.25 54 314.25H51C50.7239 314.25 50.5 314.026 50.5 313.75V312.75C50.5 312.474 50.7239 312.25 51 312.25Z" stroke="#344054" stroke-width="0.94" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<rect x="42.375" y="307.125" width="20.25" height="20.25" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
<rect x="69.375" y="307.125" width="20.25" height="20.25" rx="4.125" fill="white"/>
<path d="M83 321.75L79.5 319.25L76 321.75V313.75C76 313.485 76.1054 313.23 76.2929 313.043C76.4804 312.855 76.7348 312.75 77 312.75H82C82.2652 312.75 82.5196 312.855 82.7071 313.043C82.8946 313.23 83 313.485 83 313.75V321.75Z" stroke="#344054" stroke-width="0.94" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="69.375" y="307.125" width="20.25" height="20.25" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
<rect x="96.375" y="306.875" width="89.5" height="20.75" rx="4.125" fill="white"/>
<g clip-path="url(#clip9_3472_38727)">
<path d="M106.969 321.625V319.438M106.969 315.062V312.875M105.875 313.969H108.062M105.875 320.531H108.062M110.688 313.312L109.929 315.285C109.805 315.606 109.744 315.766 109.648 315.901C109.563 316.021 109.458 316.125 109.339 316.21C109.204 316.306 109.043 316.368 108.723 316.491L106.75 317.25L108.723 318.009C109.043 318.132 109.204 318.194 109.339 318.29C109.458 318.375 109.563 318.479 109.648 318.599C109.744 318.734 109.805 318.894 109.929 319.215L110.688 321.188L111.446 319.215C111.57 318.894 111.631 318.734 111.727 318.599C111.812 318.479 111.917 318.375 112.036 318.29C112.171 318.194 112.332 318.132 112.652 318.009L114.625 317.25L112.652 316.491C112.332 316.368 112.171 316.306 112.036 316.21C111.917 316.125 111.812 316.021 111.727 315.901C111.631 315.766 111.57 315.606 111.446 315.285L110.688 313.312Z" stroke="#344054" stroke-width="0.9375" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<path d="M119.969 313.705H121.168L123.251 318.793H123.328L125.412 313.705H126.61V320.25H125.671V315.513H125.61L123.68 320.24H122.9L120.969 315.51H120.909V320.25H119.969V313.705ZM130.037 320.349C129.577 320.349 129.176 320.244 128.832 320.033C128.489 319.822 128.223 319.527 128.033 319.147C127.844 318.768 127.749 318.325 127.749 317.818C127.749 317.309 127.844 316.863 128.033 316.482C128.223 316.1 128.489 315.804 128.832 315.593C129.176 315.382 129.577 315.277 130.037 315.277C130.498 315.277 130.899 315.382 131.242 315.593C131.585 315.804 131.852 316.1 132.041 316.482C132.231 316.863 132.326 317.309 132.326 317.818C132.326 318.325 132.231 318.768 132.041 319.147C131.852 319.527 131.585 319.822 131.242 320.033C130.899 320.244 130.498 320.349 130.037 320.349ZM130.041 319.547C130.339 319.547 130.586 319.468 130.782 319.31C130.978 319.153 131.123 318.943 131.217 318.681C131.313 318.419 131.361 318.13 131.361 317.815C131.361 317.501 131.313 317.214 131.217 316.952C131.123 316.687 130.978 316.475 130.782 316.316C130.586 316.156 130.339 316.076 130.041 316.076C129.74 316.076 129.491 316.156 129.293 316.316C129.097 316.475 128.951 316.687 128.855 316.952C128.761 317.214 128.714 317.501 128.714 317.815C128.714 318.13 128.761 318.419 128.855 318.681C128.951 318.943 129.097 319.153 129.293 319.31C129.491 319.468 129.74 319.547 130.041 319.547ZM133.392 320.25V315.341H134.316V316.121H134.367C134.457 315.857 134.614 315.649 134.84 315.498C135.068 315.344 135.326 315.267 135.614 315.267C135.673 315.267 135.744 315.27 135.825 315.274C135.908 315.278 135.973 315.283 136.02 315.29V316.204C135.981 316.193 135.913 316.181 135.815 316.169C135.717 316.154 135.619 316.146 135.521 316.146C135.295 316.146 135.094 316.194 134.917 316.29C134.742 316.384 134.604 316.515 134.501 316.683C134.399 316.849 134.348 317.039 134.348 317.252V320.25H133.392ZM138.786 320.349C138.303 320.349 137.886 320.246 137.537 320.039C137.19 319.83 136.921 319.537 136.731 319.16C136.544 318.781 136.45 318.337 136.45 317.827C136.45 317.325 136.544 316.881 136.731 316.498C136.921 316.114 137.185 315.815 137.524 315.6C137.865 315.385 138.263 315.277 138.719 315.277C138.996 315.277 139.265 315.323 139.525 315.414C139.785 315.506 140.018 315.65 140.225 315.846C140.431 316.042 140.594 316.297 140.714 316.61C140.833 316.921 140.893 317.299 140.893 317.744V318.083H136.99V317.367H139.956C139.956 317.116 139.905 316.893 139.803 316.699C139.701 316.503 139.557 316.349 139.371 316.236C139.188 316.123 138.973 316.066 138.726 316.066C138.457 316.066 138.223 316.132 138.023 316.265C137.824 316.395 137.671 316.565 137.562 316.776C137.456 316.985 137.403 317.212 137.403 317.457V318.016C137.403 318.344 137.46 318.623 137.575 318.853C137.692 319.083 137.855 319.259 138.064 319.381C138.273 319.5 138.517 319.56 138.796 319.56C138.977 319.56 139.142 319.534 139.291 319.483C139.441 319.43 139.57 319.351 139.678 319.246C139.787 319.142 139.87 319.013 139.927 318.86L140.832 319.023C140.759 319.289 140.63 319.522 140.442 319.723C140.257 319.921 140.023 320.075 139.742 320.186C139.463 320.295 139.144 320.349 138.786 320.349ZM145.29 313.705V320.25H144.335V313.705H145.29ZM146.576 320.25V315.341H147.532V320.25H146.576ZM147.059 314.583C146.892 314.583 146.75 314.528 146.63 314.417C146.513 314.304 146.455 314.17 146.455 314.015C146.455 313.857 146.513 313.723 146.63 313.612C146.75 313.499 146.892 313.442 147.059 313.442C147.225 313.442 147.366 313.499 147.484 313.612C147.603 313.723 147.663 313.857 147.663 314.015C147.663 314.17 147.603 314.304 147.484 314.417C147.366 314.528 147.225 314.583 147.059 314.583ZM149.696 318.585L149.69 317.418H149.856L151.812 315.341H152.956L150.725 317.706H150.575L149.696 318.585ZM148.817 320.25V313.705H149.773V320.25H148.817ZM151.917 320.25L150.16 317.917L150.818 317.249L153.09 320.25H151.917ZM155.741 320.349C155.257 320.349 154.84 320.246 154.491 320.039C154.144 319.83 153.875 319.537 153.686 319.16C153.498 318.781 153.404 318.337 153.404 317.827C153.404 317.325 153.498 316.881 153.686 316.498C153.875 316.114 154.139 315.815 154.478 315.6C154.819 315.385 155.218 315.277 155.673 315.277C155.95 315.277 156.219 315.323 156.479 315.414C156.739 315.506 156.972 315.65 157.179 315.846C157.385 316.042 157.548 316.297 157.668 316.61C157.787 316.921 157.847 317.299 157.847 317.744V318.083H153.944V317.367H156.91C156.91 317.116 156.859 316.893 156.757 316.699C156.655 316.503 156.511 316.349 156.325 316.236C156.142 316.123 155.927 316.066 155.68 316.066C155.411 316.066 155.177 316.132 154.977 316.265C154.779 316.395 154.625 316.565 154.517 316.776C154.41 316.985 154.357 317.212 154.357 317.457V318.016C154.357 318.344 154.414 318.623 154.529 318.853C154.646 319.083 154.809 319.259 155.018 319.381C155.227 319.5 155.471 319.56 155.75 319.56C155.931 319.56 156.096 319.534 156.246 319.483C156.395 319.43 156.524 319.351 156.632 319.246C156.741 319.142 156.824 319.013 156.882 318.86L157.786 319.023C157.714 319.289 157.584 319.522 157.396 319.723C157.211 319.921 156.977 320.075 156.696 320.186C156.417 320.295 156.099 320.349 155.741 320.349ZM163.58 315.341V316.108H160.899V315.341H163.58ZM161.618 314.165H162.574V318.809C162.574 318.994 162.601 319.134 162.657 319.227C162.712 319.319 162.784 319.382 162.871 319.416C162.96 319.448 163.057 319.464 163.162 319.464C163.238 319.464 163.306 319.458 163.363 319.448C163.421 319.437 163.465 319.429 163.497 319.422L163.67 320.212C163.615 320.233 163.536 320.254 163.433 320.276C163.331 320.299 163.203 320.312 163.05 320.314C162.798 320.318 162.564 320.273 162.347 320.18C162.129 320.086 161.954 319.941 161.819 319.745C161.685 319.549 161.618 319.303 161.618 319.007V314.165ZM165.699 317.335V320.25H164.743V313.705H165.686V316.14H165.747C165.862 315.876 166.037 315.666 166.274 315.51C166.51 315.355 166.819 315.277 167.201 315.277C167.537 315.277 167.831 315.346 168.083 315.485C168.336 315.623 168.532 315.83 168.671 316.105C168.812 316.377 168.882 316.718 168.882 317.127V320.25H167.926V317.243C167.926 316.882 167.834 316.603 167.648 316.405C167.463 316.205 167.205 316.105 166.875 316.105C166.649 316.105 166.446 316.153 166.267 316.249C166.091 316.344 165.951 316.485 165.849 316.67C165.749 316.854 165.699 317.075 165.699 317.335ZM170.157 320.25V315.341H171.113V320.25H170.157ZM170.64 314.583C170.473 314.583 170.331 314.528 170.211 314.417C170.094 314.304 170.036 314.17 170.036 314.015C170.036 313.857 170.094 313.723 170.211 313.612C170.331 313.499 170.473 313.442 170.64 313.442C170.806 313.442 170.948 313.499 171.065 313.612C171.184 313.723 171.244 313.857 171.244 314.015C171.244 314.17 171.184 314.304 171.065 314.417C170.948 314.528 170.806 314.583 170.64 314.583ZM176.077 316.539L175.211 316.693C175.175 316.582 175.117 316.477 175.038 316.376C174.961 316.276 174.857 316.194 174.725 316.13C174.593 316.066 174.428 316.034 174.23 316.034C173.959 316.034 173.733 316.095 173.552 316.217C173.371 316.336 173.28 316.49 173.28 316.68C173.28 316.844 173.341 316.976 173.463 317.076C173.584 317.176 173.78 317.259 174.051 317.322L174.83 317.501C175.282 317.606 175.619 317.767 175.84 317.984C176.062 318.201 176.173 318.484 176.173 318.831C176.173 319.125 176.088 319.387 175.917 319.617C175.749 319.845 175.513 320.024 175.211 320.154C174.91 320.284 174.562 320.349 174.166 320.349C173.616 320.349 173.167 320.232 172.82 319.998C172.473 319.761 172.26 319.425 172.181 318.991L173.105 318.85C173.162 319.091 173.28 319.273 173.459 319.397C173.638 319.518 173.872 319.579 174.159 319.579C174.472 319.579 174.723 319.514 174.91 319.384C175.098 319.252 175.192 319.091 175.192 318.901C175.192 318.748 175.134 318.619 175.019 318.515C174.906 318.41 174.732 318.331 174.498 318.278L173.667 318.096C173.209 317.991 172.87 317.825 172.651 317.597C172.433 317.369 172.325 317.081 172.325 316.731C172.325 316.441 172.406 316.188 172.568 315.971C172.73 315.753 172.953 315.584 173.239 315.462C173.524 315.339 173.851 315.277 174.22 315.277C174.751 315.277 175.168 315.392 175.473 315.622C175.778 315.85 175.979 316.156 176.077 316.539Z" fill="#344054"/>
<rect x="96.375" y="306.875" width="89.5" height="20.75" rx="4.125" stroke="#EAECF0" stroke-width="0.75"/>
</g>
<rect x="30.1875" y="250.188" width="243.625" height="89.625" rx="8.8125" stroke="#EAECF0" stroke-width="0.375"/>
</g>
</g>
</g>
<defs>
<filter id="filter0_dd_3472_38727" x="0" y="0" width="304" height="384" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_3472_38727"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="3"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.03 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3472_38727"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect2_dropShadow_3472_38727"/>
<feOffset dy="12"/>
<feGaussianBlur stdDeviation="8"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.08 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_3472_38727" result="effect2_dropShadow_3472_38727"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_3472_38727" result="shape"/>
</filter>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_3472_38727" transform="scale(0.015625)"/>
</pattern>
<filter id="filter1_d_3472_38727" x="28.5" y="47.25" width="247" height="103" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.75"/>
<feGaussianBlur stdDeviation="0.75"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3472_38727"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3472_38727" result="shape"/>
</filter>
<filter id="filter2_d_3472_38727" x="28.5" y="153.25" width="247" height="93" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.75"/>
<feGaussianBlur stdDeviation="0.75"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3472_38727"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3472_38727" result="shape"/>
</filter>
<filter id="filter3_d_3472_38727" x="28.5" y="249.25" width="247" height="93" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.75"/>
<feGaussianBlur stdDeviation="0.75"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3472_38727"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3472_38727" result="shape"/>
</filter>
<clipPath id="clip0_3472_38727">
<rect x="12" width="280" height="360" rx="12" fill="white"/>
</clipPath>
<clipPath id="clip1_3472_38727">
<rect x="30" y="48" width="244" height="100" rx="9" fill="white"/>
</clipPath>
<clipPath id="clip2_3472_38727">
<rect width="12" height="12" fill="white" transform="translate(46.5 119.25)"/>
</clipPath>
<clipPath id="clip3_3472_38727">
<rect width="10.5" height="10.5" fill="white" transform="translate(105 120)"/>
</clipPath>
<clipPath id="clip4_3472_38727">
<rect x="30" y="154" width="244" height="90" rx="9" fill="white"/>
</clipPath>
<clipPath id="clip5_3472_38727">
<rect width="12" height="12" fill="white" transform="translate(46.5 215.25)"/>
</clipPath>
<clipPath id="clip6_3472_38727">
<rect width="10.5" height="10.5" fill="white" transform="translate(105 216)"/>
</clipPath>
<clipPath id="clip7_3472_38727">
<rect x="30" y="250" width="244" height="90" rx="9" fill="white"/>
</clipPath>
<clipPath id="clip8_3472_38727">
<rect width="12" height="12" fill="white" transform="translate(46.5 311.25)"/>
</clipPath>
<clipPath id="clip9_3472_38727">
<rect width="10.5" height="10.5" fill="white" transform="translate(105 312)"/>
</clipPath>
<image id="image0_3472_38727" width="64" height="64" xlink:href="data:image/png;base64,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"/>
</defs>
</svg>
.preview {
display: none;
position: fixed;
transform: translate(410px, -54px);
width: 280px;
height: 360px;
background: center center no-repeat;
background-size: contain;
}
.wrap:hover .preview {
display: block;
}
.openingStatementPreview {
background-image: url(./preview-imgs/opening-statement.svg);
}
.suggestedQuestionsAfterAnswerPreview {
background-image: url(./preview-imgs/suggested-questions-after-answer.svg);
}
.moreLikeThisPreview {
background-image: url(./preview-imgs/more-like-this.svg);
}
\ No newline at end of file
'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import FeatureItem from './feature-item'
import FeatureGroup from '../feature-group'
import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
import FeatureItem from './feature-item'
import Modal from '@/app/components/base/modal'
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
interface IConfig {
type IConfig = {
openingStatement: boolean
moreLikeThis: boolean
suggestedQuestionsAfterAnswer: boolean
}
export interface IChooseFeatureProps {
export type IChooseFeatureProps = {
isShow: boolean
onClose: () => void
config: IConfig
......@@ -32,7 +32,7 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
onClose,
isChatApp,
config,
onChange
onChange,
}) => {
const { t } = useTranslation()
......@@ -43,6 +43,7 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
className='w-[400px]'
title={t('appDebug.operation.addFeature')}
closable
overflowVisible
>
<div className='pt-5 pb-10'>
{/* Chat Feature */}
......@@ -54,17 +55,19 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
<>
<FeatureItem
icon={OpeningStatementIcon}
previewImgClassName='openingStatementPreview'
title={t('appDebug.feature.conversationOpener.title')}
description={t('appDebug.feature.conversationOpener.description')}
value={config.openingStatement}
onChange={(value) => onChange('openingStatement', value)}
onChange={value => onChange('openingStatement', value)}
/>
<FeatureItem
icon={<SuggestedQuestionsAfterAnswerIcon />}
previewImgClassName='suggestedQuestionsAfterAnswerPreview'
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
value={config.suggestedQuestionsAfterAnswer}
onChange={(value) => onChange('suggestedQuestionsAfterAnswer', value)}
onChange={value => onChange('suggestedQuestionsAfterAnswer', value)}
/>
</>
</FeatureGroup>
......@@ -76,10 +79,11 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
<>
<FeatureItem
icon={<MoreLikeThisIcon />}
previewImgClassName='moreLikeThisPreview'
title={t('appDebug.feature.moreLikeThis.title')}
description={t('appDebug.feature.moreLikeThis.description')}
value={config.moreLikeThis}
onChange={(value) => onChange('moreLikeThis', value)}
onChange={value => onChange('moreLikeThis', value)}
/>
</>
</FeatureGroup>
......
......@@ -12,7 +12,6 @@ import { formatNumber } from '@/utils/format'
import Link from 'next/link'
import s from './style.module.css'
import Toast from '@/app/components/base/toast'
export interface ISelectDataSetProps {
isShow: boolean
......@@ -32,8 +31,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
const [loaded, setLoaded] = React.useState(false)
const [datasets, setDataSets] = React.useState<DataSet[] | null>(null)
const hasNoData = !datasets || datasets?.length === 0
// Only one dataset can be selected. Historical data retains data and supports multiple selections, but when saving, only one can be selected. This is based on considerations of performance and accuracy.
const canSelectMulti = selectedIds.length > 1
const canSelectMulti = true
useEffect(() => {
(async () => {
const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } })
......@@ -57,13 +55,6 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
}
const handleSelect = () => {
if (selected.length > 1) {
Toast.notify({
type: 'error',
message: t('appDebug.feature.dataSet.notSupportSelectMulti')
})
return
}
onSelect(selected)
}
return (
......
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import React, { useEffect, useState, useRef } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import cn from 'classnames'
import produce from 'immer'
import { useBoolean, useGetState } from 'ahooks'
import { useContext } from 'use-context-selector'
import dayjs from 'dayjs'
import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
import FormattingChanged from '../base/warning-mask/formatting-changed'
import GroupName from '../base/group-name'
import { AppType } from '@/types/app'
import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
import type { IChatItem } from '@/app/components/app/chat'
import Chat from '@/app/components/app/chat'
import ConfigContext from '@/context/debug-configuration'
import { ToastContext } from '@/app/components/base/toast'
import { sendChatMessage, sendCompletionMessage, fetchSuggestedQuestions, fetchConvesationMessages } from '@/service/debug'
import { fetchConvesationMessages, fetchSuggestedQuestions, sendChatMessage, sendCompletionMessage } from '@/service/debug'
import Button from '@/app/components/base/button'
import type { ModelConfig as BackendModelConfig } from '@/types/app'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
import FormattingChanged from '../base/warning-mask/formatting-changed'
import TextGeneration from '@/app/components/app/text-generate/item'
import GroupName from '../base/group-name'
import dayjs from 'dayjs'
import { IS_CE_EDITION } from '@/config'
interface IDebug {
type IDebug = {
hasSetAPIKEY: boolean
onSetting: () => void
}
const Debug: FC<IDebug> = ({
hasSetAPIKEY = true,
onSetting
onSetting,
}) => {
const { t } = useTranslation()
const {
......@@ -51,14 +51,12 @@ const Debug: FC<IDebug> = ({
completionParams,
} = useContext(ConfigContext)
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
const chatListDomRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// scroll to bottom
if (chatListDomRef.current) {
if (chatListDomRef.current)
chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
}
}, [chatList])
const getIntroduction = () => replaceStringWithValues(introduction, modelConfig.configs.prompt_variables, inputs)
......@@ -68,7 +66,7 @@ const Debug: FC<IDebug> = ({
id: `${Date.now()}`,
content: getIntroduction(),
isAnswer: true,
isOpeningStatement: true
isOpeningStatement: true,
}])
}
}, [introduction, modelConfig.configs.prompt_variables, inputs])
......@@ -76,11 +74,12 @@ const Debug: FC<IDebug> = ({
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false)
const [isShowSuggestion, setIsShowSuggestion] = useState(false)
useEffect(() => {
if (formattingChanged && chatList.some(item => !item.isAnswer)) {
if (formattingChanged && chatList.some(item => !item.isAnswer))
setIsShowFormattingChangeConfirm(true)
}
setFormattingChanged(false)
}, [formattingChanged])
......@@ -88,12 +87,14 @@ const Debug: FC<IDebug> = ({
setConversationId(null)
abortController?.abort()
setResponsingFalse()
setChatList(introduction ? [{
setChatList(introduction
? [{
id: `${Date.now()}`,
content: getIntroduction(),
isAnswer: true,
isOpeningStatement: true
}] : [])
isOpeningStatement: true,
}]
: [])
setIsShowSuggestion(false)
}
......@@ -119,12 +120,11 @@ const Debug: FC<IDebug> = ({
}) // compatible with old version
// debugger
requiredVars.forEach(({ key }) => {
if (hasEmptyInput) {
if (hasEmptyInput)
return
}
if (!inputs[key]) {
if (!inputs[key])
hasEmptyInput = true
}
})
if (hasEmptyInput) {
......@@ -134,7 +134,6 @@ const Debug: FC<IDebug> = ({
return !hasEmptyInput
}
const [isShowSuggestion, setIsShowSuggestion] = useState(false)
const doShowSuggestion = isShowSuggestion && !isResponsing
const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
const onSend = async (message: string) => {
......@@ -147,7 +146,7 @@ const Debug: FC<IDebug> = ({
dataset: {
enabled: true,
id,
}
},
}))
const postModelConfig: BackendModelConfig = {
......@@ -155,17 +154,17 @@ const Debug: FC<IDebug> = ({
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
opening_statement: introduction,
more_like_this: {
enabled: false
enabled: false,
},
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
agent_mode: {
enabled: true,
tools: [...postDatasets]
tools: [...postDatasets],
},
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
completion_params: completionParams as any
completion_params: completionParams as any,
},
}
......@@ -215,32 +214,32 @@ const Debug: FC<IDebug> = ({
setConversationId(newConversationId)
_newConversationId = newConversationId
}
if (messageId) {
if (messageId)
responseItem.id = messageId
}
// closesure new list is outdated.
const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId)) {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
}
draft.push({ ...responseItem })
})
setChatList(newListWithAnswer)
},
async onCompleted(hasError?: boolean) {
setResponsingFalse()
if (hasError) {
if (hasError)
return
}
if (_newConversationId) {
const { data }: any = await fetchConvesationMessages(appId, _newConversationId as string)
const newResponseItem = data.find((item: any) => item.id === responseItem.id)
if (!newResponseItem) {
if (!newResponseItem)
return
}
setChatList(produce(getChatList(), draft => {
setChatList(produce(getChatList(), (draft) => {
const index = draft.findIndex(item => item.id === responseItem.id)
if (index !== -1) {
draft[index] = {
......@@ -249,7 +248,7 @@ const Debug: FC<IDebug> = ({
time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
latency: newResponseItem.provider_response_latency.toFixed(2),
}
},
}
}
}))
......@@ -263,10 +262,10 @@ const Debug: FC<IDebug> = ({
onError() {
setResponsingFalse()
// role back placeholder answer
setChatList(produce(getChatList(), draft => {
setChatList(produce(getChatList(), (draft) => {
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
}))
}
},
})
return true
}
......@@ -277,7 +276,7 @@ const Debug: FC<IDebug> = ({
}, [controlClearChatMessage])
const [completionQuery, setCompletionQuery] = useState('')
const [completionRes, setCompletionRes] = useState(``)
const [completionRes, setCompletionRes] = useState('')
const sendTextCompletion = async () => {
if (isResponsing) {
......@@ -297,7 +296,7 @@ const Debug: FC<IDebug> = ({
dataset: {
enabled: true,
id,
}
},
}))
const postModelConfig: BackendModelConfig = {
......@@ -308,16 +307,15 @@ const Debug: FC<IDebug> = ({
more_like_this: moreLikeThisConifg,
agent_mode: {
enabled: true,
tools: [...postDatasets]
tools: [...postDatasets],
},
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
completion_params: completionParams as any
completion_params: completionParams as any,
},
}
const data = {
inputs,
query: completionQuery,
......@@ -338,11 +336,10 @@ const Debug: FC<IDebug> = ({
},
onError() {
setResponsingFalse()
}
},
})
}
return (
<>
<div className="shrink-0">
......@@ -368,7 +365,7 @@ const Debug: FC<IDebug> = ({
{/* Chat */}
{mode === AppType.chat && (
<div className="mt-[34px] h-full flex flex-col">
<div className={cn(doShowSuggestion ? 'pb-[140px]' : 'pb-[66px]', "relative mt-1.5 grow h-[200px] overflow-hidden")}>
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[66px]'), 'relative mt-1.5 grow h-[200px] overflow-hidden')}>
<div className="h-full overflow-y-auto" ref={chatListDomRef}>
{/* {JSON.stringify(chatList)} */}
<Chat
......
......@@ -56,10 +56,13 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
}, [value])
const coloredContent = (tempValue || '')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />')
const handleEdit = () => {
setFocus()
}
......
......@@ -75,7 +75,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
<div
className="max-h-48 overflow-y-auto text-sm text-gray-700 break-all"
dangerouslySetInnerHTML={{
__html: format(replaceStringWithValuesWithFormat(promptTemplate, promptVariables, inputs)),
__html: format(replaceStringWithValuesWithFormat(promptTemplate.replace(/</g, '&lt;').replace(/>/g, '&gt;'), promptVariables, inputs)),
}}
>
</div>
......
......@@ -31,7 +31,7 @@ const limit = 10
const ThreeDotsIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
......
......@@ -22,7 +22,7 @@ import type { AppDetailResponse } from '@/models/app'
export type IAppCardProps = {
className?: string
appInfo: AppDetailResponse
cardType?: 'app' | 'api'
cardType?: 'app' | 'api' | 'webapp'
customBgColor?: string
onChangeStatus: (val: boolean) => Promise<any>
onSaveSiteConfig?: (params: any) => Promise<any>
......@@ -46,15 +46,16 @@ function AppCard({
const { t } = useTranslation()
const OPERATIONS_MAP = {
app: [
webapp: [
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon },
{ opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon },
{ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon },
],
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
app: [],
}
const isApp = cardType === 'app'
const isApp = cardType === 'app' || cardType === 'webapp'
const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title')
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
const { app_base_url, access_token } = appInfo.site ?? {}
......@@ -100,7 +101,7 @@ function AppCard({
<div className={`px-6 py-4 ${customBgColor ?? bgColor} rounded-lg`}>
<div className="mb-2.5 flex flex-row items-start justify-between">
<AppBasic
iconType={isApp ? 'app' : 'api'}
iconType={cardType}
icon={appInfo.icon}
icon_background={appInfo.icon_background}
name={basicName}
......
......@@ -6,12 +6,12 @@ import type { EChartsOption } from 'echarts'
import useSWR from 'swr'
import dayjs from 'dayjs'
import { get } from 'lodash-es'
import { formatNumber } from '@/utils/format'
import { useTranslation } from 'react-i18next'
import { formatNumber } from '@/utils/format'
import Basic from '@/app/components/app-sidebar/basic'
import Loading from '@/app/components/base/loading'
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppTokenCostsResponse } from '@/models/app'
import { getAppDailyConversations, getAppDailyEndUsers, getAppTokenCosts } from '@/service/apps'
import { getAppDailyConversations, getAppDailyEndUsers, getAppStatistics, getAppTokenCosts } from '@/service/apps'
const valueFormatter = (v: string | number) => v
const COLOR_TYPE_MAP = {
......@@ -76,6 +76,9 @@ export type IBizChartProps = {
export type IChartProps = {
className?: string
basicInfo: { title: string; explanation: string; timePeriod: string }
valueKey?: string
isAvg?: boolean
unit?: string
yMax?: number
chartType: IChartType
chartData: AppDailyConversationsResponse | AppDailyEndUsersResponse | AppTokenCostsResponse | { data: Array<{ date: string; count: number }> }
......@@ -85,6 +88,9 @@ const Chart: React.FC<IChartProps> = ({
basicInfo: { title, explanation, timePeriod },
chartType = 'conversations',
chartData,
valueKey,
isAvg,
unit = '',
yMax,
className,
}) => {
......@@ -96,7 +102,7 @@ const Chart: React.FC<IChartProps> = ({
extraDataForMarkLine.unshift('')
const xData = statistics.map(({ date }) => date)
const yField = Object.keys(statistics[0]).find(name => name.includes('count')) || ''
const yField = valueKey || Object.keys(statistics[0]).find(name => name.includes('count')) || ''
const yData = statistics.map((item) => {
// @ts-expect-error field is valid
return item[yField] || 0
......@@ -211,8 +217,7 @@ const Chart: React.FC<IChartProps> = ({
},
],
}
const sumData = sum(yData)
const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData)
return (
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-sm ${className ?? ''}`}>
......@@ -221,7 +226,7 @@ const Chart: React.FC<IChartProps> = ({
</div>
<div className='mb-4'>
<Basic
name={chartType !== 'costs' ? sumData.toLocaleString() : `${sumData < 1000 ? sumData : (formatNumber(Math.round(sumData / 1000)) + 'k')}`}
name={chartType !== 'costs' ? (sumData.toLocaleString() + unit) : `${sumData < 1000 ? sumData : (`${formatNumber(Math.round(sumData / 1000))}k`)}`}
type={!CHART_TYPE_CONFIG[chartType].showTokens
? ''
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
......@@ -236,9 +241,9 @@ const Chart: React.FC<IChartProps> = ({
)
}
const getDefaultChartData = ({ start, end }: { start: string; end: string }) => {
const getDefaultChartData = ({ start, end, key = 'count' }: { start: string; end: string; key?: string }) => {
const diffDays = dayjs(end).diff(dayjs(start), 'day')
return Array.from({ length: diffDays || 1 }, () => ({ date: '', count: 0 })).map((item, index) => {
return Array.from({ length: diffDays || 1 }, () => ({ date: '', [key]: 0 })).map((item, index) => {
item.date = dayjs(start).add(index, 'day').format(commonDateFormat)
return item
})
......@@ -273,6 +278,55 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
/>
}
export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-session-interactions`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.avgSessionInteractions.title'), explanation: t('appOverview.analysis.avgSessionInteractions.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...period.query, key: 'interactions' }) } as any}
chartType='conversations'
valueKey='interactions'
isAvg
{...(noDataFlag && { yMax: 500 })}
/>
}
export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-response-time`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.avgResponseTime.title'), explanation: t('appOverview.analysis.avgResponseTime.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...period.query, key: 'latency' }) } as any}
valueKey='latency'
chartType='conversations'
isAvg
unit={t('appOverview.analysis.ms') as string}
{...(noDataFlag && { yMax: 500 })}
/>
}
export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/user-satisfaction-rate`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.userSatisfactionRate.title'), explanation: t('appOverview.analysis.userSatisfactionRate.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...period.query, key: 'rate' }) } as any}
valueKey='rate'
chartType='endUsers'
isAvg
{...(noDataFlag && { yMax: 1000 })}
/>
}
export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
......
......@@ -9,7 +9,7 @@ import Toast from '@/app/components/base/toast'
import { Feedbacktype } from '@/app/components/app/chat'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks'
import { fetcMoreLikeThis, updateFeedback } from '@/service/share'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
const MAX_DEPTH = 3
export interface IGenerationItemProps {
......@@ -24,6 +24,8 @@ export interface IGenerationItemProps {
onFeedback?: (feedback: Feedbacktype) => void
onSave?: (messageId: string) => void
isMobile?: boolean
isInstalledApp: boolean,
installedAppId?: string,
}
export const SimpleBtn = ({ className, onClick, children }: {
......@@ -75,7 +77,9 @@ const GenerationItem: FC<IGenerationItemProps> = ({
onFeedback,
onSave,
depth = 1,
isMobile
isMobile,
isInstalledApp,
installedAppId,
}) => {
const { t } = useTranslation()
const isTop = depth === 1
......@@ -88,7 +92,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
})
const handleFeedback = async (childFeedback: Feedbacktype) => {
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } })
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
setChildFeedback(childFeedback)
}
......@@ -104,7 +108,9 @@ const GenerationItem: FC<IGenerationItemProps> = ({
isLoading: isQuerying,
feedback: childFeedback,
onSave,
isMobile
isMobile,
isInstalledApp,
installedAppId,
}
const handleMoreLikeThis = async () => {
......@@ -113,7 +119,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
return
}
startQuerying()
const res: any = await fetcMoreLikeThis(messageId as string)
const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
setCompletionRes(res.answer)
setChildMessageId(res.id)
stopQuerying()
......
'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { PlusIcon } from '@heroicons/react/24/outline'
export interface INoDataProps {
import Button from '@/app/components/base/button'
export type INoDataProps = {
onStartCreateContent: () => void
}
const markIcon = (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.16699 6.5C4.16699 5.09987 4.16699 4.3998 4.43948 3.86502C4.67916 3.39462 5.06161 3.01217 5.53202 2.77248C6.0668 2.5 6.76686 2.5 8.16699 2.5H11.8337C13.2338 2.5 13.9339 2.5 14.4686 2.77248C14.939 3.01217 15.3215 3.39462 15.5612 3.86502C15.8337 4.3998 15.8337 5.09987 15.8337 6.5V17.5L10.0003 14.1667L4.16699 17.5V6.5Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M4.16699 6.5C4.16699 5.09987 4.16699 4.3998 4.43948 3.86502C4.67916 3.39462 5.06161 3.01217 5.53202 2.77248C6.0668 2.5 6.76686 2.5 8.16699 2.5H11.8337C13.2338 2.5 13.9339 2.5 14.4686 2.77248C14.939 3.01217 15.3215 3.39462 15.5612 3.86502C15.8337 4.3998 15.8337 5.09987 15.8337 6.5V17.5L10.0003 14.1667L4.16699 17.5V6.5Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
const lightIcon = (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline relative -top-3 -left-1.5"><path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline relative -top-3 -left-1.5"><path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path></svg>
)
const NoData: FC<INoDataProps> = ({
onStartCreateContent
onStartCreateContent,
}) => {
const { t } = useTranslation()
......
......@@ -39,7 +39,7 @@ const AppIcon: FC<AppIconProps> = ({
}}
onClick={onClick}
>
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id={'banana'} />}
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id='🤖' />}
</span>
)
}
......
......@@ -68,9 +68,12 @@ const BlockInput: FC<IBlockInputProps> = ({
})
const coloredContent = (currentValue || '')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />')
// Not use useCallback. That will cause out callback get old data.
const handleSubmit = () => {
if (onConfirm) {
......
......@@ -156,7 +156,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
</div>
{/* Color Select */}
<div className={cn('flex flex-col p-3 ', selectedEmoji == '' ? 'opacity-25' : '')}>
<div className={cn('p-3 ', selectedEmoji == '' ? 'opacity-25' : '')}>
<p className='font-medium uppercase text-xs text-[#101828] mb-2'>Choose Style</p>
<div className='w-full h-full grid grid-cols-8 gap-1'>
{backgroundColors.map((color) => {
......
......@@ -18,7 +18,7 @@ type InputProps = {
const GlassIcon: FC<{ className?: string }> = ({ className }) => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M12.25 12.25L10.2084 10.2083M11.6667 6.70833C11.6667 9.44675 9.44675 11.6667 6.70833 11.6667C3.96992 11.6667 1.75 9.44675 1.75 6.70833C1.75 3.96992 3.96992 1.75 6.70833 1.75C9.44675 1.75 11.6667 3.96992 11.6667 6.70833Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12.25 12.25L10.2084 10.2083M11.6667 6.70833C11.6667 9.44675 9.44675 11.6667 6.70833 11.6667C3.96992 11.6667 1.75 9.44675 1.75 6.70833C1.75 3.96992 3.96992 1.75 6.70833 1.75C9.44675 1.75 11.6667 3.96992 11.6667 6.70833Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
......
......@@ -12,6 +12,7 @@ type IModal = {
description?: React.ReactNode
children: React.ReactNode
closable?: boolean
overflowVisible?: boolean
}
export default function Modal({
......@@ -23,6 +24,7 @@ export default function Modal({
description,
children,
closable = false,
overflowVisible = false,
}: IModal) {
return (
<Transition appear show={isShow} as={Fragment}>
......@@ -50,7 +52,7 @@ export default function Modal({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className={`w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all ${className}`}>
<Dialog.Panel className={`w-full max-w-md transform ${overflowVisible ? 'overflow-visible' : 'overflow-hidden'} rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all ${className}`}>
{title && <Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
......
import { FC, useCallback, useMemo, useState } from 'react'
import React from 'react'
import type { FC } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import useSWR from 'swr'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { omit } from 'lodash-es'
import cn from 'classnames'
import { ArrowRightIcon } from '@heroicons/react/24/solid'
import SegmentCard from '../completed/SegmentCard'
import { FieldInfo } from '../metadata'
import style from '../completed/style.module.css'
import { DocumentContext } from '../index'
import s from './style.module.css'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast'
import { FullDocumentDetail, ProcessRuleResponse } from '@/models/datasets'
import type { FullDocumentDetail, ProcessRuleResponse } from '@/models/datasets'
import type { CommonResponse } from '@/models/common'
import { asyncRunSafe } from '@/utils'
import { formatNumber } from '@/utils/format'
import { fetchProcessRule, fetchIndexingEstimate, fetchIndexingStatus, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'
import SegmentCard from '../completed/SegmentCard'
import { FieldInfo } from '../metadata'
import s from './style.module.css'
import style from '../completed/style.module.css'
import { DocumentContext } from '../index'
import { fetchIndexingEstimate, fetchIndexingStatus, fetchProcessRule, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'
import DatasetDetailContext from '@/context/dataset-detail'
import StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal'
import { ArrowRightIcon } from '@heroicons/react/24/solid'
type Props = {
detail?: FullDocumentDetail
......@@ -35,7 +34,7 @@ type Props = {
const StopIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<g clip-path="url(#clip0_2328_2798)">
<path d="M1.5 3.9C1.5 3.05992 1.5 2.63988 1.66349 2.31901C1.8073 2.03677 2.03677 1.8073 2.31901 1.66349C2.63988 1.5 3.05992 1.5 3.9 1.5H8.1C8.94008 1.5 9.36012 1.5 9.68099 1.66349C9.96323 1.8073 10.1927 2.03677 10.3365 2.31901C10.5 2.63988 10.5 3.05992 10.5 3.9V8.1C10.5 8.94008 10.5 9.36012 10.3365 9.68099C10.1927 9.96323 9.96323 10.1927 9.68099 10.3365C9.36012 10.5 8.94008 10.5 8.1 10.5H3.9C3.05992 10.5 2.63988 10.5 2.31901 10.3365C2.03677 10.1927 1.8073 9.96323 1.66349 9.68099C1.5 9.36012 1.5 8.94008 1.5 8.1V3.9Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M1.5 3.9C1.5 3.05992 1.5 2.63988 1.66349 2.31901C1.8073 2.03677 2.03677 1.8073 2.31901 1.66349C2.63988 1.5 3.05992 1.5 3.9 1.5H8.1C8.94008 1.5 9.36012 1.5 9.68099 1.66349C9.96323 1.8073 10.1927 2.03677 10.3365 2.31901C10.5 2.63988 10.5 3.05992 10.5 3.9V8.1C10.5 8.94008 10.5 9.36012 10.3365 9.68099C10.1927 9.96323 9.96323 10.1927 9.68099 10.3365C9.36012 10.5 8.94008 10.5 8.1 10.5H3.9C3.05992 10.5 2.63988 10.5 2.31901 10.3365C2.03677 10.1927 1.8073 9.96323 1.66349 9.68099C1.5 9.36012 1.5 8.94008 1.5 8.1V3.9Z" stroke="#344054" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_2328_2798">
......@@ -47,9 +46,8 @@ const StopIcon: FC<{ className?: string }> = ({ className }) => {
const ResumeIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M10 3.5H5C3.34315 3.5 2 4.84315 2 6.5C2 8.15685 3.34315 9.5 5 9.5H10M10 3.5L8 1.5M10 3.5L8 5.5" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M10 3.5H5C3.34315 3.5 2 4.84315 2 6.5C2 8.15685 3.34315 9.5 5 9.5H10M10 3.5L8 1.5M10 3.5L8 5.5" stroke="#344054" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
const RuleDetail: FC<{ sourceData?: ProcessRuleResponse; docName?: string }> = ({ sourceData, docName }) => {
......@@ -61,43 +59,43 @@ const RuleDetail: FC<{ sourceData?: ProcessRuleResponse; docName?: string }> = (
segmentLength: t('datasetDocuments.embedding.segmentLength'),
textCleaning: t('datasetDocuments.embedding.textCleaning'),
}
const getRuleName = (key: string) => {
if (key === 'remove_extra_spaces')
return t('datasetCreation.stepTwo.removeExtraSpaces')
if (key === 'remove_urls_emails')
return t('datasetCreation.stepTwo.removeUrlEmails')
if (key === 'remove_stopwords')
return t('datasetCreation.stepTwo.removeStopwords')
}
const getValue = useCallback((field: string) => {
let value: string | number | undefined = '-';
let value: string | number | undefined = '-'
switch (field) {
case 'docName':
value = docName
break;
break
case 'mode':
value = sourceData?.mode === 'automatic' ? (t('datasetDocuments.embedding.automatic') as string) : (t('datasetDocuments.embedding.custom') as string);
break;
value = sourceData?.mode === 'automatic' ? (t('datasetDocuments.embedding.automatic') as string) : (t('datasetDocuments.embedding.custom') as string)
break
case 'segmentLength':
value = sourceData?.rules?.segmentation?.max_tokens
break;
break
default:
value = sourceData?.mode === 'automatic' ?
(t('datasetDocuments.embedding.automatic') as string) :
sourceData?.rules?.pre_processing_rules?.map(rule => {
if (rule.enabled) {
value = sourceData?.mode === 'automatic'
? (t('datasetDocuments.embedding.automatic') as string)
// eslint-disable-next-line array-callback-return
: sourceData?.rules?.pre_processing_rules?.map((rule) => {
if (rule.enabled)
return getRuleName(rule.id)
}
}).filter(Boolean).join(';')
break;
break
}
return value
}, [sourceData, docName])
const getRuleName = (key: string) => {
if (key === 'remove_extra_spaces') {
return t('datasetCreation.stepTwo.removeExtraSpaces')
}
if (key === 'remove_urls_emails') {
return t('datasetCreation.stepTwo.removeUrlEmails')
}
if (key === 'remove_stopwords') {
return t('datasetCreation.stepTwo.removeStopwords')
}
}
return <div className='flex flex-col pt-8 pb-10 first:mt-0'>
{Object.keys(segmentationRuleMap).map((field) => {
return <FieldInfo
......@@ -134,12 +132,12 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
datasetId: localDatasetId,
documentId: localDocumentId,
}, apiParams => fetchIndexingEstimate(omit(apiParams, 'action')), {
revalidateOnFocus: false
revalidateOnFocus: false,
})
const { data: ruleDetail, error: ruleError } = useSWR({
action: 'fetchProcessRule',
params: { documentId: localDocumentId }
params: { documentId: localDocumentId },
}, apiParams => fetchProcessRule(omit(apiParams, 'action')), {
revalidateOnFocus: false,
})
......@@ -159,7 +157,8 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
const percent = useMemo(() => {
const completedCount = indexingStatusDetail?.completed_segments || 0
const totalCount = indexingStatusDetail?.total_segments || 0
if (totalCount === 0) return 0
if (totalCount === 0)
return 0
const percent = Math.round(completedCount * 100 / totalCount)
return percent > 100 ? 100 : percent
}, [indexingStatusDetail])
......@@ -170,7 +169,8 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
if (!e) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
statusMutate()
} else {
}
else {
notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })
}
}
......@@ -255,7 +255,7 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
<Divider />
<div className={s.previewTip}>{t('datasetDocuments.embedding.previewTip')}</div>
<div className={style.cardWrapper}>
{[1, 2, 3].map((v) => (
{[1, 2, 3].map(v => (
<SegmentCard loading={true} detail={{ position: v } as any} />
))}
</div>
......
'use client'
import type { FC } from 'react'
import React, { useState, useMemo } from 'react'
import React, { useMemo, useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { debounce } from 'lodash-es'
import { debounce, omit } from 'lodash-es'
// import Link from 'next/link'
import { PlusIcon } from '@heroicons/react/24/solid'
import { omit } from 'lodash-es'
import List from './list'
import s from './style.module.css'
import Loading from '@/app/components/base/loading'
......@@ -22,22 +21,22 @@ const limit = 15
const FolderPlusIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M10.8332 5.83333L9.90355 3.9741C9.63601 3.439 9.50222 3.17144 9.30265 2.97597C9.12615 2.80311 8.91344 2.67164 8.6799 2.59109C8.41581 2.5 8.11668 2.5 7.51841 2.5H4.33317C3.39975 2.5 2.93304 2.5 2.57652 2.68166C2.26292 2.84144 2.00795 3.09641 1.84816 3.41002C1.6665 3.76654 1.6665 4.23325 1.6665 5.16667V5.83333M1.6665 5.83333H14.3332C15.7333 5.83333 16.4334 5.83333 16.9681 6.10582C17.4386 6.3455 17.821 6.72795 18.0607 7.19836C18.3332 7.73314 18.3332 8.4332 18.3332 9.83333V13.5C18.3332 14.9001 18.3332 15.6002 18.0607 16.135C17.821 16.6054 17.4386 16.9878 16.9681 17.2275C16.4334 17.5 15.7333 17.5 14.3332 17.5H5.6665C4.26637 17.5 3.56631 17.5 3.03153 17.2275C2.56112 16.9878 2.17867 16.6054 1.93899 16.135C1.6665 15.6002 1.6665 14.9001 1.6665 13.5V5.83333ZM9.99984 14.1667V9.16667M7.49984 11.6667H12.4998" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M10.8332 5.83333L9.90355 3.9741C9.63601 3.439 9.50222 3.17144 9.30265 2.97597C9.12615 2.80311 8.91344 2.67164 8.6799 2.59109C8.41581 2.5 8.11668 2.5 7.51841 2.5H4.33317C3.39975 2.5 2.93304 2.5 2.57652 2.68166C2.26292 2.84144 2.00795 3.09641 1.84816 3.41002C1.6665 3.76654 1.6665 4.23325 1.6665 5.16667V5.83333M1.6665 5.83333H14.3332C15.7333 5.83333 16.4334 5.83333 16.9681 6.10582C17.4386 6.3455 17.821 6.72795 18.0607 7.19836C18.3332 7.73314 18.3332 8.4332 18.3332 9.83333V13.5C18.3332 14.9001 18.3332 15.6002 18.0607 16.135C17.821 16.6054 17.4386 16.9878 16.9681 17.2275C16.4334 17.5 15.7333 17.5 14.3332 17.5H5.6665C4.26637 17.5 3.56631 17.5 3.03153 17.2275C2.56112 16.9878 2.17867 16.6054 1.93899 16.135C1.6665 15.6002 1.6665 14.9001 1.6665 13.5V5.83333ZM9.99984 14.1667V9.16667M7.49984 11.6667H12.4998" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
const ThreeDotsIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
const NotionIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<g clip-path="url(#clip0_2164_11263)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5725 18.2611L1.4229 15.5832C0.905706 14.9389 0.625 14.1466 0.625 13.3312V3.63437C0.625 2.4129 1.60224 1.39936 2.86295 1.31328L12.8326 0.632614C13.5569 0.583164 14.2768 0.775682 14.8717 1.17794L18.3745 3.5462C19.0015 3.97012 19.375 4.66312 19.375 5.40266V16.427C19.375 17.6223 18.4141 18.6121 17.1798 18.688L6.11458 19.3692C5.12958 19.4298 4.17749 19.0148 3.5725 18.2611Z" fill="white" />
<path fillRule="evenodd" clipRule="evenodd" d="M3.5725 18.2611L1.4229 15.5832C0.905706 14.9389 0.625 14.1466 0.625 13.3312V3.63437C0.625 2.4129 1.60224 1.39936 2.86295 1.31328L12.8326 0.632614C13.5569 0.583164 14.2768 0.775682 14.8717 1.17794L18.3745 3.5462C19.0015 3.97012 19.375 4.66312 19.375 5.40266V16.427C19.375 17.6223 18.4141 18.6121 17.1798 18.688L6.11458 19.3692C5.12958 19.4298 4.17749 19.0148 3.5725 18.2611Z" fill="white" />
<path d="M7.03006 8.48669V8.35974C7.03006 8.03794 7.28779 7.77104 7.61997 7.74886L10.0396 7.58733L13.3857 12.5147V8.19009L12.5244 8.07528V8.01498C12.5244 7.68939 12.788 7.42074 13.1244 7.4035L15.326 7.29073V7.60755C15.326 7.75628 15.2154 7.88349 15.0638 7.90913L14.534 7.99874V15.0023L13.8691 15.231C13.3136 15.422 12.6952 15.2175 12.3772 14.7377L9.12879 9.83574V14.5144L10.1287 14.7057L10.1147 14.7985C10.0711 15.089 9.82028 15.3087 9.51687 15.3222L7.03006 15.4329C6.99718 15.1205 7.23132 14.841 7.55431 14.807L7.88143 14.7727V8.53453L7.03006 8.48669Z" fill="black" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9218 1.85424L2.95217 2.53491C2.35499 2.57568 1.89209 3.05578 1.89209 3.63437V13.3312C1.89209 13.8748 2.07923 14.403 2.42402 14.8325L4.57362 17.5104C4.92117 17.9434 5.46812 18.1818 6.03397 18.147L17.0991 17.4658C17.6663 17.4309 18.1078 16.9762 18.1078 16.427V5.40266C18.1078 5.06287 17.9362 4.74447 17.6481 4.54969L14.1453 2.18143C13.7883 1.94008 13.3564 1.82457 12.9218 1.85424ZM3.44654 3.78562C3.30788 3.68296 3.37387 3.46909 3.54806 3.4566L12.9889 2.77944C13.2897 2.75787 13.5886 2.8407 13.8318 3.01305L15.7261 4.35508C15.798 4.40603 15.7642 4.51602 15.6752 4.52086L5.67742 5.0646C5.37485 5.08106 5.0762 4.99217 4.83563 4.81406L3.44654 3.78562ZM5.20848 6.76919C5.20848 6.4444 5.47088 6.1761 5.80642 6.15783L16.3769 5.58216C16.7039 5.56435 16.9792 5.81583 16.9792 6.13239V15.6783C16.9792 16.0025 16.7177 16.2705 16.3829 16.2896L5.8793 16.8872C5.51537 16.9079 5.20848 16.6283 5.20848 16.2759V6.76919Z" fill="black" />
<path fillRule="evenodd" clipRule="evenodd" d="M12.9218 1.85424L2.95217 2.53491C2.35499 2.57568 1.89209 3.05578 1.89209 3.63437V13.3312C1.89209 13.8748 2.07923 14.403 2.42402 14.8325L4.57362 17.5104C4.92117 17.9434 5.46812 18.1818 6.03397 18.147L17.0991 17.4658C17.6663 17.4309 18.1078 16.9762 18.1078 16.427V5.40266C18.1078 5.06287 17.9362 4.74447 17.6481 4.54969L14.1453 2.18143C13.7883 1.94008 13.3564 1.82457 12.9218 1.85424ZM3.44654 3.78562C3.30788 3.68296 3.37387 3.46909 3.54806 3.4566L12.9889 2.77944C13.2897 2.75787 13.5886 2.8407 13.8318 3.01305L15.7261 4.35508C15.798 4.40603 15.7642 4.51602 15.6752 4.52086L5.67742 5.0646C5.37485 5.08106 5.0762 4.99217 4.83563 4.81406L3.44654 3.78562ZM5.20848 6.76919C5.20848 6.4444 5.47088 6.1761 5.80642 6.15783L16.3769 5.58216C16.7039 5.56435 16.9792 5.81583 16.9792 6.13239V15.6783C16.9792 16.0025 16.7177 16.2705 16.3829 16.2896L5.8793 16.8872C5.51537 16.9079 5.20848 16.6283 5.20848 16.2759V6.76919Z" fill="black" />
</g>
<defs>
<clipPath id="clip0_2164_11263">
......
/* eslint-disable no-mixed-operators */
'use client'
import type { FC } from 'react'
import React, { useState, useEffect } from 'react'
import { TrashIcon, ArrowDownIcon } from '@heroicons/react/24/outline'
import React, { useEffect, useState } from 'react'
import { ArrowDownIcon, TrashIcon } from '@heroicons/react/24/outline'
import { ExclamationCircleIcon } from '@heroicons/react/24/solid'
import dayjs from 'dayjs'
import { pick } from 'lodash-es'
import { useContext } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import s from './style.module.css'
import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider'
import Popover from '@/app/components/base/popover'
......@@ -20,26 +23,24 @@ import Indicator from '@/app/components/header/indicator'
import { asyncRunSafe } from '@/utils'
import { formatNumber } from '@/utils/format'
import { archiveDocument, deleteDocument, disableDocument, enableDocument } from '@/service/datasets'
import type { DocumentListResponse, DocumentDisplayStatus } from '@/models/datasets'
import type { DocumentDisplayStatus, DocumentListResponse } from '@/models/datasets'
import type { CommonResponse } from '@/models/common'
import cn from 'classnames'
import s from './style.module.css'
export const SettingsIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M2 5.33325L10 5.33325M10 5.33325C10 6.43782 10.8954 7.33325 12 7.33325C13.1046 7.33325 14 6.43782 14 5.33325C14 4.22868 13.1046 3.33325 12 3.33325C10.8954 3.33325 10 4.22868 10 5.33325ZM6 10.6666L14 10.6666M6 10.6666C6 11.7712 5.10457 12.6666 4 12.6666C2.89543 12.6666 2 11.7712 2 10.6666C2 9.56202 2.89543 8.66659 4 8.66659C5.10457 8.66659 6 9.56202 6 10.6666Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M2 5.33325L10 5.33325M10 5.33325C10 6.43782 10.8954 7.33325 12 7.33325C13.1046 7.33325 14 6.43782 14 5.33325C14 4.22868 13.1046 3.33325 12 3.33325C10.8954 3.33325 10 4.22868 10 5.33325ZM6 10.6666L14 10.6666M6 10.6666C6 11.7712 5.10457 12.6666 4 12.6666C2.89543 12.6666 2 11.7712 2 10.6666C2 9.56202 2.89543 8.66659 4 8.66659C5.10457 8.66659 6 9.56202 6 10.6666Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
export const FilePlusIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M13.3332 6.99992V4.53325C13.3332 3.41315 13.3332 2.85309 13.1152 2.42527C12.9234 2.04895 12.6175 1.74299 12.2412 1.55124C11.8133 1.33325 11.2533 1.33325 10.1332 1.33325H5.8665C4.7464 1.33325 4.18635 1.33325 3.75852 1.55124C3.3822 1.74299 3.07624 2.04895 2.88449 2.42527C2.6665 2.85309 2.6665 3.41315 2.6665 4.53325V11.4666C2.6665 12.5867 2.6665 13.1467 2.88449 13.5746C3.07624 13.9509 3.3822 14.2569 3.75852 14.4486C4.18635 14.6666 4.7464 14.6666 5.8665 14.6666H7.99984M9.33317 7.33325H5.33317M6.6665 9.99992H5.33317M10.6665 4.66659H5.33317M11.9998 13.9999V9.99992M9.99984 11.9999H13.9998" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M13.3332 6.99992V4.53325C13.3332 3.41315 13.3332 2.85309 13.1152 2.42527C12.9234 2.04895 12.6175 1.74299 12.2412 1.55124C11.8133 1.33325 11.2533 1.33325 10.1332 1.33325H5.8665C4.7464 1.33325 4.18635 1.33325 3.75852 1.55124C3.3822 1.74299 3.07624 2.04895 2.88449 2.42527C2.6665 2.85309 2.6665 3.41315 2.6665 4.53325V11.4666C2.6665 12.5867 2.6665 13.1467 2.88449 13.5746C3.07624 13.9509 3.3822 14.2569 3.75852 14.4486C4.18635 14.6666 4.7464 14.6666 5.8665 14.6666H7.99984M9.33317 7.33325H5.33317M6.6665 9.99992H5.33317M10.6665 4.66659H5.33317M11.9998 13.9999V9.99992M9.99984 11.9999H13.9998" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
export const ArchiveIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M2.66683 5.33106C2.55749 5.32824 2.47809 5.32191 2.40671 5.30771C1.87779 5.2025 1.46432 4.78904 1.35912 4.26012C1.3335 4.13132 1.3335 3.97644 1.3335 3.66667C1.3335 3.3569 1.3335 3.20201 1.35912 3.07321C1.46432 2.54429 1.87779 2.13083 2.40671 2.02562C2.53551 2 2.69039 2 3.00016 2H13.0002C13.3099 2 13.4648 2 13.5936 2.02562C14.1225 2.13083 14.536 2.54429 14.6412 3.07321C14.6668 3.20201 14.6668 3.3569 14.6668 3.66667C14.6668 3.97644 14.6668 4.13132 14.6412 4.26012C14.536 4.78904 14.1225 5.2025 13.5936 5.30771C13.5222 5.32191 13.4428 5.32824 13.3335 5.33106M6.66683 8.66667H9.3335M2.66683 5.33333H13.3335V10.8C13.3335 11.9201 13.3335 12.4802 13.1155 12.908C12.9238 13.2843 12.6178 13.5903 12.2415 13.782C11.8137 14 11.2536 14 10.1335 14H5.86683C4.74672 14 4.18667 14 3.75885 13.782C3.38252 13.5903 3.07656 13.2843 2.88482 12.908C2.66683 12.4802 2.66683 11.9201 2.66683 10.8V5.33333Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M2.66683 5.33106C2.55749 5.32824 2.47809 5.32191 2.40671 5.30771C1.87779 5.2025 1.46432 4.78904 1.35912 4.26012C1.3335 4.13132 1.3335 3.97644 1.3335 3.66667C1.3335 3.3569 1.3335 3.20201 1.35912 3.07321C1.46432 2.54429 1.87779 2.13083 2.40671 2.02562C2.53551 2 2.69039 2 3.00016 2H13.0002C13.3099 2 13.4648 2 13.5936 2.02562C14.1225 2.13083 14.536 2.54429 14.6412 3.07321C14.6668 3.20201 14.6668 3.3569 14.6668 3.66667C14.6668 3.97644 14.6668 4.13132 14.6412 4.26012C14.536 4.78904 14.1225 5.2025 13.5936 5.30771C13.5222 5.32191 13.4428 5.32824 13.3335 5.33106M6.66683 8.66667H9.3335M2.66683 5.33333H13.3335V10.8C13.3335 11.9201 13.3335 12.4802 13.1155 12.908C12.9238 13.2843 12.6178 13.5903 12.2415 13.782C11.8137 14 11.2536 14 10.1335 14H5.86683C4.74672 14 4.18667 14 3.75885 13.782C3.38252 13.5903 3.07656 13.2843 2.88482 12.908C2.66683 12.4802 2.66683 11.9201 2.66683 10.8V5.33333Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
......@@ -59,12 +60,12 @@ export const useIndexStatus = () => {
// status item for list
export const StatusItem: FC<{
status: DocumentDisplayStatus;
reverse?: boolean;
status: DocumentDisplayStatus
reverse?: boolean
scene?: 'list' | 'detail'
textCls?: string
}> = ({ status, reverse = false, scene = 'list', textCls = '' }) => {
const DOC_INDEX_STATUS_MAP = useIndexStatus();
const DOC_INDEX_STATUS_MAP = useIndexStatus()
const localStatus = status.toLowerCase() as keyof typeof DOC_INDEX_STATUS_MAP
return <div className={
cn('flex items-center',
......@@ -81,11 +82,11 @@ type OperationName = 'delete' | 'archive' | 'enable' | 'disable'
// operation action for list and detail
export const OperationAction: FC<{
detail: {
enabled: boolean;
archived: boolean;
enabled: boolean
archived: boolean
id: string
}
datasetId: string;
datasetId: string
onUpdate: () => void
scene?: 'list' | 'detail'
className?: string
......@@ -95,7 +96,7 @@ export const OperationAction: FC<{
const { notify } = useContext(ToastContext)
const { t } = useTranslation()
const isListScene = scene === 'list';
const isListScene = scene === 'list'
const onOperate = async (operationName: OperationName) => {
let opApi = deleteDocument
......@@ -123,16 +124,16 @@ export const OperationAction: FC<{
return <div
className='flex items-center'
onClick={(e) => e.stopPropagation()}
onClick={e => e.stopPropagation()}
>
{isListScene && <>
{archived ?
<Tooltip selector={`list-switch-${id}`} content={t('datasetDocuments.list.action.enableWarning') as string} className='!font-semibold'>
{archived
? <Tooltip selector={`list-switch-${id}`} content={t('datasetDocuments.list.action.enableWarning') as string} className='!font-semibold'>
<div>
<Switch defaultValue={false} onChange={() => { }} disabled={true} size='md' />
</div>
</Tooltip> :
<Switch defaultValue={enabled} onChange={v => onOperate(v ? 'enable' : 'disable')} size='md' />
</Tooltip>
: <Switch defaultValue={enabled} onChange={v => onOperate(v ? 'enable' : 'disable')} size='md' />
}
<Divider className='!ml-4 !mr-2 !h-3' type='vertical' />
</>}
......@@ -187,7 +188,7 @@ export const OperationAction: FC<{
trigger='click'
position='br'
btnElement={<div className={cn(s.actionIcon, s.commonIcon)} />}
btnClassName={(open) => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!bg-gray-100 !shadow-none' : '!bg-transparent')}
btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!bg-gray-100 !shadow-none' : '!bg-transparent')}
className={`!w-[200px] h-fit !z-20 ${className}`}
/>
{showModal && <Modal isShow={showModal} onClose={() => setShowModal(false)} className={s.delModal} closable>
......@@ -221,12 +222,12 @@ export const renderTdValue = (value: string | number | null, isEmptyStyle = fals
}
const renderCount = (count: number | undefined) => {
if (!count) {
if (!count)
return renderTdValue(0, true)
}
if (count < 1000) {
return count;
}
if (count < 1000)
return count
return `${formatNumber((count / 1000).toFixed(1))}k`
}
......@@ -242,20 +243,21 @@ type IDocumentListProps = {
const DocumentList: FC<IDocumentListProps> = ({ documents = [], datasetId, onUpdate }) => {
const { t } = useTranslation()
const router = useRouter()
const [localDocs, setLocalDocs] = useState<DocumentListResponse['data']>(documents);
const [enableSort, setEnableSort] = useState(false);
const [localDocs, setLocalDocs] = useState<DocumentListResponse['data']>(documents)
const [enableSort, setEnableSort] = useState(false)
useEffect(() => {
setLocalDocs(documents)
}, [documents])
const onClickSort = () => {
setEnableSort(!enableSort);
setEnableSort(!enableSort)
if (!enableSort) {
const sortedDocs = [...localDocs].sort((a, b) => dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? -1 : 1);
setLocalDocs(sortedDocs);
} else {
setLocalDocs(documents);
const sortedDocs = [...localDocs].sort((a, b) => dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? -1 : 1)
setLocalDocs(sortedDocs)
}
else {
setLocalDocs(documents)
}
}
......@@ -290,7 +292,7 @@ const DocumentList: FC<IDocumentListProps> = ({ documents = [], datasetId, onUpd
<td className='text-left align-middle text-gray-500 text-xs'>{doc.position}</td>
<td className={s.tdValue}>
<div className={cn(s[`${doc?.data_source_info?.upload_file?.extension ?? suffix}Icon`], s.commonIcon, 'mr-1.5')}></div>
<span>{doc?.name?.replace(/\.[^/.]+$/, "")}<span className='text-gray-500'>.{suffix}</span></span>
<span>{doc?.name?.replace(/\.[^/.]+$/, '')}<span className='text-gray-500'>.{suffix}</span></span>
</td>
<td>{renderCount(doc.word_count)}</td>
<td>{renderCount(doc.hit_count)}</td>
......
'use client'
import { useState } from 'react'
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { BookOpenIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
......@@ -7,8 +8,8 @@ import { ToastContext } from '@/app/components/base/toast'
import PermissionsRadio from '../permissions-radio'
import IndexMethodRadio from '../index-method-radio'
import Button from '@/app/components/base/button'
import { useDatasetsContext } from '@/context/datasets-context'
import { updateDatasetSetting } from '@/service/datasets'
import { updateDatasetSetting, fetchDataDetail } from '@/service/datasets'
import { DataSet } from '@/models/datasets'
const rowClass = `
flex justify-between py-4
......@@ -20,13 +21,25 @@ const inputClass = `
w-[480px] px-3 bg-gray-100 text-sm text-gray-800 rounded-lg outline-none appearance-none
`
const Form = () => {
const useInitialValue = <T,>(depend: T, dispatch: Dispatch<SetStateAction<T>>) => {
useEffect(() => {
dispatch(depend)
}, [depend])
}
type Props = {
datasetId: string
}
const Form = ({
datasetId
}: Props) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { currentDataset, mutateDatasets } = useDatasetsContext()
const { data: currentDataset, mutate: mutateDatasets } = useSWR(datasetId, fetchDataDetail)
const [loading, setLoading] = useState(false)
const [name, setName] = useState(currentDataset?.name)
const [description, setDescription] = useState(currentDataset?.description)
const [name, setName] = useState(currentDataset?.name ?? '')
const [description, setDescription] = useState(currentDataset?.description ?? '')
const [permission, setPermission] = useState(currentDataset?.permission)
const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
......@@ -48,7 +61,7 @@ const Form = () => {
}
})
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutateDatasets()
await mutateDatasets()
} catch (e) {
notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })
} finally {
......@@ -56,6 +69,11 @@ const Form = () => {
}
}
useInitialValue<string>(currentDataset?.name ?? '', setName)
useInitialValue<string>(currentDataset?.description ?? '', setDescription)
useInitialValue<DataSet['permission'] | undefined>(currentDataset?.permission, setPermission)
useInitialValue<DataSet['indexing_technique'] | undefined>(currentDataset?.indexing_technique, setIndexMethod)
return (
<div className='w-[800px] px-16 py-6'>
<div className={rowClass}>
......
'use client'
import React, { useEffect, useState } from 'react'
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
import copy from 'copy-to-clipboard'
import Tooltip from '@/app/components/base/tooltip'
import { t } from 'i18next'
import s from './style.module.css'
......@@ -18,7 +18,6 @@ const InputCopy = ({
readOnly = true,
children,
}: IInputCopyProps) => {
const [_, copy] = useCopyToClipboard()
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
......
'use client'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/20/solid'
import Button from '../../base/button'
import s from './style.module.css'
import type { App } from '@/models/explore'
import AppModeLabel from '@/app/(commonLayout)/apps/AppModeLabel'
import AppIcon from '@/app/components/base/app-icon'
const CustomizeBtn = (
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 2.33366C6.69458 2.33366 6.04167 2.98658 6.04167 3.79199C6.04167 4.59741 6.69458 5.25033 7.5 5.25033C8.30542 5.25033 8.95833 4.59741 8.95833 3.79199C8.95833 2.98658 8.30542 2.33366 7.5 2.33366ZM7.5 2.33366V1.16699M12.75 8.71385C11.4673 10.1671 9.59071 11.0837 7.5 11.0837C5.40929 11.0837 3.53265 10.1671 2.25 8.71385M6.76782 5.05298L2.25 12.8337M8.23218 5.05298L12.75 12.8337" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)
export type AppCardProps = {
app: App
canCreate: boolean
onCreate: () => void
onAddToWorkspace: (appId: string) => void
}
const AppCard = ({
app,
canCreate,
onCreate,
onAddToWorkspace,
}: AppCardProps) => {
const { t } = useTranslation()
const { app: appBasicInfo } = app
return (
<div className={s.wrap}>
<div className='col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<AppIcon size='small' icon={app.app.icon} background={app.app.icon_background} />
<div className='relative h-8 text-sm font-medium leading-8 grow'>
<div className='absolute top-0 left-0 w-full h-full overflow-hidden text-ellipsis whitespace-nowrap'>{appBasicInfo.name}</div>
</div>
</div>
<div className='mb-3 px-[14px] h-9 text-xs leading-normal text-gray-500 line-clamp-2'>{app.description}</div>
<div className='flex items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px]'>
<div className={s.mode}>
<AppModeLabel mode={appBasicInfo.mode} />
</div>
<div className={cn(s.opWrap, 'flex items-center w-full space-x-2')}>
<Button type='primary' className='grow flex items-center !h-7' onClick={() => onAddToWorkspace(appBasicInfo.id)}>
<PlusIcon className='w-4 h-4 mr-1' />
<span className='text-xs'>{t('explore.appCard.addToWorkspace')}</span>
</Button>
{canCreate && (
<Button className='grow flex items-center !h-7 space-x-1' onClick={onCreate}>
{CustomizeBtn}
<span className='text-xs'>{t('explore.appCard.customize')}</span>
</Button>
)}
</div>
</div>
</div>
</div>
)
}
export default AppCard
.wrap {
min-width: 312px;
}
.mode {
display: flex;
height: 28px;
}
.opWrap {
display: none;
}
.wrap:hover .mode {
display: none;
}
.wrap:hover .opWrap {
display: flex;
}
\ No newline at end of file
'use client'
import React, { FC, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import ExploreContext from '@/context/explore-context'
import { App } from '@/models/explore'
import Category from '@/app/components/explore/category'
import AppCard from '@/app/components/explore/app-card'
import { fetchAppList, installApp, fetchAppDetail } from '@/service/explore'
import { createApp } from '@/service/apps'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import Loading from '@/app/components/base/loading'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import s from './style.module.css'
import Toast from '../../base/toast'
const Apps: FC = ({ }) => {
const { t } = useTranslation()
const router = useRouter()
const { setControlUpdateInstalledApps, hasEditPermission } = useContext(ExploreContext)
const [currCategory, setCurrCategory] = React.useState('')
const [allList, setAllList] = React.useState<App[]>([])
const [isLoaded, setIsLoaded] = React.useState(false)
const currList = (() => {
if(currCategory === '') return allList
return allList.filter(item => item.category === currCategory)
})()
const [categories, setCategories] = React.useState([])
useEffect(() => {
(async () => {
const {categories, recommended_apps}:any = await fetchAppList()
setCategories(categories)
setAllList(recommended_apps)
setIsLoaded(true)
})()
}, [])
const handleAddToWorkspace = async (appId: string) => {
await installApp(appId)
Toast.notify({
type: 'success',
message: t('common.api.success'),
})
setControlUpdateInstalledApps(Date.now())
}
const [currApp, setCurrApp] = React.useState<App | null>(null)
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
const onCreate = async ({name, icon, icon_background}: any) => {
const { app_model_config: model_config } = await fetchAppDetail(currApp?.app.id as string)
try {
const app = await createApp({
name,
icon,
icon_background,
mode: currApp?.app.mode as any,
config: model_config,
})
setIsShowCreateModal(false)
Toast.notify({
type: 'success',
message: t('app.newApp.appCreated'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
router.push(`/app/${app.id}/overview`)
} catch (e) {
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
if(!isLoaded) {
return (
<div className='flex h-full items-center'>
<Loading type='area' />
</div>
)
}
return (
<div className='h-full flex flex-col'>
<div className='shrink-0 pt-6 px-12'>
<div className='mb-1 text-primary-600 text-xl font-semibold'>{t('explore.apps.title')}</div>
<div className='text-gray-500 text-sm'>{t('explore.apps.description')}</div>
</div>
<Category
className='mt-6 px-12'
list={categories}
value={currCategory}
onChange={setCurrCategory}
/>
<div
className='flex mt-6 flex-col overflow-auto bg-gray-100 shrink-0 grow'
style={{
maxHeight: 'calc(100vh - 243px)'
}}
>
<nav
className={`${s.appList} grid content-start grid-cols-1 gap-4 px-12 pb-10grow shrink-0`}>
{currList.map(app => (
<AppCard
key={app.app_id}
app={app}
canCreate={hasEditPermission}
onCreate={() => {
setCurrApp(app)
setIsShowCreateModal(true)
}}
onAddToWorkspace={handleAddToWorkspace}
/>
))}
</nav>
</div>
{isShowCreateModal && (
<CreateAppModal
appName={currApp?.app.name || ''}
show={isShowCreateModal}
onConfirm={onCreate}
onHide={() => setIsShowCreateModal(false)}
/>
)}
</div>
)
}
export default React.memo(Apps)
@media (min-width: 1624px) {
.appList {
grid-template-columns: repeat(4, minmax(0, 1fr))
}
}
@media (min-width: 1300px) and (max-width: 1624px) {
.appList {
grid-template-columns: repeat(3, minmax(0, 1fr))
}
}
@media (min-width: 1025px) and (max-width: 1300px) {
.appList {
grid-template-columns: repeat(2, minmax(0, 1fr))
}
}
\ No newline at end of file
'use client'
import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import exploreI18n from '@/i18n/lang/explore.en'
import cn from 'classnames'
const categoryI18n = exploreI18n.category
export interface ICategoryProps {
className?: string
list: string[]
value: string
onChange: (value: string) => void
}
const Category: FC<ICategoryProps> = ({
className,
list,
value,
onChange
}) => {
const { t } = useTranslation()
const itemClassName = (isSelected: boolean) => cn(isSelected ? 'bg-white text-primary-600 border-gray-200 font-semibold' : 'border-transparent font-medium','flex items-center h-7 px-3 border cursor-pointer rounded-lg')
const itemStyle = (isSelected: boolean) => isSelected ? {boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'} : {}
return (
<div className={cn(className, 'flex space-x-1 text-[13px]')}>
<div
className={itemClassName('' === value)}
style={itemStyle('' === value)}
onClick={() => onChange('')}
>
{t('explore.apps.allCategories')}
</div>
{list.map(name => (
<div
key={name}
className={itemClassName(name === value)}
style={itemStyle(name === value)}
onClick={() => onChange(name)}
>
{(categoryI18n as any)[name] ? t(`explore.category.${name}`) : name}
</div>
))}
</div>
)
}
export default React.memo(Category)
'use client'
import React, { useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import EmojiPicker from '@/app/components/base/emoji-picker'
import s from './style.module.css'
type IProps = {
appName: string,
show: boolean,
onConfirm: (info: any) => void,
onHide: () => void,
}
const CreateAppModal = ({
appName,
show = false,
onConfirm,
onHide,
}: IProps) => {
const { t } = useTranslation()
const [name, setName] = React.useState('')
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const submit = () => {
if(!name.trim()) {
Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') })
return
}
onConfirm({
name,
...emoji,
})
onHide()
}
return (
<>
<Modal
isShow={show}
onClose={onHide}
className={cn(s.modal, '!max-w-[480px]', 'px-8')}
>
<span className={s.close} onClick={onHide}/>
<div className={s.title}>{t('explore.appCustomize.title', {name: appName})}</div>
<div className={s.content}>
<div className={s.subTitle}>{t('explore.appCustomize.subTitle')}</div>
<div className='flex items-center justify-between space-x-3'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
<input
value={name}
onChange={e => setName(e.target.value)}
className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
/>
</div>
</div>
<div className='flex flex-row-reverse'>
<Button className='w-24 ml-2' type='primary' onClick={submit}>{t('common.operation.create')}</Button>
<Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
</div>
</Modal>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
console.log(icon, icon_background)
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}
</>
)
}
export default CreateAppModal
.modal {
position: relative;
}
.modal .close {
position: absolute;
right: 16px;
top: 25px;
width: 32px;
height: 32px;
border-radius: 8px;
background: center no-repeat url(~@/app/components/datasets/create/assets/close.svg);
background-size: 16px;
cursor: pointer;
}
.modal .title {
@apply mb-9;
font-weight: 600;
font-size: 20px;
line-height: 30px;
color: #101828;
}
.modal .content {
@apply mb-9;
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #101828;
}
.subTitle {
margin-bottom: 8px;
font-weight: 500;
}
\ No newline at end of file
'use client'
import React, { FC, useEffect, useState } from 'react'
import ExploreContext from '@/context/explore-context'
import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context'
import { fetchMembers } from '@/service/common'
import { InstalledApp } from '@/models/explore'
import { useTranslation } from 'react-i18next'
export interface IExploreProps {
children: React.ReactNode
}
const Explore: FC<IExploreProps> = ({
children
}) => {
const { t } = useTranslation()
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
const { userProfile } = useAppContext()
const [hasEditPermission, setHasEditPermission] = useState(false)
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
useEffect(() => {
document.title = `${t('explore.title')} - Dify`;
(async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {}})
if(!accounts) return
const currUser = accounts.find(account => account.id === userProfile.id)
setHasEditPermission(currUser?.role !== 'normal')
})()
}, [])
return (
<div className='flex h-full bg-gray-100 border-t border-gray-200'>
<ExploreContext.Provider
value={
{
controlUpdateInstalledApps,
setControlUpdateInstalledApps,
hasEditPermission,
installedApps,
setInstalledApps
}
}
>
<Sidebar controlUpdateInstalledApps={controlUpdateInstalledApps} />
<div className='grow'>
{children}
</div>
</ExploreContext.Provider>
</div>
)
}
export default React.memo(Explore)
'use client'
import React, { FC } from 'react'
import { useContext } from 'use-context-selector'
import ExploreContext from '@/context/explore-context'
import ChatApp from '@/app/components/share/chat'
import TextGenerationApp from '@/app/components/share/text-generation'
import Loading from '@/app/components/base/loading'
export interface IInstalledAppProps {
id: string
}
const InstalledApp: FC<IInstalledAppProps> = ({
id,
}) => {
const { installedApps } = useContext(ExploreContext)
const installedApp = installedApps.find(item => item.id === id)
if(!installedApp) {
return (
<div className='flex h-full items-center'>
<Loading type='area' />
</div>
)
}
return (
<div className='h-full p-2'>
{installedApp?.app.mode === 'chat' ? (
<ChatApp isInstalledApp installedAppInfo={installedApp}/>
): (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
)}
</div>
)
}
export default React.memo(InstalledApp)
'use client'
import React, { FC } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import Popover from '@/app/components/base/popover'
import { TrashIcon } from '@heroicons/react/24/outline'
import s from './style.module.css'
const PinIcon = (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00012 9.99967L8.00012 14.6663M5.33346 4.87176V6.29217C5.33346 6.43085 5.33346 6.50019 5.31985 6.56652C5.30777 6.62536 5.2878 6.6823 5.26047 6.73579C5.22966 6.79608 5.18635 6.85023 5.09972 6.95852L4.0532 8.26667C3.60937 8.82145 3.38746 9.09884 3.38721 9.33229C3.38699 9.53532 3.4793 9.72738 3.63797 9.85404C3.82042 9.99967 4.17566 9.99967 4.88612 9.99967H11.1141C11.8246 9.99967 12.1798 9.99967 12.3623 9.85404C12.5209 9.72738 12.6133 9.53532 12.613 9.33229C12.6128 9.09884 12.3909 8.82145 11.947 8.26667L10.9005 6.95852C10.8139 6.85023 10.7706 6.79608 10.7398 6.73579C10.7125 6.6823 10.6925 6.62536 10.6804 6.56652C10.6668 6.50019 10.6668 6.43085 10.6668 6.29217V4.87176C10.6668 4.79501 10.6668 4.75664 10.6711 4.71879C10.675 4.68517 10.6814 4.6519 10.6903 4.61925C10.7003 4.5825 10.7146 4.54687 10.7431 4.47561L11.415 2.79582C11.611 2.30577 11.709 2.06074 11.6682 1.86404C11.6324 1.69203 11.5302 1.54108 11.3838 1.44401C11.2163 1.33301 10.9524 1.33301 10.4246 1.33301H5.57563C5.04782 1.33301 4.78391 1.33301 4.61646 1.44401C4.47003 1.54108 4.36783 1.69203 4.33209 1.86404C4.29122 2.06074 4.38923 2.30577 4.58525 2.79583L5.25717 4.47561C5.28567 4.54687 5.29992 4.5825 5.30995 4.61925C5.31886 4.6519 5.32526 4.68517 5.32912 4.71879C5.33346 4.75664 5.33346 4.79501 5.33346 4.87176Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)
export interface IItemOperationProps {
className?: string
isPinned: boolean
isShowDelete: boolean
togglePin: () => void
onDelete: () => void
}
const ItemOperation: FC<IItemOperationProps> = ({
className,
isPinned,
isShowDelete,
togglePin,
onDelete
}) => {
const { t } = useTranslation()
return (
<Popover
htmlContent={
<div className='w-full py-1' onClick={(e) => {
e.stopPropagation()
}}>
<div className={cn(s.actionItem, 'hover:bg-gray-50 group')} onClick={togglePin}>
{PinIcon}
<span className={s.actionName}>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
</div>
{isShowDelete && (
<div className={cn(s.actionItem, s.deleteActionItem, 'hover:bg-gray-50 group')} onClick={onDelete} >
<TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
<span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('explore.sidebar.action.delete')}</span>
</div>
)}
</div>
}
trigger='click'
position='br'
btnElement={<div />}
btnClassName={(open) => cn(className, s.btn, 'h-6 w-6 rounded-md border-none p-1', open && '!bg-gray-100 !shadow-none')}
className={`!w-[120px] h-fit !z-20`}
/>
)
}
export default React.memo(ItemOperation)
.actionItem {
@apply h-9 py-2 px-3 mx-1 flex items-center gap-2 rounded-lg cursor-pointer;
}
.actionName {
@apply text-gray-700 text-sm;
}
.commonIcon {
@apply w-4 h-4 inline-block align-middle;
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
}
.actionIcon {
@apply bg-gray-500;
mask-image: url(~@/app/components/datasets/documents/assets/action.svg);
}
body .btn {
background: url(~@/app/components/datasets/documents/assets/action.svg) center center no-repeat transparent;
background-size: 16px 16px;
/* mask-image: ; */
}
body .btn:hover {
/* background-image: ; */
background-color: #F2F4F7;
}
\ No newline at end of file
'use client'
import cn from 'classnames'
import { useRouter } from 'next/navigation'
import ItemOperation from '@/app/components/explore/item-operation'
import AppIcon from '@/app/components/base/app-icon'
import s from './style.module.css'
export interface IAppNavItemProps {
name: string
id: string
icon: string
icon_background: string
isSelected: boolean
isPinned: boolean
togglePin: () => void
uninstallable: boolean
onDelete: (id: string) => void
}
export default function AppNavItem({
name,
id,
icon,
icon_background,
isSelected,
isPinned,
togglePin,
uninstallable,
onDelete,
}: IAppNavItemProps) {
const router = useRouter()
const url = `/explore/installed/${id}`
return (
<div
key={id}
className={cn(
s.item,
isSelected ? s.active : 'hover:bg-gray-200',
'flex h-8 justify-between px-2 rounded-lg text-sm font-normal ',
)}
onClick={() => {
router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation().
}}
>
<div className='flex items-center space-x-2 w-0 grow'>
{/* <div
className={cn(
'shrink-0 mr-2 h-6 w-6 rounded-md border bg-[#D5F5F6]',
)}
style={{
borderColor: '0.5px solid rgba(0, 0, 0, 0.05)'
}}
/> */}
<AppIcon size='tiny' icon={icon} background={icon_background} />
<div className='overflow-hidden text-ellipsis whitespace-nowrap'>{name}</div>
</div>
{
!isSelected && (
<div className={cn(s.opBtn, 'shrink-0')} onClick={e => e.stopPropagation()}>
<ItemOperation
isPinned={isPinned}
togglePin={togglePin}
isShowDelete={!uninstallable}
onDelete={() => onDelete(id)}
/>
</div>
)
}
</div>
)
}
/* .item:hover, */
.item.active {
border: 0.5px solid #EAECF0;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
border-radius: 8px;
background: #FFFFFF;
color: #344054;
font-weight: 500;
}
.opBtn {
visibility: hidden;
}
.item:hover .opBtn {
visibility: visible;
}
\ No newline at end of file
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import cn from 'classnames'
import { useSelectedLayoutSegments } from 'next/navigation'
import Link from 'next/link'
import Toast from '../../base/toast'
import Item from './app-nav-item'
import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore'
import ExploreContext from '@/context/explore-context'
import Confirm from '@/app/components/base/confirm'
const SelectedDiscoveryIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M13.4135 1.11725C13.5091 1.09983 13.6483 1.08355 13.8078 1.11745C14.0143 1.16136 14.2017 1.26953 14.343 1.42647C14.4521 1.54766 14.5076 1.67634 14.5403 1.76781C14.5685 1.84673 14.593 1.93833 14.6136 2.01504L15.5533 5.5222C15.5739 5.5989 15.5985 5.69049 15.6135 5.77296C15.6309 5.86852 15.6472 6.00771 15.6133 6.16722C15.5694 6.37378 15.4612 6.56114 15.3043 6.70245C15.1831 6.81157 15.0544 6.86706 14.9629 6.89975C14.884 6.92796 14.7924 6.95247 14.7157 6.97299L14.676 6.98364C14.3365 7.07461 14.0437 7.15309 13.7972 7.19802C13.537 7.24543 13.2715 7.26736 12.9946 7.20849C12.7513 7.15677 12.5213 7.06047 12.3156 6.92591L9.63273 7.64477C9.86399 7.97104 9.99992 8.36965 9.99992 8.80001C9.99992 9.2424 9.85628 9.65124 9.6131 9.98245L12.5508 14.291C12.7582 14.5952 12.6797 15.01 12.3755 15.2174C12.0713 15.4248 11.6566 15.3464 11.4492 15.0422L8.51171 10.7339C8.34835 10.777 8.17682 10.8 7.99992 10.8C7.82305 10.8 7.65155 10.777 7.48823 10.734L4.5508 15.0422C4.34338 15.3464 3.92863 15.4248 3.62442 15.2174C3.32021 15.01 3.24175 14.5952 3.44916 14.291L6.3868 9.98254C6.14358 9.65132 5.99992 9.24244 5.99992 8.80001C5.99992 8.73795 6.00274 8.67655 6.00827 8.61594L4.59643 8.99424C4.51973 9.01483 4.42813 9.03941 4.34567 9.05444C4.25011 9.07185 4.11092 9.08814 3.95141 9.05423C3.74485 9.01033 3.55748 8.90215 3.41618 8.74522C3.38535 8.71097 3.3588 8.67614 3.33583 8.64171L2.49206 8.8678C2.41536 8.88838 2.32376 8.91296 2.2413 8.92799C2.14574 8.94541 2.00655 8.96169 1.84704 8.92779C1.64048 8.88388 1.45311 8.77571 1.31181 8.61877C1.20269 8.49759 1.1472 8.3689 1.1145 8.27744C1.08629 8.1985 1.06177 8.10689 1.04125 8.03018L0.791701 7.09885C0.771119 7.02215 0.746538 6.93055 0.731508 6.84809C0.714092 6.75253 0.697808 6.61334 0.731712 6.45383C0.775619 6.24726 0.883793 6.0599 1.04073 5.9186C1.16191 5.80948 1.2906 5.75399 1.38206 5.72129C1.461 5.69307 1.55261 5.66856 1.62932 5.64804L2.47318 5.42193C2.47586 5.38071 2.48143 5.33735 2.49099 5.29237C2.5349 5.08581 2.64307 4.89844 2.80001 4.75714C2.92119 4.64802 3.04988 4.59253 3.14134 4.55983C3.22027 4.53162 3.31189 4.50711 3.3886 4.48658L11.1078 2.41824C11.2186 2.19888 11.3697 2.00049 11.5545 1.83406C11.7649 1.64462 12.0058 1.53085 12.2548 1.44183C12.4907 1.35749 12.7836 1.27904 13.123 1.18809L13.1628 1.17744C13.2395 1.15686 13.3311 1.13228 13.4135 1.11725ZM13.3642 2.5039C13.0648 2.58443 12.8606 2.64126 12.7036 2.69735C12.5325 2.75852 12.4742 2.80016 12.4467 2.82492C12.3421 2.91912 12.2699 3.04403 12.2407 3.18174C12.233 3.21793 12.2261 3.28928 12.2587 3.46805C12.2927 3.6545 12.3564 3.89436 12.4559 4.26563L12.5594 4.652C12.6589 5.02328 12.7236 5.26287 12.7874 5.44133C12.8486 5.61244 12.8902 5.67079 12.915 5.69829C13.0092 5.80291 13.1341 5.87503 13.2718 5.9043C13.308 5.91199 13.3793 5.91887 13.5581 5.88629C13.7221 5.85641 13.9273 5.80352 14.2269 5.72356L13.3642 2.5039Z" fill="#155EEF"/>
</svg>
)
const DiscoveryIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.74786 9.89676L12.0003 14.6669M7.25269 9.89676L4.00027 14.6669M9.3336 8.80031C9.3336 9.53669 8.73665 10.1336 8.00027 10.1336C7.26389 10.1336 6.66694 9.53669 6.66694 8.80031C6.66694 8.06393 7.26389 7.46698 8.00027 7.46698C8.73665 7.46698 9.3336 8.06393 9.3336 8.80031ZM11.4326 3.02182L3.57641 5.12689C3.39609 5.1752 3.30593 5.19936 3.24646 5.25291C3.19415 5.30001 3.15809 5.36247 3.14345 5.43132C3.12681 5.5096 3.15097 5.59976 3.19929 5.78008L3.78595 7.96951C3.83426 8.14984 3.85842 8.24 3.91197 8.29947C3.95907 8.35178 4.02153 8.38784 4.09038 8.40248C4.16866 8.41911 4.25882 8.39496 4.43914 8.34664L12.2953 6.24158L11.4326 3.02182ZM14.5285 6.33338C13.8072 6.52665 13.4466 6.62328 13.1335 6.55673C12.8581 6.49819 12.6082 6.35396 12.4198 6.14471C12.2056 5.90682 12.109 5.54618 11.9157 4.82489L11.8122 4.43852C11.6189 3.71722 11.5223 3.35658 11.5889 3.04347C11.6474 2.76805 11.7916 2.51823 12.0009 2.32982C12.2388 2.11563 12.5994 2.019 13.3207 1.82573C13.501 1.77741 13.5912 1.75325 13.6695 1.76989C13.7383 1.78452 13.8008 1.82058 13.8479 1.87289C13.9014 1.93237 13.9256 2.02253 13.9739 2.20285L14.9057 5.68018C14.954 5.86051 14.9781 5.95067 14.9615 6.02894C14.9469 6.0978 14.9108 6.16025 14.8585 6.20736C14.799 6.2609 14.7088 6.28506 14.5285 6.33338ZM2.33475 8.22033L3.23628 7.97876C3.4166 7.93044 3.50676 7.90628 3.56623 7.85274C3.61854 7.80563 3.6546 7.74318 3.66924 7.67433C3.68588 7.59605 3.66172 7.50589 3.6134 7.32556L3.37184 6.42403C3.32352 6.24371 3.29936 6.15355 3.24581 6.09408C3.19871 6.04176 3.13626 6.00571 3.0674 5.99107C2.98912 5.97443 2.89896 5.99859 2.71864 6.04691L1.81711 6.28847C1.63678 6.33679 1.54662 6.36095 1.48715 6.4145C1.43484 6.4616 1.39878 6.52405 1.38415 6.59291C1.36751 6.67119 1.39167 6.76135 1.43998 6.94167L1.68155 7.8432C1.72987 8.02352 1.75402 8.11369 1.80757 8.17316C1.85467 8.22547 1.91713 8.26153 1.98598 8.27616C2.06426 8.2928 2.15442 8.26864 2.33475 8.22033Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)
const SideBar: FC<{
controlUpdateInstalledApps: number
}> = ({
controlUpdateInstalledApps,
}) => {
const { t } = useTranslation()
const segments = useSelectedLayoutSegments()
const lastSegment = segments.slice(-1)[0]
const isDiscoverySelected = lastSegment === 'apps'
const { installedApps, setInstalledApps } = useContext(ExploreContext)
const fetchInstalledAppList = async () => {
const { installed_apps }: any = await doFetchInstalledAppList()
setInstalledApps(installed_apps)
}
const [showConfirm, setShowConfirm] = useState(false)
const [currId, setCurrId] = useState('')
const handleDelete = async () => {
const id = currId
await uninstallApp(id)
setShowConfirm(false)
Toast.notify({
type: 'success',
message: t('common.api.remove'),
})
fetchInstalledAppList()
}
const handleUpdatePinStatus = async (id: string, isPinned: boolean) => {
await updatePinStatus(id, isPinned)
Toast.notify({
type: 'success',
message: t('common.api.success'),
})
fetchInstalledAppList()
}
useEffect(() => {
fetchInstalledAppList()
}, [])
useEffect(() => {
fetchInstalledAppList()
}, [controlUpdateInstalledApps])
return (
<div className='w-[216px] shrink-0 pt-6 px-4 border-gray-200 cursor-pointer'>
<div>
<Link
href='/explore/apps'
className={cn(isDiscoverySelected ? 'text-primary-600 bg-white font-semibold' : 'text-gray-700 font-medium', 'flex items-center h-9 pl-3 space-x-2 rounded-lg')}
style={isDiscoverySelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}}
>
{isDiscoverySelected ? <SelectedDiscoveryIcon /> : <DiscoveryIcon />}
<div className='text-sm'>{t('explore.sidebar.discovery')}</div>
</Link>
</div>
{installedApps.length > 0 && (
<div className='mt-10'>
<div className='pl-2 text-xs text-gray-500 font-medium uppercase'>{t('explore.sidebar.workspace')}</div>
<div className='mt-3 space-y-1 overflow-y-auto overflow-x-hidden pb-20'
style={{
maxHeight: 'calc(100vh - 250px)',
}}
>
{installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon, icon_background } }) => {
return (
<Item
key={id}
name={name}
icon={icon}
icon_background={icon_background}
id={id}
isSelected={lastSegment?.toLowerCase() === id}
isPinned={is_pinned}
togglePin={() => handleUpdatePinStatus(id, !is_pinned)}
uninstallable={uninstallable}
onDelete={(id) => {
setCurrId(id)
setShowConfirm(true)
}}
/>
)
})}
</div>
</div>
)}
{showConfirm && (
<Confirm
title={t('explore.sidebar.delete.title')}
content={t('explore.sidebar.delete.content')}
isShow={showConfirm}
onClose={() => setShowConfirm(false)}
onConfirm={handleDelete}
onCancel={() => setShowConfirm(false)}
/>
)}
</div>
)
}
export default React.memo(SideBar)
......@@ -67,7 +67,7 @@ export default function AccountAbout({
<div className='flex items-center'>
<Link
className={classNames(buttonClassName, 'mr-2')}
href={'https://github.com/langgenius'}
href={'https://github.com/langgenius/dify/releases'}
target='_blank'
>
{t('common.about.changeLog')}
......
......@@ -5,7 +5,7 @@ import Link from 'next/link'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
import { useState, useEffect } from 'react'
import ProviderInput from '../provider-input'
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
import useValidateToken, { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
ValidatedSuccessIcon,
......@@ -15,7 +15,7 @@ import {
interface IAzureProviderProps {
provider: Provider
onValidatedStatus: (status?: ValidatedStatus) => void
onValidatedStatus: (status?: ValidatedStatusState) => void
onTokenChange: (token: ProviderAzureToken) => void
}
const AzureProvider = ({
......@@ -31,7 +31,7 @@ const AzureProvider = ({
token[type] = ''
setToken({...token})
onTokenChange({...token})
setValidatedStatus(undefined)
setValidatedStatus({})
}
}
const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => {
......@@ -41,7 +41,7 @@ const AzureProvider = ({
validate({...token}, {
beforeValidating: () => {
if (!token.openai_api_base || !token.openai_api_key) {
setValidatedStatus(undefined)
setValidatedStatus({})
return false
}
return true
......@@ -49,10 +49,10 @@ const AzureProvider = ({
})
}
const getValidatedIcon = () => {
if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) {
if (validatedStatus.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) {
return <ValidatedErrorIcon />
}
if (validatedStatus === ValidatedStatus.Success) {
if (validatedStatus.status === ValidatedStatus.Success) {
return <ValidatedSuccessIcon />
}
}
......@@ -60,8 +60,8 @@ const AzureProvider = ({
if (validating) {
return <ValidatingTip />
}
if (validatedStatus === ValidatedStatus.Error) {
return <ValidatedErrorOnAzureOpenaiTip />
if (validatedStatus.status === ValidatedStatus.Error) {
return <ValidatedErrorOnAzureOpenaiTip errorMessage={validatedStatus.message ?? ''} />
}
}
useEffect(() => {
......
......@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import ProviderInput from '../provider-input'
import Link from 'next/link'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
import useValidateToken, { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
ValidatedSuccessIcon,
......@@ -15,7 +15,7 @@ import {
interface IOpenaiProviderProps {
provider: Provider
onValidatedStatus: (status?: ValidatedStatus) => void
onValidatedStatus: (status?: ValidatedStatusState) => void
onTokenChange: (token: string) => void
}
......@@ -31,7 +31,7 @@ const OpenaiProvider = ({
if (token === provider.token) {
setToken('')
onTokenChange('')
setValidatedStatus(undefined)
setValidatedStatus({})
}
}
const handleChange = (v: string) => {
......@@ -40,7 +40,7 @@ const OpenaiProvider = ({
validate(v, {
beforeValidating: () => {
if (!v) {
setValidatedStatus(undefined)
setValidatedStatus({})
return false
}
return true
......@@ -54,10 +54,10 @@ const OpenaiProvider = ({
}, [validatedStatus])
const getValidatedIcon = () => {
if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) {
if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) {
return <ValidatedErrorIcon />
}
if (validatedStatus === ValidatedStatus.Success) {
if (validatedStatus.status === ValidatedStatus.Success) {
return <ValidatedSuccessIcon />
}
}
......@@ -65,11 +65,8 @@ const OpenaiProvider = ({
if (validating) {
return <ValidatingTip />
}
if (validatedStatus === ValidatedStatus.Exceed) {
return <ValidatedExceedOnOpenaiTip />
}
if (validatedStatus === ValidatedStatus.Error) {
return <ValidatedErrorOnOpenaiTip />
if (validatedStatus?.status === ValidatedStatus.Error) {
return <ValidatedErrorOnOpenaiTip errorMessage={validatedStatus.message ?? ''} />
}
}
......
......@@ -38,22 +38,22 @@ export const ValidatedExceedOnOpenaiTip = () => {
)
}
export const ValidatedErrorOnOpenaiTip = () => {
export const ValidatedErrorOnOpenaiTip = ({ errorMessage }: { errorMessage: string }) => {
const { t } = useTranslation()
return (
<div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
{t('common.provider.invalidKey')}
{t('common.provider.validatedError')}{errorMessage}
</div>
)
}
export const ValidatedErrorOnAzureOpenaiTip = () => {
export const ValidatedErrorOnAzureOpenaiTip = ({ errorMessage }: { errorMessage: string }) => {
const { t } = useTranslation()
return (
<div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
{t('common.provider.invalidApiKey')}
{t('common.provider.validatedError')}{errorMessage}
</div>
)
}
\ No newline at end of file
......@@ -8,11 +8,16 @@ export enum ValidatedStatus {
Error = 'error',
Exceed = 'exceed'
}
export type SetValidatedStatus = Dispatch<SetStateAction<ValidatedStatus | undefined>>
export type ValidatedStatusState = {
status?: ValidatedStatus,
message?: string
}
// export type ValidatedStatusState = ValidatedStatus | undefined | ValidatedError
export type SetValidatedStatus = Dispatch<SetStateAction<ValidatedStatusState>>
export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void>
type ValidateTokenReturn = [
boolean,
ValidatedStatus | undefined,
ValidatedStatusState,
SetValidatedStatus,
ValidateFn
]
......@@ -22,7 +27,7 @@ export type ValidateFnConfig = {
const useValidateToken = (providerName: string): ValidateTokenReturn => {
const [validating, setValidating] = useState(false)
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatus | undefined>()
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => {
if (!config.beforeValidating(token)) {
return false
......@@ -30,19 +35,12 @@ const useValidateToken = (providerName: string): ValidateTokenReturn => {
setValidating(true)
try {
const res = await validateProviderKey({ url: `/workspaces/current/providers/${providerName}/token-validate`, body: { token } })
setValidatedStatus(res.result === 'success' ? ValidatedStatus.Success : ValidatedStatus.Error)
setValidatedStatus(
res.result === 'success'
? { status: ValidatedStatus.Success }
: { status: ValidatedStatus.Error, message: res.error })
} catch (e: any) {
if (e.status === 400) {
e.json().then(({ code }: any) => {
if (code === 'provider_request_failed' && providerName === 'openai') {
setValidatedStatus(ValidatedStatus.Exceed)
} else {
setValidatedStatus(ValidatedStatus.Error)
}
})
} else {
setValidatedStatus(ValidatedStatus.Error)
}
setValidatedStatus({ status: ValidatedStatus.Error, message: e.message })
} finally {
setValidating(false)
}
......
......@@ -8,7 +8,7 @@ import type { Provider, ProviderAzureToken } from '@/models/common'
import { ProviderName } from '@/models/common'
import OpenaiProvider from '../openai-provider'
import AzureProvider from '../azure-provider'
import { ValidatedStatus } from '../provider-input/useValidateToken'
import { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken'
import { updateProviderAIKey } from '@/service/common'
import { ToastContext } from '@/app/components/base/toast'
......@@ -29,7 +29,7 @@ const ProviderItem = ({
onSave
}: IProviderItemProps) => {
const { t } = useTranslation()
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatus>()
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>()
const [loading, setLoading] = useState(false)
const { notify } = useContext(ToastContext)
const [token, setToken] = useState<ProviderAzureToken | string>(
......@@ -55,7 +55,7 @@ const ProviderItem = ({
}
const handleUpdateToken = async () => {
if (loading) return
if (validatedStatus === ValidatedStatus.Success) {
if (validatedStatus?.status === ValidatedStatus.Success) {
try {
setLoading(true)
await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } })
......
......@@ -15,6 +15,12 @@ import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog'
import { WorkspaceProvider } from '@/context/workspace-context'
import { useDatasetsContext } from '@/context/datasets-context'
const BuildAppsIcon = ({isSelected}: {isSelected: boolean}) => (
<svg className='mr-1' width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6666 4.85221L7.99998 8.00036M7.99998 8.00036L2.33331 4.85221M7.99998 8.00036L8 14.3337M14 10.7061V5.29468C14 5.06625 14 4.95204 13.9663 4.85017C13.9366 4.76005 13.8879 4.67733 13.8236 4.60754C13.7509 4.52865 13.651 4.47318 13.4514 4.36224L8.51802 1.6215C8.32895 1.51646 8.23442 1.46395 8.1343 1.44336C8.0457 1.42513 7.95431 1.42513 7.8657 1.44336C7.76559 1.46395 7.67105 1.51646 7.48198 1.6215L2.54865 4.36225C2.34896 4.47318 2.24912 4.52865 2.17642 4.60754C2.11211 4.67733 2.06343 4.76005 2.03366 4.85017C2 4.95204 2 5.06625 2 5.29468V10.7061C2 10.9345 2 11.0487 2.03366 11.1506C2.06343 11.2407 2.11211 11.3234 2.17642 11.3932C2.24912 11.4721 2.34897 11.5276 2.54865 11.6385L7.48198 14.3793C7.67105 14.4843 7.76559 14.5368 7.8657 14.5574C7.95431 14.5756 8.0457 14.5756 8.1343 14.5574C8.23442 14.5368 8.32895 14.4843 8.51802 14.3793L13.4514 11.6385C13.651 11.5276 13.7509 11.4721 13.8236 11.3932C13.8879 11.3234 13.9366 11.2407 13.9663 11.1506C14 11.0487 14 10.9345 14 10.7061Z" stroke={isSelected ? '#155EEF': '#667085'} strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)
export type IHeaderProps = {
appItems: AppDetailResponse[]
curApp: AppDetailResponse
......@@ -38,8 +44,9 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
const { datasets, currentDataset } = useDatasetsContext()
const router = useRouter()
const showEnvTag = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT'
const isPluginsComingSoon = useSelectedLayoutSegment() === 'plugins-coming-soon'
const selectedSegment = useSelectedLayoutSegment()
const isPluginsComingSoon = selectedSegment === 'plugins-coming-soon'
const isExplore = selectedSegment === 'explore'
return (
<div className={classNames(
'sticky top-0 left-0 right-0 z-20 flex bg-gray-100 grow-0 shrink-0 basis-auto h-14',
......@@ -64,8 +71,16 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
</div>
</div>
<div className='flex items-center'>
<Link href="/explore/apps" className={classNames(
navClassName, 'group',
isExplore && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)]',
isExplore ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700'
)}>
<Squares2X2Icon className='mr-1 w-[18px] h-[18px]' />
{t('common.menus.explore')}
</Link>
<Nav
icon={<Squares2X2Icon className='mr-1 w-[18px] h-[18px]' />}
icon={<BuildAppsIcon isSelected={['apps', 'app'].includes(selectedSegment || '')} />}
text={t('common.menus.apps')}
activeSegment={['apps', 'app']}
link='/apps'
......
......@@ -37,7 +37,7 @@ const Nav = ({
<Link href={link}>
<div
className={classNames(`
flex items-center h-8 pl-2.5 pr-2
flex items-center h-7 pl-2.5 pr-2
font-semibold cursor-pointer rounded-[10px]
${isActived ? 'text-[#1C64F2]' : 'text-gray-500 hover:bg-gray-200'}
${curNav && isActived && 'hover:bg-[#EBF5FF]'}
......
/* eslint-disable @typescript-eslint/no-use-before-define */
'use client'
import type { FC } from 'react'
import React, { useEffect, useState, useRef } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import { useBoolean, useGetState } from 'ahooks'
import AppUnavailable from '../../base/app-unavailable'
import useConversation from './hooks/use-conversation'
import s from './style.module.css'
import { ToastContext } from '@/app/components/base/toast'
import Sidebar from '@/app/components/share/chat/sidebar'
import ConfigSence from '@/app/components/share/chat/config-scence'
import Header from '@/app/components/share/header'
import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, sendChatMessage, updateFeedback, fetchSuggestedQuestions } from '@/service/share'
import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, sendChatMessage, updateFeedback } from '@/service/share'
import type { ConversationItem, SiteInfo } from '@/models/share'
import type { PromptConfig } from '@/models/debug'
import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
import type { Feedbacktype, IChatItem } from '@/app/components/app/chat'
import Chat from '@/app/components/app/chat'
import { changeLanguage } from '@/i18n/i18next-config'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Loading from '@/app/components/base/loading'
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
import AppUnavailable from '../../base/app-unavailable'
import { userInputsFormToPromptVariables } from '@/utils/model-config'
import { SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
import type { InstalledApp } from '@/models/explore'
export type IMainProps = {
params: {
locale: string
appId: string
conversationId: string
token: string
}
isInstalledApp?: boolean
installedAppInfo?: InstalledApp
}
const Main: FC<IMainProps> = () => {
const Main: FC<IMainProps> = ({
isInstalledApp = false,
installedAppInfo,
}) => {
const { t } = useTranslation()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
......@@ -58,7 +60,6 @@ const Main: FC<IMainProps> = () => {
else
document.title = `${siteInfo.title} - Powered by Dify`
}
}, [siteInfo?.title, plan])
/*
......@@ -78,8 +79,13 @@ const Main: FC<IMainProps> = () => {
resetNewConversationInputs,
setCurrInputs,
setNewConversationInfo,
setExistConversationInfo
setExistConversationInfo,
} = useConversation()
const [hasMore, setHasMore] = useState<boolean>(false)
const onMoreLoaded = ({ data: conversations, has_more }: any) => {
setHasMore(has_more)
setConversationList([...conversationList, ...conversations])
}
const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
......@@ -93,9 +99,9 @@ const Main: FC<IMainProps> = () => {
setChatList(generateNewChatListWithOpenstatement('', inputs))
}
const hasSetInputs = (() => {
if (!isNewConversation) {
if (!isNewConversation)
return true
}
return isChatStarted
})()
......@@ -103,7 +109,8 @@ const Main: FC<IMainProps> = () => {
const conversationIntroduction = currConversationInfo?.introduction || ''
const handleConversationSwitch = () => {
if (!inited) return
if (!inited)
return
if (!appId) {
// wait for appId
setTimeout(handleConversationSwitch, 100)
......@@ -122,14 +129,15 @@ const Main: FC<IMainProps> = () => {
name: item?.name || '',
introduction: notSyncToStateIntroduction,
})
} else {
}
else {
notSyncToStateInputs = newConversationInputs
setCurrInputs(notSyncToStateInputs)
}
// update chat list of current conversation
if (!isNewConversation && !conversationIdChangeBecauseOfNew && !isResponsing) {
fetchChatList(currConversationId).then((res: any) => {
fetchChatList(currConversationId, isInstalledApp, installedAppInfo?.id).then((res: any) => {
const { data } = res
const newChatList: IChatItem[] = generateNewChatListWithOpenstatement(notSyncToStateIntroduction, notSyncToStateInputs)
......@@ -150,9 +158,8 @@ const Main: FC<IMainProps> = () => {
})
}
if (isNewConversation && isChatStarted) {
if (isNewConversation && isChatStarted)
setChatList(generateNewChatListWithOpenstatement())
}
setControlFocus(Date.now())
}
......@@ -162,7 +169,8 @@ const Main: FC<IMainProps> = () => {
if (id === '-1') {
createNewChat()
setConversationIdChangeBecauseOfNew(true)
} else {
}
else {
setConversationIdChangeBecauseOfNew(false)
}
// trigger handleConversationSwitch
......@@ -178,9 +186,8 @@ const Main: FC<IMainProps> = () => {
const chatListDomRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// scroll to bottom
if (chatListDomRef.current) {
if (chatListDomRef.current)
chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
}
}, [chatList, currConversationId])
// user can not edit inputs if user had send message
const canEditInpus = !chatList.some(item => item.isAnswer === false) && isNewConversation
......@@ -188,15 +195,15 @@ const Main: FC<IMainProps> = () => {
// if new chat is already exist, do not create new chat
abortController?.abort()
setResponsingFalse()
if (conversationList.some(item => item.id === '-1')) {
if (conversationList.some(item => item.id === '-1'))
return
}
setConversationList(produce(conversationList, draft => {
setConversationList(produce(conversationList, (draft) => {
draft.unshift({
id: '-1',
name: t('share.chat.newChatDefaultName'),
inputs: newConversationInputs,
introduction: conversationIntroduction
introduction: conversationIntroduction,
})
}))
}
......@@ -205,45 +212,59 @@ const Main: FC<IMainProps> = () => {
const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => {
let caculatedIntroduction = introduction || conversationIntroduction || ''
const caculatedPromptVariables = inputs || currInputs || null
if (caculatedIntroduction && caculatedPromptVariables) {
if (caculatedIntroduction && caculatedPromptVariables)
caculatedIntroduction = replaceStringWithValues(caculatedIntroduction, promptConfig?.prompt_variables || [], caculatedPromptVariables)
}
// console.log(isPublicVersion)
const openstatement = {
id: `${Date.now()}`,
content: caculatedIntroduction,
isAnswer: true,
feedbackDisabled: true,
isOpeningStatement: isPublicVersion
isOpeningStatement: isPublicVersion,
}
if (caculatedIntroduction) {
if (caculatedIntroduction)
return [openstatement]
}
return []
}
const fetchInitData = () => {
return Promise.all([isInstalledApp
? {
app_id: installedAppInfo?.id,
site: {
title: installedAppInfo?.app.name,
prompt_public: false,
copyright: '',
},
plan: 'basic',
}
: fetchAppInfo(), fetchConversations(isInstalledApp, installedAppInfo?.id), fetchAppParams(isInstalledApp, installedAppInfo?.id)])
}
// init
useEffect(() => {
(async () => {
try {
const [appData, conversationData, appParams] = await Promise.all([fetchAppInfo(), fetchConversations(), fetchAppParams()])
const { app_id: appId, site: siteInfo, model_config, plan }: any = appData
const [appData, conversationData, appParams]: any = await fetchInitData()
const { app_id: appId, site: siteInfo, plan }: any = appData
setAppId(appId)
setPlan(plan)
const tempIsPublicVersion = siteInfo.prompt_public
setIsPublicVersion(tempIsPublicVersion)
const prompt_template = tempIsPublicVersion ? model_config.pre_prompt : ''
const prompt_template = ''
// handle current conversation id
const { data: conversations } = conversationData as { data: ConversationItem[] }
const { data: conversations, has_more } = conversationData as { data: ConversationItem[]; has_more: boolean }
const _conversationId = getConversationIdFromStorage(appId)
const isNotNewConversation = conversations.some(item => item.id === _conversationId)
setHasMore(has_more)
// fetch new conversation info
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer }: any = appParams
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
if (siteInfo.default_language)
changeLanguage(siteInfo.default_language)
setNewConversationInfo({
name: t('share.chat.newChatDefaultName'),
introduction,
......@@ -251,20 +272,22 @@ const Main: FC<IMainProps> = () => {
setSiteInfo(siteInfo as SiteInfo)
setPromptConfig({
prompt_template,
prompt_variables: prompt_variables,
prompt_variables,
} as PromptConfig)
setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer)
setConversationList(conversations as ConversationItem[])
if (isNotNewConversation) {
if (isNotNewConversation)
setCurrConversationId(_conversationId, appId, false)
}
setInited(true)
} catch (e: any) {
}
catch (e: any) {
if (e.status === 404) {
setAppUnavailable(true)
} else {
}
else {
setIsUnknwonReason(true)
setAppUnavailable(true)
}
......@@ -282,21 +305,20 @@ const Main: FC<IMainProps> = () => {
const checkCanSend = () => {
const prompt_variables = promptConfig?.prompt_variables
const inputs = currInputs
if (!inputs || !prompt_variables || prompt_variables?.length === 0) {
if (!inputs || !prompt_variables || prompt_variables?.length === 0)
return true
}
let hasEmptyInput = false
const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res
}) || [] // compatible with old version
requiredVars.forEach(({ key }) => {
if (hasEmptyInput) {
if (hasEmptyInput)
return
}
if (!inputs?.[key]) {
if (!inputs?.[key])
hasEmptyInput = true
}
})
if (hasEmptyInput) {
......@@ -357,9 +379,8 @@ const Main: FC<IMainProps> = () => {
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId }: any) => {
responseItem.content = responseItem.content + message
responseItem.id = messageId
if (isFirstMessage && newConversationId) {
if (isFirstMessage && newConversationId)
tempNewConversationId = newConversationId
}
// closesure new list is outdated.
const newListWithAnswer = produce(
......@@ -374,12 +395,13 @@ const Main: FC<IMainProps> = () => {
},
async onCompleted(hasError?: boolean) {
setResponsingFalse()
if (hasError) {
if (hasError)
return
}
let currChatList = conversationList
if (getConversationIdChangeBecauseOfNew()) {
const { data: conversations }: any = await fetchConversations()
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppInfo?.id)
setHasMore(has_more)
setConversationList(conversations as ConversationItem[])
currChatList = conversations
}
......@@ -388,7 +410,7 @@ const Main: FC<IMainProps> = () => {
setChatNotStarted()
setCurrConversationId(tempNewConversationId, appId, true)
if (suggestedQuestionsAfterAnswerConfig?.enabled) {
const { data }: any = await fetchSuggestedQuestions(responseItem.id)
const { data }: any = await fetchSuggestedQuestions(responseItem.id, isInstalledApp, installedAppInfo?.id)
setSuggestQuestions(data)
setIsShowSuggestion(true)
}
......@@ -396,15 +418,15 @@ const Main: FC<IMainProps> = () => {
onError() {
setResponsingFalse()
// role back placeholder answer
setChatList(produce(getChatList(), draft => {
setChatList(produce(getChatList(), (draft) => {
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
}))
},
})
}, isInstalledApp, installedAppInfo?.id)
}
const handleFeedback = async (messageId: string, feedback: Feedbacktype) => {
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } })
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id)
const newChatList = chatList.map((item) => {
if (item.id === messageId) {
return {
......@@ -424,9 +446,14 @@ const Main: FC<IMainProps> = () => {
return (
<Sidebar
list={conversationList}
onMoreLoaded={onMoreLoaded}
isNoMore={!hasMore}
onCurrentIdChange={handleConversationIdChange}
currentId={currConversationId}
copyRight={siteInfo.copyright || siteInfo.title}
isInstalledApp={isInstalledApp}
installedAppId={installedAppInfo?.id}
siteInfo={siteInfo}
/>
)
}
......@@ -439,18 +466,31 @@ const Main: FC<IMainProps> = () => {
return (
<div className='bg-gray-100'>
{!isInstalledApp && (
<Header
title={siteInfo.title}
icon={siteInfo.icon || ''}
icon_background={siteInfo.icon_background || '#FFEAD5'}
icon_background={siteInfo.icon_background}
isMobile={isMobile}
onShowSideBar={showSidebar}
onCreateNewChat={() => handleConversationIdChange('-1')}
/>
)}
{/* {isNewConversation ? 'new' : 'exist'}
{JSON.stringify(newConversationInputs ? newConversationInputs : {})}
{JSON.stringify(existConversationInputs ? existConversationInputs : {})} */}
<div className="flex rounded-t-2xl bg-white overflow-hidden">
<div
className={cn(
'flex rounded-t-2xl bg-white overflow-hidden',
isInstalledApp && 'rounded-b-2xl',
)}
style={isInstalledApp
? {
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
}
: {}}
>
{/* sidebar */}
{!isMobile && renderSidebar()}
{isMobile && isShowSidebar && (
......@@ -464,7 +504,11 @@ const Main: FC<IMainProps> = () => {
</div>
)}
{/* main */}
<div className='flex-grow flex flex-col h-[calc(100vh_-_3rem)] overflow-y-auto'>
<div className={cn(
isInstalledApp ? s.installedApp : 'h-[calc(100vh_-_3rem)]',
'flex-grow flex flex-col overflow-y-auto',
)
}>
<ConfigSence
conversationName={conversationName}
hasSetInputs={hasSetInputs}
......@@ -480,7 +524,7 @@ const Main: FC<IMainProps> = () => {
{
hasSetInputs && (
<div className={cn(doShowSuggestion ? 'pb-[140px]' : 'pb-[66px]', 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}>
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[66px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}>
<div className='h-full overflow-y-auto' ref={chatListDomRef}>
<Chat
chatList={chatList}
......
'use client'
import React, { FC } from 'react'
import cn from 'classnames'
import { appDefaultIconBackground } from '@/config/index'
import AppIcon from '@/app/components/base/app-icon'
export interface IAppInfoProps {
className?: string
icon: string
icon_background?: string
name: string
}
const AppInfo: FC<IAppInfoProps> = ({
className,
icon,
icon_background,
name
}) => {
return (
<div className={cn(className, 'flex items-center space-x-3')}>
<AppIcon size="small" icon={icon} background={icon_background || appDefaultIconBackground} />
<div className='w-0 grow text-sm font-semibold text-gray-800 overflow-hidden text-ellipsis whitespace-nowrap'>{name}</div>
</div>
)
}
export default React.memo(AppInfo)
import React from 'react'
import React, { useEffect, useRef } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import {
......@@ -7,33 +7,77 @@ import {
} from '@heroicons/react/24/outline'
import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon, } from '@heroicons/react/24/solid'
import Button from '../../../base/button'
import AppInfo from '@/app/components/share/chat/sidebar/app-info'
// import Card from './card'
import type { ConversationItem } from '@/models/share'
import type { ConversationItem, SiteInfo } from '@/models/share'
import { useInfiniteScroll } from 'ahooks'
import { fetchConversations } from '@/service/share'
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ')
}
const MAX_CONVERSATION_LENTH = 20
export type ISidebarProps = {
copyRight: string
currentId: string
onCurrentIdChange: (id: string) => void
list: ConversationItem[]
isInstalledApp: boolean
installedAppId?: string
siteInfo: SiteInfo
onMoreLoaded: (res: {data: ConversationItem[], has_more: boolean}) => void
isNoMore: boolean
}
const Sidebar: FC<ISidebarProps> = ({
copyRight,
currentId,
onCurrentIdChange,
list }) => {
list,
isInstalledApp,
installedAppId,
siteInfo,
onMoreLoaded,
isNoMore,
}) => {
const { t } = useTranslation()
const listRef = useRef<HTMLDivElement>(null)
useInfiniteScroll(
async () => {
if(!isNoMore) {
const lastId = list[list.length - 1].id
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId)
onMoreLoaded({ data: conversations, has_more })
}
return {list: []}
},
{
target: listRef,
isNoMore: () => {
return isNoMore
},
reloadDeps: [isNoMore]
},
)
return (
<div
className="shrink-0 flex flex-col overflow-y-auto bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 tablet:h-[calc(100vh_-_3rem)] mobile:h-screen"
className={
classNames(
isInstalledApp ? 'tablet:h-[calc(100vh_-_74px)]' : 'tablet:h-[calc(100vh_-_3rem)]',
"shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 mobile:h-screen"
)
}
>
{list.length < MAX_CONVERSATION_LENTH && (
{isInstalledApp && (
<AppInfo
className='my-4 px-4'
name={siteInfo.title || ''}
icon={siteInfo.icon || ''}
icon_background={siteInfo.icon_background}
/>
)}
<div className="flex flex-shrink-0 p-4 !pb-0">
<Button
onClick={() => { onCurrentIdChange('-1') }}
......@@ -41,9 +85,11 @@ const Sidebar: FC<ISidebarProps> = ({
<PencilSquareIcon className="mr-2 h-4 w-4" /> {t('share.chat.newChat')}
</Button>
</div>
)}
<nav className="mt-4 flex-1 space-y-1 bg-white p-4 !pt-0">
<nav
ref={listRef}
className="mt-4 flex-1 space-y-1 bg-white p-4 !pt-0 overflow-y-auto"
>
{list.map((item) => {
const isCurrent = item.id === currentId
const ItemIcon
......
.installedApp {
height: calc(100vh - 74px);
}
\ No newline at end of file
......@@ -60,8 +60,9 @@ const ConfigSence: FC<IConfigSenceProps> = ({
</div>
</div>
))}
{promptConfig.prompt_variables.length > 0 && (
<div className='mt-6 h-[1px] bg-gray-100'></div>
)}
<div className='w-full mt-5'>
<label className='text-gray-900 text-sm font-medium'>{t('share.generation.queryTitle')}</label>
<div className="mt-2 overflow-hidden rounded-lg bg-gray-50 ">
......
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g filter="url(#filter0_d_3468_34550)">
<rect x="2" y="1" width="32" height="32" rx="8" fill="#FFE4E8"/>
<path d="M9 26.26H27V8.26H9V26.26Z" fill="url(#pattern0)"/>
<rect x="2.25" y="1.25" width="31.5" height="31.5" rx="7.75" stroke="black" stroke-opacity="0.05" stroke-width="0.5"/>
</g>
<defs>
<filter id="filter0_d_3468_34550" x="0" y="0" width="36" height="36" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3468_34550"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3468_34550" result="shape"/>
</filter>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_3468_34550" transform="scale(0.00625)"/>
</pattern>
<image id="image0_3468_34550" width="160" height="160" xlink:href="data:image/png;base64,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"/>
</defs>
</svg>
'use client'
import React, { useEffect, useState, useRef } from 'react'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import cn from 'classnames'
import { useBoolean, useClickAway } from 'ahooks'
import { useContext } from 'use-context-selector'
import { XMarkIcon } from '@heroicons/react/24/outline'
import TabHeader from '../../base/tab-header'
import Button from '../../base/button'
import s from './style.module.css'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import ConfigScence from '@/app/components/share/text-generation/config-scence'
import NoData from '@/app/components/share/text-generation/no-data'
// import History from '@/app/components/share/text-generation/history'
import { fetchAppInfo, fetchAppParams, sendCompletionMessage, updateFeedback, saveMessage, fetchSavedMessage as doFetchSavedMessage, removeMessage } from '@/service/share'
import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage, sendCompletionMessage, updateFeedback } from '@/service/share'
import type { SiteInfo } from '@/models/share'
import type { PromptConfig, MoreLikeThisConfig, SavedMessage } from '@/models/debug'
import type { MoreLikeThisConfig, PromptConfig, SavedMessage } from '@/models/debug'
import Toast from '@/app/components/base/toast'
import { Feedbacktype } from '@/app/components/app/chat'
import AppIcon from '@/app/components/base/app-icon'
import type { Feedbacktype } from '@/app/components/app/chat'
import { changeLanguage } from '@/i18n/i18next-config'
import Loading from '@/app/components/base/loading'
import { userInputsFormToPromptVariables } from '@/utils/model-config'
import TextGenerationRes from '@/app/components/app/text-generate/item'
import SavedItems from '@/app/components/app/text-generate/saved-items'
import TabHeader from '../../base/tab-header'
import { XMarkIcon } from '@heroicons/react/24/outline'
import s from './style.module.css'
import Button from '../../base/button'
import type { InstalledApp } from '@/models/explore'
import { appDefaultIconBackground } from '@/config'
export type IMainProps = {
isInstalledApp?: boolean
installedAppInfo?: InstalledApp
}
const TextGeneration = () => {
const TextGeneration: FC<IMainProps> = ({
isInstalledApp = false,
installedAppInfo,
}) => {
const { t } = useTranslation()
const media = useBreakpoints()
const isPC = media === MediaType.pc
......@@ -45,18 +56,18 @@ const TextGeneration = () => {
const [messageId, setMessageId] = useState<string | null>(null)
const [feedback, setFeedback] = useState<Feedbacktype>({
rating: null
rating: null,
})
const handleFeedback = async (feedback: Feedbacktype) => {
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } })
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id)
setFeedback(feedback)
}
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
const fetchSavedMessage = async () => {
const res: any = await doFetchSavedMessage()
const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id)
setSavedMessages(res.data)
}
......@@ -65,13 +76,13 @@ const TextGeneration = () => {
}, [])
const handleSaveMessage = async (messageId: string) => {
await saveMessage(messageId)
await saveMessage(messageId, isInstalledApp, installedAppInfo?.id)
notify({ type: 'success', message: t('common.api.saved') })
fetchSavedMessage()
}
const handleRemoveSavedMessage = async (messageId: string) => {
await removeMessage(messageId)
await removeMessage(messageId, isInstalledApp, installedAppInfo?.id)
notify({ type: 'success', message: t('common.api.remove') })
fetchSavedMessage()
}
......@@ -82,21 +93,20 @@ const TextGeneration = () => {
const checkCanSend = () => {
const prompt_variables = promptConfig?.prompt_variables
if (!prompt_variables || prompt_variables?.length === 0) {
if (!prompt_variables || prompt_variables?.length === 0)
return true
}
let hasEmptyInput = false
const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res
}) || [] // compatible with old version
requiredVars.forEach(({ key }) => {
if (hasEmptyInput) {
if (hasEmptyInput)
return
}
if (!inputs[key]) {
if (!inputs[key])
hasEmptyInput = true
}
})
if (hasEmptyInput) {
......@@ -127,16 +137,17 @@ const TextGeneration = () => {
setMessageId(null)
setFeedback({
rating: null
rating: null,
})
setCompletionRes('')
const res: string[] = []
let tempMessageId = ''
if (!isPC) {
if (!isPC)
// eslint-disable-next-line @typescript-eslint/no-use-before-define
showResSidebar()
}
setResponsingTrue()
sendCompletionMessage(data, {
onData: (data: string, _isFirstMessage: boolean, { messageId }: any) => {
......@@ -150,13 +161,27 @@ const TextGeneration = () => {
},
onError() {
setResponsingFalse()
},
}, isInstalledApp, installedAppInfo?.id)
}
})
const fetchInitData = () => {
return Promise.all([isInstalledApp
? {
app_id: installedAppInfo?.id,
site: {
title: installedAppInfo?.app.name,
prompt_public: false,
copyright: '',
},
plan: 'basic',
}
: fetchAppInfo(), fetchAppParams(isInstalledApp, installedAppInfo?.id)])
}
useEffect(() => {
(async () => {
const [appData, appParams]: any = await Promise.all([fetchAppInfo(), fetchAppParams()])
const [appData, appParams]: any = await fetchInitData()
const { app_id: appId, site: siteInfo } = appData
setAppId(appId)
setSiteInfo(siteInfo as SiteInfo)
......@@ -181,16 +206,18 @@ const TextGeneration = () => {
const [isShowResSidebar, { setTrue: showResSidebar, setFalse: hideResSidebar }] = useBoolean(false)
const resRef = useRef<HTMLDivElement>(null)
useClickAway(() => {
hideResSidebar();
hideResSidebar()
}, resRef)
const renderRes = (
<div
ref={resRef}
className={
cn("flex flex-col h-full shrink-0",
cn(
'flex flex-col h-full shrink-0',
isPC ? 'px-10 py-8' : 'bg-gray-50',
isTablet && 'p-6', isMoble && 'p-4')}
isTablet && 'p-6', isMoble && 'p-4')
}
>
<>
<div className='shrink-0 flex items-center justify-between'>
......@@ -208,11 +235,13 @@ const TextGeneration = () => {
)}
</div>
<div className='grow'>
{(isResponsing && !completionRes) ? (
<div className='grow overflow-y-auto'>
{(isResponsing && !completionRes)
? (
<div className='flex h-full w-full justify-center items-center'>
<Loading type='area' />
</div>) : (
</div>)
: (
<>
{isNoData
? <NoData />
......@@ -227,6 +256,8 @@ const TextGeneration = () => {
feedback={feedback}
onSave={handleSaveMessage}
isMobile={isMoble}
isInstalledApp={isInstalledApp}
installedAppId={installedAppInfo?.id}
/>
)
}
......@@ -240,16 +271,23 @@ const TextGeneration = () => {
if (!appId || !siteInfo || !promptConfig)
return <Loading type='app' />
return (
<>
<div className={cn(isPC && 'flex', 'h-screen bg-gray-50')}>
<div className={cn(
isPC && 'flex',
isInstalledApp ? s.installedApp : 'h-screen',
'bg-gray-50',
)}>
{/* Left */}
<div className={cn(isPC ? 'w-[600px] max-w-[50%] p-8' : 'p-4', "shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white")}>
<div className={cn(
isPC ? 'w-[600px] max-w-[50%] p-8' : 'p-4',
isInstalledApp && 'rounded-l-2xl',
'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white',
)}>
<div className='mb-6'>
<div className='flex justify-between items-center'>
<div className='flex items-center space-x-3'>
<div className={cn(s.appIcon, 'shrink-0')}></div>
<AppIcon size="small" icon={siteInfo.icon} background={siteInfo.icon_background || appDefaultIconBackground} />
<div className='text-lg text-gray-800 font-semibold'>{siteInfo.title}</div>
</div>
{!isPC && (
......@@ -272,12 +310,16 @@ const TextGeneration = () => {
items={[
{ id: 'create', name: t('share.generation.tabs.create') },
{
id: 'saved', name: t('share.generation.tabs.saved'), extra: savedMessages.length > 0 ? (
id: 'saved',
name: t('share.generation.tabs.saved'),
extra: savedMessages.length > 0
? (
<div className='ml-1 flext items-center h-5 px-1.5 rounded-md border border-gray-200 text-gray-500 text-xs font-medium'>
{savedMessages.length}
</div>
) : null
}
)
: null,
},
]}
value={currTab}
onChange={setCurrTab}
......@@ -305,9 +347,11 @@ const TextGeneration = () => {
)}
</div>
{/* copyright */}
<div className='fixed left-8 bottom-4 flex space-x-2 text-gray-400 font-normal text-xs'>
<div className={cn(
isInstalledApp ? 'left-[248px]' : 'left-8',
'fixed bottom-4 flex space-x-2 text-gray-400 font-normal text-xs',
)}>
<div className="">© {siteInfo.copyright || siteInfo.title} {(new Date()).getFullYear()}</div>
{siteInfo.privacy_policy && (
<>
......@@ -335,7 +379,7 @@ const TextGeneration = () => {
<div
className={cn('fixed z-50 inset-0', isTablet ? 'pl-[128px]' : 'pl-6')}
style={{
background: 'rgba(35, 56, 118, 0.2)'
background: 'rgba(35, 56, 118, 0.2)',
}}
>
{renderRes}
......
.appIcon {
width: 32px;
height: 32px;
background: url(./icons/app-icon.svg) center center no-repeat;
background-size: contain;
.installedApp {
height: calc(100vh - 74px);
border-radius: 16px;
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
}
.starIcon {
......
......@@ -779,7 +779,7 @@
max-width: 100%;
padding: 0;
margin: 0;
overflow-x: scroll;
overflow-x: auto;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
......
......@@ -97,5 +97,9 @@ export const VAR_ITEM_TEMPLATE = {
required: true
}
export const appDefaultIconBackground = '#D5F5F6'
export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList'
import { createContext } from 'use-context-selector'
import { InstalledApp } from '@/models/explore'
type IExplore = {
controlUpdateInstalledApps: number
setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void
hasEditPermission: boolean
installedApps: InstalledApp[]
setInstalledApps: (installedApps: InstalledApp[]) => void
}
const ExploreContext = createContext<IExplore>({
controlUpdateInstalledApps: 0,
setControlUpdateInstalledApps: () => { },
hasEditPermission: false,
installedApps: [],
setInstalledApps: () => { },
})
export default ExploreContext
......@@ -31,6 +31,8 @@ import datasetSettingsEn from './lang/dataset-settings.en'
import datasetSettingsZh from './lang/dataset-settings.zh'
import datasetCreationEn from './lang/dataset-creation.en'
import datasetCreationZh from './lang/dataset-creation.zh'
import exploreEn from './lang/explore.en'
import exploreZh from './lang/explore.zh'
import { getLocaleOnClient } from '@/i18n/client'
const resources = {
......@@ -53,6 +55,7 @@ const resources = {
datasetHitTesting: datasetHitTestingEn,
datasetSettings: datasetSettingsEn,
datasetCreation: datasetCreationEn,
explore: exploreEn,
},
},
'zh-Hans': {
......@@ -74,6 +77,7 @@ const resources = {
datasetHitTesting: datasetHitTestingZh,
datasetSettings: datasetSettingsZh,
datasetCreation: datasetCreationZh,
explore: exploreZh,
},
},
}
......
......@@ -71,6 +71,7 @@ const translation = {
},
analysis: {
title: 'Analysis',
ms: 'ms',
totalMessages: {
title: 'Total Messages',
explanation: 'Daily AI interactions count; prompt engineering/debugging excluded.',
......
......@@ -71,6 +71,7 @@ const translation = {
},
analysis: {
title: '分析',
ms: '毫秒',
totalMessages: {
title: '全部消息数',
explanation: '反映 AI 每天的互动总次数,每回答用户一个问题算一条 Message。提示词编排和调试的消息不计入。',
......
const translation = {
title: 'Apps',
createApp: 'Create new App',
modes: {
completion: 'Text Generator',
......
const translation = {
title: '应用',
createApp: '创建应用',
modes: {
completion: '文本生成型',
......
......@@ -6,6 +6,7 @@ const translation = {
remove: 'Removed',
},
operation: {
create: 'Create',
confirm: 'Confirm',
cancel: 'Cancel',
clear: 'Clear',
......@@ -61,7 +62,8 @@ const translation = {
},
menus: {
status: 'beta',
apps: 'Apps',
explore: 'Explore',
apps: 'Build Apps',
plugins: 'Plugins',
pluginsTips: 'Integrate third-party plugins or create ChatGPT-compatible AI-Plugins.',
datasets: 'Datasets',
......@@ -80,11 +82,11 @@ const translation = {
settings: {
accountGroup: 'ACCOUNT',
workplaceGroup: 'WORKPLACE',
account: "My account",
members: "Members",
integrations: "Integrations",
language: "Language",
provider: "Model Provider"
account: 'My account',
members: 'Members',
integrations: 'Integrations',
language: 'Language',
provider: 'Model Provider',
},
account: {
avatar: 'Avatar',
......@@ -97,7 +99,7 @@ const translation = {
},
members: {
team: 'Team',
invite: 'Invite',
invite: 'Add',
name: 'NAME',
lastActive: 'LAST ACTIVE',
role: 'ROLES',
......@@ -107,14 +109,14 @@ const translation = {
adminTip: 'Can build apps & manage team settings',
normal: 'Normal',
normalTip: 'Only can use apps,can not build apps',
inviteTeamMember: 'Invite team member',
inviteTeamMemberTip: 'The other person will receive an email. If he\'s already a Dify user, he can access your team data directly after signing in.',
inviteTeamMember: 'Add team member',
inviteTeamMemberTip: 'He can access your team data directly after signing in.',
email: 'Email',
emailInvalid: 'Invalid Email Format',
emailPlaceholder: 'Input Email',
sendInvite: 'Send Invite',
invitationSent: 'Invitation sent',
invitationSentTip: 'The invitation is sent, and they can sign in to Dify to access your team data.',
sendInvite: 'Add',
invitationSent: 'Added',
invitationSentTip: 'Added, and they can sign in to Dify to access your team data.',
ok: 'OK',
removeFromTeam: 'Remove from team',
removeFromTeamTip: 'Will remove team access',
......@@ -130,19 +132,20 @@ const translation = {
googleAccount: 'Login with Google account',
github: 'GitHub',
githubAccount: 'Login with GitHub account',
connect: 'Connect'
connect: 'Connect',
},
language: {
displayLanguage: 'Display Language',
timezone: 'Time Zone',
},
provider: {
apiKey: "API Key",
enterYourKey: "Enter your API key here",
invalidKey: "Invalid OpenAI API key",
validating: "Validating key...",
saveFailed: "Save api key failed",
apiKeyExceedBill: "This API KEY has no quota available, please read",
apiKey: 'API Key',
enterYourKey: 'Enter your API key here',
invalidKey: 'Invalid OpenAI API key',
validatedError: 'Validation failed: ',
validating: 'Validating key...',
saveFailed: 'Save api key failed',
apiKeyExceedBill: 'This API KEY has no quota available, please read',
addKey: 'Add Key',
comingSoon: 'Coming Soon',
editKey: 'Edit',
......@@ -167,7 +170,7 @@ const translation = {
encrypted: {
front: 'Your API KEY will be encrypted and stored using',
back: ' technology.',
}
},
},
about: {
changeLog: 'Changlog',
......
......@@ -6,6 +6,7 @@ const translation = {
remove: '已移除',
},
operation: {
create: '创建',
confirm: '确认',
cancel: '取消',
clear: '清空',
......@@ -49,7 +50,7 @@ const translation = {
'Frequency penalty 是根据重复词在目前文本中的出现频率来对其进行惩罚。正值将不太可能重复常用单词和短语。',
maxToken: '最大 Token',
maxTokenTip:
'生成的最大令牌数为 2,048 或 4,000,取决于模型。提示和完成共享令牌数限制。一个令牌约等于 1 个英文或 4 个中文字符。',
'生成的最大令牌数为 2,048 或 4,000,取决于模型。提示和完成共享令牌数限制。一个令牌约等于 1 个英文或 个中文字符。',
setToCurrentModelMaxTokenTip: '最大令牌数更新为当前模型最大的令牌数 4,000。',
},
tone: {
......@@ -61,7 +62,8 @@ const translation = {
},
menus: {
status: 'beta',
apps: '应用',
explore: '探索',
apps: '构建应用',
plugins: '插件',
pluginsTips: '集成第三方插件或创建与 ChatGPT 兼容的 AI 插件。',
datasets: '数据集',
......@@ -80,11 +82,11 @@ const translation = {
settings: {
accountGroup: '账户',
workplaceGroup: '工作空间',
account: "我的账户",
members: "成员",
integrations: "集成",
language: "语言",
provider: "模型供应商"
account: '我的账户',
members: '成员',
integrations: '集成',
language: '语言',
provider: '模型供应商',
},
account: {
avatar: '头像',
......@@ -98,7 +100,7 @@ const translation = {
},
members: {
team: '团队',
invite: '邀请',
invite: '添加',
name: '姓名',
lastActive: '上次活动时间',
role: '角色',
......@@ -108,14 +110,14 @@ const translation = {
adminTip: '能够建立应用程序和管理团队设置',
normal: '正常人',
normalTip: '只能使用应用程序,不能建立应用程序',
inviteTeamMember: '邀请团队成员',
inviteTeamMemberTip: '对方会收到一封邮件。如果他已经是 Dify 用户则可直接在登录后访问你的团队数据。',
inviteTeamMember: '添加团队成员',
inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。',
email: '邮箱',
emailInvalid: '邮箱格式无效',
emailPlaceholder: '输入邮箱',
sendInvite: '发送邀请',
invitationSent: '邀请已发送',
invitationSentTip: '邀请已发送,对方登录 Dify 后即可访问你的团队数据。',
sendInvite: '添加',
invitationSent: '已添加',
invitationSentTip: '已添加,对方登录 Dify 后即可访问你的团队数据。',
ok: '好的',
removeFromTeam: '移除团队',
removeFromTeamTip: '将取消团队访问',
......@@ -131,19 +133,20 @@ const translation = {
googleAccount: 'Google 账号登录',
github: 'GitHub',
githubAccount: 'GitHub 账号登录',
connect: '绑定'
connect: '绑定',
},
language: {
displayLanguage: '界面语言',
timezone: '时区',
},
provider: {
apiKey: "API 密钥",
enterYourKey: "输入你的 API 密钥",
apiKey: 'API 密钥',
enterYourKey: '输入你的 API 密钥',
invalidKey: '无效的 OpenAI API 密钥',
validating: "验证密钥中...",
saveFailed: "API 密钥保存失败",
apiKeyExceedBill: "此 API KEY 已没有可用配额,请阅读",
validatedError: '校验失败:',
validating: '验证密钥中...',
saveFailed: 'API 密钥保存失败',
apiKeyExceedBill: '此 API KEY 已没有可用配额,请阅读',
addKey: '添加 密钥',
comingSoon: '即将推出',
editKey: '编辑',
......@@ -168,7 +171,7 @@ const translation = {
encrypted: {
front: '密钥将使用 ',
back: ' 技术进行加密和存储。',
}
},
},
about: {
changeLog: '更新日志',
......
......@@ -9,7 +9,7 @@ const translation = {
'删除数据集是不可逆的。用户将无法再访问您的数据集,所有的提示配置和日志将被永久删除。',
datasetDeleted: '数据集已删除',
datasetDeleteFailed: '删除数据集失败',
didYouKnow: '你知道吗??',
didYouKnow: '你知道吗?',
intro1: '数据集可以被集成到 Dify 应用中',
intro2: '作为上下文',
intro3: ',',
......
const translation = {
title: 'My Apps',
sidebar: {
discovery: 'Discovery',
workspace: 'Workspace',
action: {
pin: 'Pin',
unpin: 'Unpin',
delete: 'Delete',
},
delete: {
title: 'Delete app',
content: 'Are you sure you want to delete this app?',
}
},
apps: {
title: 'Explore Apps by Dify',
description: 'Use these template apps instantly or customize your own apps based on the templates.',
allCategories: 'All Categories',
},
appCard: {
addToWorkspace: 'Add to Workspace',
customize: 'Customize',
},
appCustomize: {
title: 'Create app from {{name}}',
subTitle: 'App icon & name',
nameRequired: 'App name is required',
},
category: {
'Assistant': 'Assistant',
'Writing': 'Writing',
'Translate': 'Translate',
'Programming': 'Programming',
'HR': 'HR',
}
}
export default translation
const translation = {
title: '我的应用',
sidebar: {
discovery: '发现',
workspace: '工作区',
action: {
pin: '置顶',
unpin: '取消置顶',
delete: '删除',
},
delete: {
title: '删除程序',
content: '您确定要删除此程序吗?',
}
},
apps: {
title: '探索 Dify 的应用',
description: '使用这些模板应用程序,或根据模板自定义您自己的应用程序。',
allCategories: '所有类别',
},
appCard: {
addToWorkspace: '添加到工作区',
customize: '自定义',
},
appCustomize: {
title: '从 {{name}} 创建应用程序',
subTitle: '应用程序图标和名称',
nameRequired: '应用程序名称不能为空',
},
category: {
'Assistant': '助手',
'Writing': '写作',
'Translate': '翻译',
'Programming': '编程',
'HR': '人力资源',
}
}
export default translation
......@@ -23,11 +23,12 @@ export const getLocale = (request: NextRequest): Locale => {
}
// match locale
let matchedLocale:Locale = i18n.defaultLocale
let matchedLocale: Locale = i18n.defaultLocale
try {
// If languages is ['*'], Error would happen in match function.
matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale
} catch(e) {}
}
catch (e) {}
return matchedLocale
}
......
......@@ -83,6 +83,10 @@ export type AppDailyConversationsResponse = {
data: Array<{ date: string; conversation_count: number }>
}
export type AppStatisticsResponse = {
data: Array<{ date: string }>
}
export type AppDailyEndUsersResponse = {
data: Array<{ date: string; terminal_count: number }>
}
......
import { AppMode } from "./app";
export type AppBasicInfo = {
id: string;
name: string;
mode: AppMode;
icon: string;
icon_background: string;
}
export type App = {
app: AppBasicInfo;
app_id: string;
description: string;
copyright: string;
privacy_policy: string;
category: string;
position: number;
is_listed: boolean;
install_count: number;
installed: boolean;
editable: boolean;
}
export type InstalledApp = {
app: AppBasicInfo;
id: string;
uninstallable: boolean
is_pinned: boolean
}
\ No newline at end of file
const { withSentryConfig } = require('@sentry/nextjs')
const EDITION = process.env.NEXT_PUBLIC_EDITION
const IS_CE_EDITION = EDITION === 'SELF_HOSTED'
const isDevelopment = process.env.NODE_ENV === 'development'
const isHideSentry = isDevelopment || IS_CE_EDITION
const withMDX = require('@next/mdx')({
extension: /\.mdx?$/,
options: {
......@@ -38,6 +45,25 @@ const nextConfig = {
},
]
},
...(isHideSentry
? {}
: {
sentry: {
hideSourceMaps: true,
},
}),
}
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup
const sentryWebpackPluginOptions = {
org: 'perfectworld',
project: 'javascript-nextjs',
silent: true, // Suppresses all logs
sourcemaps: {
assets: './**',
ignore: ['./node_modules/**'],
},
// https://github.com/getsentry/sentry-webpack-plugin#options.
}
module.exports = withMDX(nextConfig)
module.exports = isHideSentry ? withMDX(nextConfig) : withMDX(withSentryConfig(nextConfig, sentryWebpackPluginOptions))
{
"name": "dify-web",
"version": "0.2.0",
"version": "0.3.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"fix": "next lint --fix"
"fix": "next lint --fix",
"eslint-fix": "eslint --fix",
"prepare": "cd ../ && husky install ./web/.husky"
},
"dependencies": {
"@emoji-mart/data": "^1.1.2",
......@@ -17,6 +19,7 @@
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
"@next/mdx": "^13.2.4",
"@sentry/nextjs": "^7.53.1",
"@tailwindcss/line-clamp": "^0.4.2",
"@types/crypto-js": "^4.1.1",
"@types/lodash-es": "^4.17.7",
......@@ -77,8 +80,18 @@
"@types/qs": "^6.9.7",
"autoprefixer": "^10.4.14",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.2",
"miragejs": "^0.1.47",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7"
},
"lint-staged": {
"**/*.js?(x)": [
"eslint --fix"
],
"**/*.ts?(x)": [
"eslint --fix"
]
}
}
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: 'https://6bf48a450f054d749398c02a61bae343@o4505264807215104.ingest.sentry.io/4505264809115648',
// Replay may only be enabled for the client-side
integrations: [new Sentry.Replay()],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
})
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: 'https://6bf48a450f054d749398c02a61bae343@o4505264807215104.ingest.sentry.io/4505264809115648',
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
})
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: 'https://6bf48a450f054d749398c02a61bae343@o4505264807215104.ingest.sentry.io/4505264809115648',
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
})
import type { Fetcher } from 'swr'
import { del, get, post } from './base'
import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppTemplatesResponse, AppTokenCostsResponse, CreateApiKeyResponse, GenerationIntroductionResponse, UpdateAppModelConfigResponse, UpdateAppNameResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse } from '@/models/app'
import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, CreateApiKeyResponse, GenerationIntroductionResponse, UpdateAppModelConfigResponse, UpdateAppNameResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse } from '@/models/app'
import type { CommonResponse } from '@/models/common'
import type { AppMode, ModelConfig } from '@/types/app'
......@@ -16,7 +16,7 @@ export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> =
return get(url) as Promise<AppTemplatesResponse>
}
export const createApp: Fetcher<AppDetailResponse, { name: string; icon: string, icon_background: string, mode: AppMode; config?: ModelConfig }> = ({ name, icon, icon_background, mode, config }) => {
export const createApp: Fetcher<AppDetailResponse, { name: string; icon: string; icon_background: string; mode: AppMode; config?: ModelConfig }> = ({ name, icon, icon_background, mode, config }) => {
return post('apps', { body: { name, icon, icon_background, mode, model_config: config } }) as Promise<AppDetailResponse>
}
......@@ -54,6 +54,10 @@ export const getAppDailyConversations: Fetcher<AppDailyConversationsResponse, {
return get(url, { params }) as Promise<AppDailyConversationsResponse>
}
export const getAppStatistics: Fetcher<AppStatisticsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get(url, { params }) as Promise<AppStatisticsResponse>
}
export const getAppDailyEndUsers: Fetcher<AppDailyEndUsersResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get(url, { params }) as Promise<AppDailyEndUsersResponse>
}
......
import { get, post, del, patch } from './base'
export const fetchAppList = () => {
return get('/explore/apps')
}
export const fetchAppDetail = (id: string) : Promise<any> => {
return get(`/explore/apps/${id}`)
}
export const fetchInstalledAppList = () => {
return get('/installed-apps')
}
export const installApp = (id: string) => {
return post('/installed-apps', {
body: {
app_id: id
}
})
}
export const uninstallApp = (id: string) => {
return del(`/installed-apps/${id}`)
}
export const updatePinStatus = (id: string, isPinned: boolean) => {
return patch(`/installed-apps/${id}`, {
body: {
is_pinned: isPinned
}
})
}
import type { IOnCompleted, IOnData, IOnError } from './base'
import { getPublic as get, postPublic as post, ssePost, delPublic as del } from './base'
import {
get as consoleGet, post as consolePost, del as consoleDel,
getPublic as get, postPublic as post, ssePost, delPublic as del
} from './base'
import type { Feedbacktype } from '@/app/components/app/chat'
function getAction(action: 'get' | 'post' | 'del', isInstalledApp: boolean) {
switch (action) {
case 'get':
return isInstalledApp ? consoleGet : get
case 'post':
return isInstalledApp ? consolePost : post
case 'del':
return isInstalledApp ? consoleDel : del
}
}
function getUrl(url: string, isInstalledApp: boolean, installedAppId: string) {
return isInstalledApp ? `installed-apps/${installedAppId}/${url.startsWith('/') ? url.slice(1) : url}` : url
}
export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, getAbortController }: {
onData: IOnData
onCompleted: IOnCompleted
onError: IOnError,
getAbortController?: (abortController: AbortController) => void
}) => {
return ssePost('chat-messages', {
}, isInstalledApp: boolean, installedAppId = '') => {
return ssePost(getUrl('chat-messages', isInstalledApp, installedAppId), {
body: {
...body,
response_mode: 'streaming',
},
}, { onData, onCompleted, isPublicAPI: true, onError, getAbortController })
}, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, getAbortController })
}
export const sendCompletionMessage = async (body: Record<string, any>, { onData, onCompleted, onError }: {
onData: IOnData
onCompleted: IOnCompleted
onError: IOnError
}) => {
return ssePost('completion-messages', {
}, isInstalledApp: boolean, installedAppId = '') => {
return ssePost(getUrl('completion-messages', isInstalledApp, installedAppId), {
body: {
...body,
response_mode: 'streaming',
},
}, { onData, onCompleted, isPublicAPI: true, onError })
}, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError })
}
export const fetchAppInfo = async () => {
return get('/site')
}
export const fetchConversations = async () => {
return get('conversations', { params: { limit: 20, first_id: '' } })
export const fetchConversations = async (isInstalledApp: boolean, installedAppId='', last_id?: string) => {
return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: {...{ limit: 20 }, ...(last_id ? { last_id } : {}) } })
}
export const fetchChatList = async (conversationId: string) => {
return get('messages', { params: { conversation_id: conversationId, limit: 20, last_id: '' } })
export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId='') => {
return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } })
}
// Abandoned API interface
......@@ -47,35 +65,34 @@ export const fetchChatList = async (conversationId: string) => {
// }
// init value. wait for server update
export const fetchAppParams = async () => {
return get('parameters')
export const fetchAppParams = async (isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl('parameters', isInstalledApp, installedAppId))
}
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => {
return post(url, { body })
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('post', isInstalledApp))(getUrl(url, isInstalledApp, installedAppId), { body })
}
export const fetcMoreLikeThis = async (messageId: string) => {
return get(`/messages/${messageId}/more-like-this`, {
export const fetchMoreLikeThis = async (messageId: string, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/more-like-this`, isInstalledApp, installedAppId), {
params: {
response_mode: 'blocking',
}
})
}
export const saveMessage = (messageId: string) => {
return post('/saved-messages', { body: { message_id: messageId } })
export const saveMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('post', isInstalledApp))(getUrl('/saved-messages', isInstalledApp, installedAppId), { body: { message_id: messageId } })
}
export const fetchSavedMessage = async () => {
return get(`/saved-messages`)
export const fetchSavedMessage = async (isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl(`/saved-messages`, isInstalledApp, installedAppId))
}
export const removeMessage = (messageId: string) => {
return del(`/saved-messages/${messageId}`)
export const removeMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('del', isInstalledApp))(getUrl(`/saved-messages/${messageId}`, isInstalledApp, installedAppId))
}
export const fetchSuggestedQuestions = (messageId: string) => {
return get(`/messages/${messageId}/suggested-questions`)
export const fetchSuggestedQuestions = (messageId: string, isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/suggested-questions`, isInstalledApp, installedAppId))
}
......@@ -50,21 +50,20 @@ module.exports = {
indigo: {
25: '#F5F8FF',
100: '#E0EAFF',
600: '#444CE7'
}
600: '#444CE7',
},
},
screens: {
'mobile': '100px',
mobile: '100px',
// => @media (min-width: 100px) { ... }
'tablet': '640px', // 391
tablet: '640px', // 391
// => @media (min-width: 600px) { ... }
'pc': '769px',
pc: '769px',
// => @media (min-width: 769px) { ... }
},
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/line-clamp'),
],
}
......@@ -210,6 +210,7 @@ export type App = {
is_demo: boolean
/** Model configuration */
model_config: ModelConfig
app_model_config: ModelConfig
/** Timestamp of creation */
created_at: number
/** Web Application Configuration */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment