Merge branch 'main' into feat/explore

This commit is contained in:
金伟强 2023-05-17 14:10:48 +08:00
commit 2558a61b28
26 changed files with 362 additions and 104 deletions

View File

@ -5,8 +5,6 @@ on:
branches:
- 'main'
- 'deploy/dev'
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
jobs:
build-and-push:

View File

@ -5,8 +5,6 @@ on:
branches:
- 'main'
- 'deploy/dev'
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
jobs:
build-and-push:

2
.gitignore vendored
View File

@ -139,7 +139,7 @@ dmypy.json
api/.env
api/storage/*
docker/volumes/app/storage/privkeys/*
docker/volumes/app/storage/*
docker/volumes/db/data/*
docker/volumes/redis/data/*
docker/volumes/weaviate/*

View File

@ -22,14 +22,14 @@ To set up a working development environment, just fork the project git repositor
### Fork the repository
you need to fork the [repository](https://github.com/langgenius/langgenius-gateway).
you need to fork the [repository](https://github.com/langgenius/dify).
### Clone the repo
Clone your GitHub forked repository:
```
git clone git@github.com:<github_username>/langgenius-gateway.git
git clone git@github.com:<github_username>/dify.git
```
### Install backend

View File

@ -4,7 +4,7 @@
<a href="./README_CN.md">简体中文</a>
</p>
[Website](https://dify.ai) • [Docs](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai)
[Website](https://dify.ai) • [Docs](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7)
**Dify** is an easy-to-use LLMOps platform designed to empower more people to create sustainable, AI-native applications. With visual orchestration for various application types, Dify offers out-of-the-box, ready-to-use applications that can also serve as Backend-as-a-Service APIs. Unify your development process with one API for plugins and datasets integration, and streamline your operations using a single interface for prompt engineering, visual analytics, and continuous improvement.
@ -41,7 +41,7 @@ cd docker
docker-compose up -d
```
After running, you can access the Dify console in your browser at [http://localhost](http://localhost) and start the initialization operation.
After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization installation process.
### Configuration
@ -86,7 +86,7 @@ A: English and Chinese are currently supported, and you can contribute language
If you have any questions, suggestions, or partnership inquiries, feel free to contact us through the following channels:
- Submit an Issue or PR on our GitHub Repo
- Join the discussion in our [Discord](https://discord.gg/AhzKf7dNgk) Community
- Join the discussion in our [Discord](https://discord.gg/FngNHpbcY7) Community
- Send an email to hello@dify.ai
We're eager to assist you and together create more fun and useful AI applications!

View File

@ -5,7 +5,7 @@
</p>
[官方网站](https://dify.ai) • [文档](https://docs.dify.ai/v/zh-hans) • [Twitter](https://twitter.com/dify_ai)
[官方网站](https://dify.ai) • [文档](https://docs.dify.ai/v/zh-hans) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7)
**Dify** 是一个易用的 LLMOps 平台,旨在让更多人可以创建可持续运营的原生 AI 应用。Dify 提供多种类型应用的可视化编排,应用可开箱即用,也能以“后端即服务”的 API 提供服务。
@ -43,7 +43,7 @@ cd docker
docker-compose up -d
```
运行后,可以在浏览器上访问 [http://localhost](http://localhost) 进入 Dify 控制台并开始初始化操作。
运行后,可以在浏览器上访问 [http://localhost/install](http://localhost/install) 进入 Dify 控制台并开始初始化安装操作。
### 配置
@ -87,7 +87,7 @@ A: 现已支持英文与中文,你可以为我们贡献语言包。
如果您有任何问题、建议或合作意向,欢迎通过以下方式联系我们:
- 在我们的 [GitHub Repo](https://github.com/langgenius/dify) 上提交 Issue 或 PR
- 在我们的 [Discord 社区](https://discord.gg/AhzKf7dNgk) 上加入讨论
- 在我们的 [Discord 社区](https://discord.gg/FngNHpbcY7) 上加入讨论
- 发送邮件至 hello@dify.ai
## 贡献代码

View File

@ -45,7 +45,7 @@ message_detail_fields = {
'message_tokens': fields.Integer,
'answer': fields.String,
'answer_tokens': fields.Integer,
'provider_response_latency': fields.Integer,
'provider_response_latency': fields.Float,
'from_source': fields.String,
'from_end_user_id': fields.String,
'from_account_id': fields.String,

View File

@ -26,46 +26,46 @@ from services.errors.conversation import ConversationNotExistsError
from services.errors.message import MessageNotExistsError
from services.message_service import MessageService
account_fields = {
'id': fields.String,
'name': fields.String,
'email': fields.String
}
class ChatMessageApi(Resource):
account_fields = {
'id': fields.String,
'name': fields.String,
'email': fields.String
}
feedback_fields = {
'rating': fields.String,
'content': fields.String,
'from_source': fields.String,
'from_end_user_id': fields.String,
'from_account': fields.Nested(account_fields, allow_null=True),
}
feedback_fields = {
'rating': fields.String,
'content': fields.String,
'from_source': fields.String,
'from_end_user_id': fields.String,
'from_account': fields.Nested(account_fields, allow_null=True),
}
annotation_fields = {
'content': fields.String,
'account': fields.Nested(account_fields, allow_null=True),
'created_at': TimestampField
}
annotation_fields = {
'content': fields.String,
'account': fields.Nested(account_fields, allow_null=True),
'created_at': TimestampField
}
message_detail_fields = {
'id': fields.String,
'conversation_id': fields.String,
'inputs': fields.Raw,
'query': fields.String,
'message': fields.Raw,
'message_tokens': fields.Integer,
'answer': fields.String,
'answer_tokens': fields.Integer,
'provider_response_latency': fields.Float,
'from_source': fields.String,
'from_end_user_id': fields.String,
'from_account_id': fields.String,
'feedbacks': fields.List(fields.Nested(feedback_fields)),
'annotation': fields.Nested(annotation_fields, allow_null=True),
'created_at': TimestampField
}
message_detail_fields = {
'id': fields.String,
'conversation_id': fields.String,
'inputs': fields.Raw,
'query': fields.String,
'message': fields.Raw,
'message_tokens': fields.Integer,
'answer': fields.String,
'answer_tokens': fields.Integer,
'provider_response_latency': fields.Integer,
'from_source': fields.String,
'from_end_user_id': fields.String,
'from_account_id': fields.String,
'feedbacks': fields.List(fields.Nested(feedback_fields)),
'annotation': fields.Nested(annotation_fields, allow_null=True),
'created_at': TimestampField
}
class ChatMessageListApi(Resource):
message_infinite_scroll_pagination_fields = {
'limit': fields.Integer,
'has_more': fields.Boolean,
@ -253,7 +253,8 @@ class MessageMoreLikeThisApi(Resource):
message_id = str(message_id)
parser = reqparse.RequestParser()
parser.add_argument('response_mode', type=str, required=True, choices=['blocking', 'streaming'], location='args')
parser.add_argument('response_mode', type=str, required=True, choices=['blocking', 'streaming'],
location='args')
args = parser.parse_args()
streaming = args['response_mode'] == 'streaming'
@ -301,7 +302,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
except QuotaExceededError:
yield "data: " + json.dumps(api.handle_error(ProviderQuotaExceededError()).get_json()) + "\n\n"
except ModelCurrentlyNotSupportError:
yield "data: " + json.dumps(api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
yield "data: " + json.dumps(
api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
yield "data: " + json.dumps(api.handle_error(CompletionRequestError(str(e))).get_json()) + "\n\n"
@ -353,9 +355,33 @@ class MessageSuggestedQuestionApi(Resource):
return {'data': questions}
class MessageApi(Resource):
@setup_required
@login_required
@account_initialization_required
@marshal_with(message_detail_fields)
def get(self, app_id, message_id):
app_id = str(app_id)
message_id = str(message_id)
# get app info
app_model = _get_app(app_id, 'chat')
message = db.session.query(Message).filter(
Message.id == message_id,
Message.app_id == app_model.id
).first()
if not message:
raise NotFound("Message Not Exists.")
return message
api.add_resource(MessageMoreLikeThisApi, '/apps/<uuid:app_id>/completion-messages/<uuid:message_id>/more-like-this')
api.add_resource(MessageSuggestedQuestionApi, '/apps/<uuid:app_id>/chat-messages/<uuid:message_id>/suggested-questions')
api.add_resource(ChatMessageApi, '/apps/<uuid:app_id>/chat-messages', endpoint='chat_messages')
api.add_resource(ChatMessageListApi, '/apps/<uuid:app_id>/chat-messages', endpoint='console_chat_messages')
api.add_resource(MessageFeedbackApi, '/apps/<uuid:app_id>/feedbacks')
api.add_resource(MessageAnnotationApi, '/apps/<uuid:app_id>/annotations')
api.add_resource(MessageAnnotationCountApi, '/apps/<uuid:app_id>/annotations/count')
api.add_resource(MessageApi, '/apps/<uuid:app_id>/messages/<uuid:message_id>', endpoint='console_message')

View File

@ -2,8 +2,6 @@ import decimal
import json
from typing import Optional, Union
from gunicorn.config import User
from core.callback_handler.entity.agent_loop import AgentLoop
from core.callback_handler.entity.dataset_query import DatasetQueryObj
from core.callback_handler.entity.llm_message import LLMMessage
@ -269,7 +267,7 @@ class ConversationMessageTask:
class PubHandler:
def __init__(self, user: Union[Account | User], task_id: str,
def __init__(self, user: Union[Account | EndUser], task_id: str,
message: Message, conversation: Conversation,
chain_pub: bool = False, agent_thought_pub: bool = False):
self._channel = PubHandler.generate_channel_name(user, task_id)
@ -282,12 +280,12 @@ class PubHandler:
self._agent_thought_pub = agent_thought_pub
@classmethod
def generate_channel_name(cls, user: Union[Account | User], task_id: str):
def generate_channel_name(cls, user: Union[Account | EndUser], task_id: str):
user_str = 'account-' + user.id if isinstance(user, Account) else 'end-user-' + user.id
return "generate_result:{}-{}".format(user_str, task_id)
@classmethod
def generate_stopped_cache_key(cls, user: Union[Account | User], task_id: str):
def generate_stopped_cache_key(cls, user: Union[Account | EndUser], task_id: str):
user_str = 'account-' + user.id if isinstance(user, Account) else 'end-user-' + user.id
return "generate_result_stopped:{}-{}".format(user_str, task_id)
@ -366,7 +364,7 @@ class PubHandler:
redis_client.publish(self._channel, json.dumps(content))
@classmethod
def pub_error(cls, user: Union[Account | User], task_id: str, e):
def pub_error(cls, user: Union[Account | EndUser], task_id: str, e):
content = {
'error': type(e).__name__,
'description': e.description if getattr(e, 'description', None) is not None else str(e)
@ -379,7 +377,7 @@ class PubHandler:
return redis_client.get(self._stopped_cache_key) is not None
@classmethod
def stop(cls, user: Union[Account | User], task_id: str):
def stop(cls, user: Union[Account | EndUser], task_id: str):
stopped_cache_key = cls.generate_stopped_cache_key(user, task_id)
redis_client.setex(stopped_cache_key, 600, 1)

View File

@ -0,0 +1,68 @@
"""Functionality for splitting text."""
from __future__ import annotations
from typing import (
Any,
List,
Optional,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
class FixedRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter):
def __init__(self, fixed_separator: str = "\n\n", separators: Optional[List[str]] = None, **kwargs: Any):
"""Create a new TextSplitter."""
super().__init__(**kwargs)
self._fixed_separator = fixed_separator
self._separators = separators or ["\n\n", "\n", " ", ""]
def split_text(self, text: str) -> List[str]:
"""Split incoming text and return chunks."""
if self._fixed_separator:
chunks = text.split(self._fixed_separator)
else:
chunks = list(text)
final_chunks = []
for chunk in chunks:
if self._length_function(chunk) > self._chunk_size:
final_chunks.extend(self.recursive_split_text(chunk))
else:
final_chunks.append(chunk)
return final_chunks
def recursive_split_text(self, text: str) -> List[str]:
"""Split incoming text and return chunks."""
final_chunks = []
# Get appropriate separator to use
separator = self._separators[-1]
for _s in self._separators:
if _s == "":
separator = _s
break
if _s in text:
separator = _s
break
# Now that we have the separator, split the text
if separator:
splits = text.split(separator)
else:
splits = list(text)
# Now go merging things, recursively splitting longer texts.
_good_splits = []
for s in splits:
if self._length_function(s) < self._chunk_size:
_good_splits.append(s)
else:
if _good_splits:
merged_text = self._merge_splits(_good_splits, separator)
final_chunks.extend(merged_text)
_good_splits = []
other_info = self.recursive_split_text(s)
final_chunks.extend(other_info)
if _good_splits:
merged_text = self._merge_splits(_good_splits, separator)
final_chunks.extend(merged_text)
return final_chunks

View File

@ -18,6 +18,7 @@ from core.docstore.dataset_docstore import DatesetDocumentStore
from core.index.keyword_table_index import KeywordTableIndex
from core.index.readers.html_parser import HTMLParser
from core.index.readers.pdf_parser import PDFParser
from core.index.spiltter.fixed_text_splitter import FixedRecursiveCharacterTextSplitter
from core.index.vector_index import VectorIndex
from core.llm.token_calculator import TokenCalculator
from extensions.ext_database import db
@ -267,16 +268,14 @@ class IndexingRunner:
raise ValueError("Custom segment length should be between 50 and 1000.")
separator = segmentation["separator"]
if not separator:
separators = ["\n\n", "", ".", " ", ""]
else:
if separator:
separator = separator.replace('\\n', '\n')
separators = [separator, ""]
character_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
character_splitter = FixedRecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=segmentation["max_tokens"],
chunk_overlap=0,
separators=separators
fixed_separator=separator,
separators=["\n\n", "", ".", " ", ""]
)
else:
# Automatic segmentation

View File

@ -1,6 +1,6 @@
import json
from flask import current_app
from flask import current_app, request
from flask_login import UserMixin
from sqlalchemy.dialects.postgresql import UUID
@ -56,7 +56,7 @@ class App(db.Model):
@property
def api_base_url(self):
return current_app.config['API_URL'] + '/v1'
return (current_app.config['API_URL'] if current_app.config['API_URL'] else request.host_url.rstrip('/')) + '/v1'
@property
def tenant(self):
@ -505,7 +505,7 @@ class Site(db.Model):
@property
def app_base_url(self):
return current_app.config['APP_URL']
return (current_app.config['APP_URL'] if current_app.config['APP_URL'] else request.host_url.rstrip('/'))
class ApiToken(db.Model):

View File

@ -11,12 +11,18 @@ services:
LOG_LEVEL: INFO
# A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`.
SECRET_KEY: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
# The base URL of console application, refers to the Console base URL of WEB service.
CONSOLE_URL: http://localhost
# The URL for Service API endpointsrefers to the base URL of the current API service.
API_URL: http://localhost
# The URL for Web APP, refers to the Web App base URL of WEB service.
APP_URL: http://localhost
# The base URL of console application, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai
CONSOLE_URL: ''
# The URL for Service API endpointsrefers to the base URL of the current API service if api domain is
# different from console domain.
# example: http://api.dify.ai
API_URL: ''
# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app
APP_URL: ''
# When enabled, migrations will be executed prior to application startup and the application will start after the migrations have completed.
MIGRATION_ENABLED: 'true'
# The configurations of postgres database connection.
@ -43,16 +49,25 @@ services:
# The configurations of celery broker.
# Use redis as the broker, and redis db 1 for celery broker.
CELERY_BROKER_URL: redis://:difyai123456@redis:6379/1
# Specifies the allowed origins for cross-origin requests to the Web API
WEB_API_CORS_ALLOW_ORIGINS: http://localhost,*
# Specifies the allowed origins for cross-origin requests to the console API
CONSOLE_CORS_ALLOW_ORIGINS: http://localhost,*
# Specifies the allowed origins for cross-origin requests to the Web API, e.g. https://dify.app or * for all origins.
WEB_API_CORS_ALLOW_ORIGINS: '*'
# Specifies the allowed origins for cross-origin requests to the console API, e.g. https://cloud.dify.ai or * for all origins.
CONSOLE_CORS_ALLOW_ORIGINS: '*'
# CSRF Cookie settings
# Controls whether a cookie is sent with cross-site requests,
# providing some protection against cross-site request forgery attacks
#
# Default: `SameSite=Lax, Secure=false, HttpOnly=true`
# This default configuration supports same-origin requests using either HTTP or HTTPS,
# but does not support cross-origin requests. It is suitable for local debugging purposes.
#
# If you want to enable cross-origin support,
# you must use the HTTPS protocol and set the configuration to `SameSite=None, Secure=true, HttpOnly=true`.
#
# For **production** purposes, please set `SameSite=Lax, Secure=true, HttpOnly=true`.
COOKIE_HTTPONLY: 'true'
COOKIE_SAMESITE: 'None'
COOKIE_SECURE: 'true'
COOKIE_SAMESITE: 'Lax'
COOKIE_SECURE: 'false'
# The type of storage to use for storing user files. Supported values are `local` and `s3`, Default: `local`
STORAGE_TYPE: local
# The path to the local storage directory, the directory relative the root path of API service codes or absolute path. Default: `storage` or `/home/john/storage`.
@ -86,7 +101,7 @@ services:
- weaviate
volumes:
# Mount the storage directory to the container, for storing user files.
- ./volumes/app/storage:/app/storage
- ./volumes/app/storage:/app/api/storage
# worker service
# The Celery worker for processing the queue.
@ -104,12 +119,6 @@ services:
# A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`.
# same as the API service
SECRET_KEY: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
# The base URL of console application, refers to the Console base URL of WEB service.
CONSOLE_URL: http://localhost
# The URL for Service API endpointsrefers to the base URL of the current API service.
API_URL: http://localhost
# The URL for Web APP, refers to the Web App base URL of WEB service.
APP_URL: http://localhost
# The configurations of postgres database connection.
# It is consistent with the configuration in the 'db' service below.
DB_USERNAME: postgres
@ -137,7 +146,7 @@ services:
- weaviate
volumes:
# Mount the storage directory to the container, for storing user files.
- ./volumes/app/storage:/app/storage
- ./volumes/app/storage:/app/api/storage
# Frontend web application.
web:
@ -145,10 +154,14 @@ services:
restart: always
environment:
EDITION: SELF_HOSTED
# The base URL of console application, refers to the Console base URL of WEB service.
CONSOLE_URL: http://localhost
# The URL for Web APP, refers to the Web App base URL of WEB service.
APP_URL: http://localhost
# The base URL of console application, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai
CONSOLE_URL: ''
# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app
APP_URL: ''
# The postgres database.
db:

View File

@ -1,6 +1,6 @@
server {
listen 80;
server_name localhost;
server_name _;
location /console/api {
proxy_pass http://api:5001;

View File

@ -0,0 +1,5 @@
{
"presets": [
"@babel/preset-env"
]
}

View File

@ -1,8 +1,8 @@
import axios from 'axios'
const BASE_URL = 'https://api.dify.ai/v1'
export const BASE_URL = 'https://api.dify.ai/v1'
const routes = {
export const routes = {
application: {
method: 'GET',
url: () => `/parameters`
@ -17,7 +17,7 @@ const routes = {
},
createChatMessage: {
method: 'POST',
url: () => `/chat-message`,
url: () => `/chat-messages`,
},
getConversationMessages: {
method: 'GET',

View File

@ -0,0 +1,66 @@
import { DifyClient, BASE_URL, routes } from ".";
import axios from 'axios'
jest.mock('axios')
describe('Client', () => {
let difyClient
beforeEach(() => {
difyClient = new DifyClient('test')
})
test('should create a client', () => {
expect(difyClient).toBeDefined();
})
// test updateApiKey
test('should update the api key', () => {
difyClient.updateApiKey('test2');
expect(difyClient.apiKey).toBe('test2');
})
});
describe('Send Requests', () => {
let difyClient
beforeEach(() => {
difyClient = new DifyClient('test')
})
afterEach(() => {
jest.resetAllMocks()
})
it('should make a successful request to the application parameter', async () => {
const method = 'GET'
const endpoint = routes.application.url
const expectedResponse = { data: 'response' }
axios.mockResolvedValue(expectedResponse)
await difyClient.sendRequest(method, endpoint)
expect(axios).toHaveBeenCalledWith({
method,
url: `${BASE_URL}${endpoint}`,
data: null,
params: null,
headers: {
Authorization: `Bearer ${difyClient.apiKey}`,
'Content-Type': 'application/json',
},
responseType: 'json',
})
})
it('should handle errors from the API', async () => {
const method = 'GET'
const endpoint = '/test-endpoint'
const errorMessage = 'Request failed with status code 404'
axios.mockRejectedValue(new Error(errorMessage))
await expect(difyClient.sendRequest(method, endpoint)).rejects.toThrow(
errorMessage
)
})
})

View File

@ -1,6 +1,6 @@
{
"name": "dify-client",
"version": "1.0.2",
"version": "1.0.3",
"description": "This is the Node.js SDK for the Dify.AI API, which allows you to easily integrate Dify.AI into your Node.js applications.",
"main": "index.js",
"type": "module",
@ -14,7 +14,21 @@
"<crazywoola> <<427733928@qq.com>> (https://github.com/crazywoola)"
],
"license": "MIT",
"scripts": {
"test": "jest"
},
"jest": {
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
}
},
"dependencies": {
"axios": "^1.3.5"
},
"devDependencies": {
"@babel/core": "^7.21.8",
"@babel/preset-env": "^7.21.5",
"babel-jest": "^29.5.0",
"jest": "^29.5.0"
}
}

View File

@ -0,0 +1,44 @@
'use client'
import React from 'react'
import Tooltip from '@/app/components/base/tooltip'
import { t } from 'i18next'
import s from './style.module.css'
import copy from 'copy-to-clipboard'
type ICopyBtnProps = {
value: string
className?: string
}
const CopyBtn = ({
value,
className,
}: ICopyBtnProps) => {
const [isCopied, setIsCopied] = React.useState(false)
return (
<div className={`${className}`}>
<Tooltip
selector="copy-btn-tooltip"
content={(isCopied ? t('appApi.copied') : t('appApi.copy')) as string}
className='z-10'
>
<div
className={`box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer`}
style={{
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)'
}}
onClick={() => {
copy(value)
setIsCopied(true)
}}
>
<div className={`w-6 h-6 hover:bg-gray-50 ${s.copyIcon} ${isCopied ? s.copied : ''}`}></div>
</div>
</Tooltip>
</div>
)
}
export default CopyBtn

View File

@ -0,0 +1,15 @@
.copyIcon {
background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
background-position: center;
background-repeat: no-repeat;
}
.copyIcon:hover {
background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
background-position: center;
background-repeat: no-repeat;
}
.copyIcon.copied {
background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
}

View File

@ -16,6 +16,8 @@ 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'
const stopIcon = (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -105,7 +107,7 @@ const MoreInfo: FC<{ more: MessageMore; isQuestion: boolean }> = ({ more, isQues
const { t } = useTranslation()
return (<div className={`mt-1 space-x-2 text-xs text-gray-400 ${isQuestion ? 'mr-2 text-right ' : 'ml-2 text-left float-right'}`}>
<span>{`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`}</span>
<span>{`${t('appLog.detail.tokenCost')} ${more.tokens}`}</span>
<span>{`${t('appLog.detail.tokenCost')} ${formatNumber(more.tokens)}`}</span>
<span>· </span>
<span>{more.time} </span>
</div>)
@ -345,6 +347,10 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
}
</div>
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
<CopyBtn
value={content}
className={cn(s.copyBtn, 'mr-1')}
/>
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')}
{/* Admin feedback is displayed only in the background. */}
{!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)}

View File

@ -38,6 +38,14 @@
background: url(./icons/answer.svg) no-repeat;
}
.copyBtn {
display: none;
}
.answerWrap:hover .copyBtn {
display: block;
}
.answerWrap .itemOperation {
display: none;
}

View File

@ -247,8 +247,8 @@ const Debug: FC<IDebug> = ({
...draft[index],
more: {
time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
tokens: newResponseItem.answer_tokens,
latency: (newResponseItem.provider_response_latency / 1000).toFixed(2),
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
latency: newResponseItem.provider_response_latency.toFixed(2),
}
}
}

View File

@ -94,8 +94,8 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
isAnswer: true,
more: {
time: dayjs.unix(item.created_at).format('hh:mm A'),
tokens: item.answer_tokens,
latency: (item.provider_response_latency / 1000).toFixed(2),
tokens: item.answer_tokens + item.message_tokens,
latency: item.provider_response_latency.toFixed(2),
},
annotation: item.annotation,
})

View File

@ -71,7 +71,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
<div className='flex flex-col w-full'>
<div className='text-gray-900'>{t(`${prefixCustomize}.way1.step2`)}</div>
<div className='text-gray-500 text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step2Tip`)}</div>
<pre className='box-border py-3 px-4 bg-gray-100 text-xs font-medium rounded-lg'>
<pre className='box-border py-3 px-4 bg-gray-100 text-xs font-medium rounded-lg select-text'>
export const APP_ID = '{appId}'<br />
export const API_KEY = {`'<Web API Key From Dify>'`}
</pre>

View File

@ -89,7 +89,7 @@ export default function AppSelector({ userProfile, onLogout, langeniusVersionInf
<Link
className={classNames(itemClassName, 'group justify-between')}
href={
locale === 'zh-Hans' ? 'https://docs.dify.ai/zh-hans/' : 'https://docs.dify.ai/en/'
locale === 'zh-Hans' ? 'https://docs.dify.ai/v/zh-hans/' : 'https://docs.dify.ai/'
}
target='_blank'>
<div>{t('common.userProfile.helpCenter')}</div>