mirror of
https://github.com/langgenius/dify.git
synced 2026-01-14 02:27:00 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1c6cecf10 | ||
|
|
c5ccf382df | ||
|
|
8358d0abfa | ||
|
|
bad3b14438 | ||
|
|
f42ef494f8 | ||
|
|
bb7f454ecd | ||
|
|
7f48fadd41 | ||
|
|
af2138e8b8 | ||
|
|
091beffae7 | ||
|
|
408fb502a1 | ||
|
|
7660539689 | ||
|
|
5a6061ff61 | ||
|
|
970950e3a8 | ||
|
|
431b2fd4a8 | ||
|
|
88545184be | ||
|
|
2c23caacd4 | ||
|
|
9edea9bc49 | ||
|
|
d43279a1cc | ||
|
|
10848d74a0 | ||
|
|
f9df23a091 | ||
|
|
17a1c05728 | ||
|
|
66782ef19c | ||
|
|
fb7f509e5c | ||
|
|
1a5acf43aa | ||
|
|
4ef6392de5 | ||
|
|
effdc824d9 | ||
|
|
24fa452307 | ||
|
|
9e00e3894e | ||
|
|
023783372e | ||
|
|
1d06eba61a | ||
|
|
93e99fb343 | ||
|
|
b9ebce7ab7 | ||
|
|
33b3eaf324 |
61
.github/workflows/build-api-image.sh
vendored
61
.github/workflows/build-api-image.sh
vendored
@@ -1,61 +0,0 @@
|
||||
#!/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}"
|
||||
41
.github/workflows/build-api-image.yml
vendored
41
.github/workflows/build-api-image.yml
vendored
@@ -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'
|
||||
|
||||
60
.github/workflows/build-web-image.sh
vendored
60
.github/workflows/build-web-image.sh
vendored
@@ -1,60 +0,0 @@
|
||||
#!/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}"
|
||||
41
.github/workflows/build-web-image.yml
vendored
41
.github/workflows/build-web-image.yml
vendored
@@ -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'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -130,7 +130,6 @@ dmypy.json
|
||||
.idea/'
|
||||
|
||||
.DS_Store
|
||||
.vscode
|
||||
|
||||
# Intellij IDEA Files
|
||||
.idea/
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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.filter(App.is_public == True).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')
|
||||
|
||||
@@ -44,10 +44,11 @@ class InsertExploreAppListApi(Resource):
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('app_id', type=str, required=True, nullable=False, location='json')
|
||||
parser.add_argument('desc_en', type=str, location='json')
|
||||
parser.add_argument('desc_zh', type=str, 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()
|
||||
@@ -58,25 +59,24 @@ class InsertExploreAppListApi(Resource):
|
||||
|
||||
site = app.site
|
||||
if not site:
|
||||
desc = args['desc_en']
|
||||
copy_right = args['copyright']
|
||||
privacy_policy = args['privacy_policy']
|
||||
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 not args['desc_en'] else args['desc_en']
|
||||
copy_right = site.copyright if not args['copyright'] else args['copyright']
|
||||
privacy_policy = site.privacy_policy if not args['privacy_policy'] else args['privacy_policy']
|
||||
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={
|
||||
'en': desc,
|
||||
'zh': desc if not args['desc_zh'] else args['desc_zh']
|
||||
},
|
||||
description=desc,
|
||||
copyright=copy_right,
|
||||
privacy_policy=privacy_policy,
|
||||
language=args['language'],
|
||||
category=args['category'],
|
||||
position=args['position']
|
||||
)
|
||||
@@ -88,13 +88,10 @@ class InsertExploreAppListApi(Resource):
|
||||
|
||||
return {'result': 'success'}, 201
|
||||
else:
|
||||
recommended_app.description = {
|
||||
'en': desc,
|
||||
'zh': desc if not args['desc_zh'] else args['desc_zh']
|
||||
}
|
||||
|
||||
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']
|
||||
|
||||
|
||||
@@ -43,8 +43,11 @@ class RecommendedAppListApi(Resource):
|
||||
@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.is_listed == True,
|
||||
RecommendedApp.language == language_prefix
|
||||
).all()
|
||||
|
||||
categories = set()
|
||||
@@ -62,21 +65,17 @@ class RecommendedAppListApi(Resource):
|
||||
if not app or not app.is_public:
|
||||
continue
|
||||
|
||||
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']
|
||||
site = app.site
|
||||
if not site:
|
||||
continue
|
||||
|
||||
recommended_app_result = {
|
||||
'id': recommended_app.id,
|
||||
'app': app,
|
||||
'app_id': recommended_app.app_id,
|
||||
'description': desc,
|
||||
'copyright': recommended_app.copyright,
|
||||
'privacy_policy': recommended_app.privacy_policy,
|
||||
'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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
135
api/core/chain/llm_router_chain.py
Normal file
135
api/core/chain/llm_router_chain.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""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
|
||||
|
||||
|
||||
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_json_markdown(self, json_string: str) -> dict:
|
||||
# Remove the triple backticks if present
|
||||
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)
|
||||
else:
|
||||
raise Exception("Could not find JSON block in the output.")
|
||||
|
||||
return parsed
|
||||
|
||||
def parse_and_check_json_markdown(self, text: str, expected_keys: List[str]) -> dict:
|
||||
try:
|
||||
json_obj = self.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
|
||||
|
||||
def parse(self, text: str) -> Dict[str, Any]:
|
||||
try:
|
||||
expected_keys = ["destination", "next_inputs"]
|
||||
parsed = self.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 raised following error:\n{e}"
|
||||
)
|
||||
@@ -1,18 +1,18 @@
|
||||
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)
|
||||
|
||||
|
||||
140
api/core/chain/multi_dataset_router_chain.py
Normal file
140
api/core/chain/multi_dataset_router_chain.py
Normal file
@@ -0,0 +1,140 @@
|
||||
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:
|
||||
```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)
|
||||
)
|
||||
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}'"
|
||||
)
|
||||
@@ -1,14 +1,17 @@
|
||||
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}
|
||||
introduction = prompt_template.format(**prompt_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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""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'
|
||||
|
||||
@@ -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
|
||||
|
||||
7
web/.eslintignore
Normal file
7
web/.eslintignore
Normal file
@@ -0,0 +1,7 @@
|
||||
/**/node_modules/*
|
||||
node_modules/
|
||||
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
.next/
|
||||
@@ -23,6 +23,6 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"react-hooks/exhaustive-deps": "warning"
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
web/.husky/pre-commit
Executable file
4
web/.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
cd ./web && npx lint-staged
|
||||
25
web/.vscode/settings.json
vendored
Normal file
25
web/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -7,9 +7,9 @@ export type IAppDetail = {
|
||||
|
||||
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
8
web/app/(commonLayout)/explore/apps/page.tsx
Normal file
8
web/app/(commonLayout)/explore/apps/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import AppList from "@/app/components/explore/app-list"
|
||||
import React from 'react'
|
||||
|
||||
const Apps = ({ }) => {
|
||||
return <AppList />
|
||||
}
|
||||
|
||||
export default React.memo(Apps)
|
||||
15
web/app/(commonLayout)/explore/installed/[appId]/page.tsx
Normal file
15
web/app/(commonLayout)/explore/installed/[appId]/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
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)
|
||||
16
web/app/(commonLayout)/explore/layout.tsx
Normal file
16
web/app/(commonLayout)/explore/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import ExploreClient from '@/app/components/explore'
|
||||
export type IAppDetail = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
||||
return (
|
||||
<ExploreClient>
|
||||
{children}
|
||||
</ExploreClient>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppDetail)
|
||||
@@ -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 />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,9 +17,8 @@ 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">
|
||||
@@ -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,13 +302,15 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.openingStatement.title')}</div>
|
||||
</div>
|
||||
)}
|
||||
{(isResponsing && !content) ? (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
) : (
|
||||
<Markdown content={content} />
|
||||
)}
|
||||
{(isResponsing && !content)
|
||||
? (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<Markdown content={content} />
|
||||
)}
|
||||
{!showEdit
|
||||
? (annotation?.content
|
||||
&& <>
|
||||
@@ -384,13 +387,15 @@ const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar
|
||||
</div>
|
||||
{more && <MoreInfo more={more} isQuestion={true} />}
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
{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,27 +441,24 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,6 +470,10 @@ 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>
|
||||
|
||||
return (
|
||||
<div className={cn(!feedbackDisabled && 'px-3.5', 'h-full')}>
|
||||
{/* Chat List */}
|
||||
@@ -506,7 +512,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,7 +520,7 @@ 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'>
|
||||
@@ -544,17 +550,21 @@ 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>
|
||||
<Tooltip
|
||||
selector='send-tip'
|
||||
htmlContent={
|
||||
<div>
|
||||
<div>{t('common.operation.send')} Enter</div>
|
||||
<div>{t('common.operation.lineBreak')} Shift Enter</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={`${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`} onClick={handleSend}></div>
|
||||
</Tooltip>
|
||||
{isMobile
|
||||
? sendBtn
|
||||
: (
|
||||
<Tooltip
|
||||
selector='send-tip'
|
||||
htmlContent={
|
||||
<div>
|
||||
<div>{t('common.operation.send')} Enter</div>
|
||||
<div>{t('common.operation.lineBreak')} Shift Enter</div>
|
||||
</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;
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -56,8 +56,11 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
|
||||
}, [value])
|
||||
|
||||
const coloredContent = (tempValue || '')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
|
||||
|
||||
|
||||
const handleEdit = () => {
|
||||
|
||||
@@ -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, '<').replace(/>/g, '>'), promptVariables, inputs)),
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,8 +68,11 @@ const BlockInput: FC<IBlockInputProps> = ({
|
||||
})
|
||||
|
||||
const coloredContent = (currentValue || '')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.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 = () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
65
web/app/components/explore/app-card/index.tsx
Normal file
65
web/app/components/explore/app-card/index.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { App } from '@/models/explore'
|
||||
import AppModeLabel from '@/app/(commonLayout)/apps/AppModeLabel'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { PlusIcon } from '@heroicons/react/20/solid'
|
||||
import Button from '../../base/button'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
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" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="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
|
||||
20
web/app/components/explore/app-card/style.module.css
Normal file
20
web/app/components/explore/app-card/style.module.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.wrap {
|
||||
min-width: 312px;
|
||||
}
|
||||
|
||||
.mode {
|
||||
display: flex;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.opWrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrap:hover .mode {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrap:hover .opWrap {
|
||||
display: flex;
|
||||
}
|
||||
130
web/app/components/explore/app-list/index.tsx
Normal file
130
web/app/components/explore/app-list/index.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
'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)
|
||||
17
web/app/components/explore/app-list/style.module.css
Normal file
17
web/app/components/explore/app-list/style.module.css
Normal file
@@ -0,0 +1,17 @@
|
||||
@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))
|
||||
}
|
||||
}
|
||||
48
web/app/components/explore/category.tsx
Normal file
48
web/app/components/explore/category.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
'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)
|
||||
86
web/app/components/explore/create-app-modal/index.tsx
Normal file
86
web/app/components/explore/create-app-modal/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'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
|
||||
36
web/app/components/explore/create-app-modal/style.module.css
Normal file
36
web/app/components/explore/create-app-modal/style.module.css
Normal file
@@ -0,0 +1,36 @@
|
||||
.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;
|
||||
}
|
||||
54
web/app/components/explore/index.tsx
Normal file
54
web/app/components/explore/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
'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)
|
||||
37
web/app/components/explore/installed-app/index.tsx
Normal file
37
web/app/components/explore/installed-app/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'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)
|
||||
60
web/app/components/explore/item-operation/index.tsx
Normal file
60
web/app/components/explore/item-operation/index.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'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)
|
||||
31
web/app/components/explore/item-operation/style.module.css
Normal file
31
web/app/components/explore/item-operation/style.module.css
Normal file
@@ -0,0 +1,31 @@
|
||||
.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;
|
||||
}
|
||||
73
web/app/components/explore/sidebar/app-nav-item/index.tsx
Normal file
73
web/app/components/explore/sidebar/app-nav-item/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/* .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;
|
||||
}
|
||||
128
web/app/components/explore/sidebar/index.tsx
Normal file
128
web/app/components/explore/sidebar/index.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import cn from 'classnames'
|
||||
import { useSelectedLayoutSegments } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import Item from './app-nav-item'
|
||||
import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore'
|
||||
import Toast from '../../base/toast'
|
||||
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" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="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')}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -23,16 +23,19 @@ import { replaceStringWithValues } from '@/app/components/app/configuration/prom
|
||||
import AppUnavailable from '../../base/app-unavailable'
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import { SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
|
||||
import { InstalledApp } from '@/models/explore'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
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
|
||||
@@ -80,6 +83,11 @@ const Main: FC<IMainProps> = () => {
|
||||
setNewConversationInfo,
|
||||
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)
|
||||
@@ -129,7 +137,7 @@ const Main: FC<IMainProps> = () => {
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -222,28 +230,41 @@ const Main: FC<IMainProps> = () => {
|
||||
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)
|
||||
changeLanguage(siteInfo.default_language)
|
||||
if(siteInfo.default_language) {
|
||||
changeLanguage(siteInfo.default_language)
|
||||
}
|
||||
setNewConversationInfo({
|
||||
name: t('share.chat.newChatDefaultName'),
|
||||
introduction,
|
||||
@@ -379,7 +400,8 @@ const Main: FC<IMainProps> = () => {
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -400,11 +422,11 @@ const Main: FC<IMainProps> = () => {
|
||||
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,29 @@ const Main: FC<IMainProps> = () => {
|
||||
|
||||
return (
|
||||
<div className='bg-gray-100'>
|
||||
<Header
|
||||
title={siteInfo.title}
|
||||
icon={siteInfo.icon || ''}
|
||||
icon_background={siteInfo.icon_background || '#FFEAD5'}
|
||||
isMobile={isMobile}
|
||||
onShowSideBar={showSidebar}
|
||||
onCreateNewChat={() => handleConversationIdChange('-1')}
|
||||
/>
|
||||
{!isInstalledApp && (
|
||||
<Header
|
||||
title={siteInfo.title}
|
||||
icon={siteInfo.icon || ''}
|
||||
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 +502,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}
|
||||
|
||||
27
web/app/components/share/chat/sidebar/app-info/index.tsx
Normal file
27
web/app/components/share/chat/sidebar/app-info/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'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)
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@@ -7,43 +7,89 @@ 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 && (
|
||||
<div className="flex flex-shrink-0 p-4 !pb-0">
|
||||
<Button
|
||||
onClick={() => { onCurrentIdChange('-1') }}
|
||||
className="group block w-full flex-shrink-0 !justify-start !h-9 text-primary-600 items-center text-sm">
|
||||
<PencilSquareIcon className="mr-2 h-4 w-4" /> {t('share.chat.newChat')}
|
||||
</Button>
|
||||
</div>
|
||||
{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') }}
|
||||
className="group block w-full flex-shrink-0 !justify-start !h-9 text-primary-600 items-center text-sm">
|
||||
<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
|
||||
|
||||
3
web/app/components/share/chat/style.module.css
Normal file
3
web/app/components/share/chat/style.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.installedApp {
|
||||
height: calc(100vh - 74px);
|
||||
}
|
||||
@@ -60,8 +60,9 @@ const ConfigSence: FC<IConfigSenceProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className='mt-6 h-[1px] bg-gray-100'></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 ">
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import React, { FC, useEffect, useState, useRef } 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 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'
|
||||
@@ -12,6 +11,7 @@ import { fetchAppInfo, fetchAppParams, sendCompletionMessage, updateFeedback, sa
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig, MoreLikeThisConfig, SavedMessage } from '@/models/debug'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { Feedbacktype } from '@/app/components/app/chat'
|
||||
import { changeLanguage } from '@/i18n/i18next-config'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
@@ -22,8 +22,19 @@ 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 { App } from '@/types/app'
|
||||
import { InstalledApp } from '@/models/explore'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
|
||||
const TextGeneration = () => {
|
||||
export type IMainProps = {
|
||||
isInstalledApp?: boolean,
|
||||
installedAppInfo? : InstalledApp
|
||||
}
|
||||
|
||||
const TextGeneration: FC<IMainProps> = ({
|
||||
isInstalledApp = false,
|
||||
installedAppInfo
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
const isPC = media === MediaType.pc
|
||||
@@ -49,14 +60,14 @@ const TextGeneration = () => {
|
||||
})
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -151,12 +162,24 @@ 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)
|
||||
@@ -188,9 +211,11 @@ const TextGeneration = () => {
|
||||
<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'>
|
||||
@@ -227,6 +252,8 @@ const TextGeneration = () => {
|
||||
feedback={feedback}
|
||||
onSave={handleSaveMessage}
|
||||
isMobile={isMoble}
|
||||
isInstalledApp={isInstalledApp}
|
||||
installedAppId={installedAppInfo?.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -243,13 +270,21 @@ const TextGeneration = () => {
|
||||
|
||||
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 && (
|
||||
@@ -307,7 +342,10 @@ const TextGeneration = () => {
|
||||
|
||||
|
||||
{/* 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 && (
|
||||
<>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
.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'
|
||||
|
||||
|
||||
|
||||
|
||||
20
web/context/explore-context.ts
Normal file
20
web/context/explore-context.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const translation = {
|
||||
title: 'Apps',
|
||||
createApp: 'Create new App',
|
||||
modes: {
|
||||
completion: 'Text Generator',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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,20 +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",
|
||||
validatedError: "Validation failed: ",
|
||||
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',
|
||||
@@ -168,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,20 +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 密钥',
|
||||
validatedError: "校验失败:",
|
||||
validating: "验证密钥中...",
|
||||
saveFailed: "API 密钥保存失败",
|
||||
apiKeyExceedBill: "此 API KEY 已没有可用配额,请阅读",
|
||||
validatedError: '校验失败:',
|
||||
validating: '验证密钥中...',
|
||||
saveFailed: 'API 密钥保存失败',
|
||||
apiKeyExceedBill: '此 API KEY 已没有可用配额,请阅读',
|
||||
addKey: '添加 密钥',
|
||||
comingSoon: '即将推出',
|
||||
editKey: '编辑',
|
||||
@@ -169,7 +171,7 @@ const translation = {
|
||||
encrypted: {
|
||||
front: '密钥将使用 ',
|
||||
back: ' 技术进行加密和存储。',
|
||||
}
|
||||
},
|
||||
},
|
||||
about: {
|
||||
changeLog: '更新日志',
|
||||
|
||||
@@ -9,7 +9,7 @@ const translation = {
|
||||
'删除数据集是不可逆的。用户将无法再访问您的数据集,所有的提示配置和日志将被永久删除。',
|
||||
datasetDeleted: '数据集已删除',
|
||||
datasetDeleteFailed: '删除数据集失败',
|
||||
didYouKnow: '你知道吗??',
|
||||
didYouKnow: '你知道吗?',
|
||||
intro1: '数据集可以被集成到 Dify 应用中',
|
||||
intro2: '作为上下文',
|
||||
intro3: ',',
|
||||
|
||||
39
web/i18n/lang/explore.en.ts
Normal file
39
web/i18n/lang/explore.en.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
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
|
||||
39
web/i18n/lang/explore.zh.ts
Normal file
39
web/i18n/lang/explore.zh.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
30
web/models/explore.ts
Normal file
30
web/models/explore.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
const { withSentryConfig } = require('@sentry/nextjs')
|
||||
|
||||
const withMDX = require('@next/mdx')({
|
||||
extension: /\.mdx?$/,
|
||||
options: {
|
||||
@@ -29,6 +31,9 @@ const nextConfig = {
|
||||
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
sentry: {
|
||||
hideSourceMaps: true,
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
@@ -40,4 +45,17 @@ const nextConfig = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = withMDX(nextConfig)
|
||||
// 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(withSentryConfig(nextConfig, sentryWebpackPluginOptions))
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
23
web/sentry.client.config.js
Normal file
23
web/sentry.client.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
})
|
||||
16
web/sentry.edge.config.js
Normal file
16
web/sentry.edge.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
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
|
||||
})
|
||||
10
web/sentry.server.config.js
Normal file
10
web/sentry.server.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
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,
|
||||
})
|
||||
33
web/service/explore.ts
Normal file
33
web/service/explore.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,44 +1,62 @@
|
||||
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 */
|
||||
|
||||
Reference in New Issue
Block a user