Compare commits

...

12 Commits

27 changed files with 347 additions and 106 deletions

View File

@@ -82,6 +82,33 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: yarn run lint run: yarn run lint
docker-compose-template:
name: Docker Compose Template
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v45
with:
files: |
docker/generate_docker_compose
docker/.env.example
docker/docker-compose-template.yaml
docker/docker-compose.yaml
- name: Generate Docker Compose
if: steps.changed-files.outputs.any_changed == 'true'
run: |
cd docker
./generate_docker_compose
- name: Check for changes
if: steps.changed-files.outputs.any_changed == 'true'
run: git diff --exit-code
superlinter: superlinter:
name: SuperLinter name: SuperLinter

View File

@@ -7,4 +7,4 @@ api = ExternalApi(bp)
from . import index from . import index
from .app import app, audio, completion, conversation, file, message, workflow from .app import app, audio, completion, conversation, file, message, workflow
from .dataset import dataset, document, hit_testing, segment from .dataset import dataset, document, hit_testing, segment, upload_file

View File

@@ -0,0 +1,54 @@
from werkzeug.exceptions import NotFound
from controllers.service_api import api
from controllers.service_api.wraps import (
DatasetApiResource,
)
from core.file import helpers as file_helpers
from extensions.ext_database import db
from models.dataset import Dataset
from models.model import UploadFile
from services.dataset_service import DocumentService
class UploadFileApi(DatasetApiResource):
def get(self, tenant_id, dataset_id, document_id):
"""Get upload file."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
# check upload file
if document.data_source_type != "upload_file":
raise ValueError(f"Document data source type ({document.data_source_type}) is not upload_file.")
data_source_info = document.data_source_info_dict
if data_source_info and "upload_file_id" in data_source_info:
file_id = data_source_info["upload_file_id"]
upload_file = db.session.query(UploadFile).filter(UploadFile.id == file_id).first()
if not upload_file:
raise NotFound("UploadFile not found.")
else:
raise ValueError("Upload file id not found in document data source info.")
url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
return {
"id": upload_file.id,
"name": upload_file.name,
"size": upload_file.size,
"extension": upload_file.extension,
"url": url,
"download_url": f"{url}&as_attachment=true",
"mime_type": upload_file.mime_type,
"created_by": upload_file.created_by,
"created_at": upload_file.created_at.timestamp(),
}, 200
api.add_resource(UploadFileApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/upload-file")

View File

@@ -530,7 +530,6 @@ class IndexingRunner:
# chunk nodes by chunk size # chunk nodes by chunk size
indexing_start_at = time.perf_counter() indexing_start_at = time.perf_counter()
tokens = 0 tokens = 0
chunk_size = 10
if dataset_document.doc_form != IndexType.PARENT_CHILD_INDEX: if dataset_document.doc_form != IndexType.PARENT_CHILD_INDEX:
# create keyword index # create keyword index
create_keyword_thread = threading.Thread( create_keyword_thread = threading.Thread(
@@ -539,11 +538,22 @@ class IndexingRunner:
) )
create_keyword_thread.start() create_keyword_thread.start()
max_workers = 10
if dataset.indexing_technique == "high_quality": if dataset.indexing_technique == "high_quality":
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [] futures = []
for i in range(0, len(documents), chunk_size):
chunk_documents = documents[i : i + chunk_size] # Distribute documents into multiple groups based on the hash values of page_content
# This is done to prevent multiple threads from processing the same document,
# Thereby avoiding potential database insertion deadlocks
document_groups: list[list[Document]] = [[] for _ in range(max_workers)]
for document in documents:
hash = helper.generate_text_hash(document.page_content)
group_index = int(hash, 16) % max_workers
document_groups[group_index].append(document)
for chunk_documents in document_groups:
if len(chunk_documents) == 0:
continue
futures.append( futures.append(
executor.submit( executor.submit(
self._process_chunk, self._process_chunk,

View File

@@ -1,6 +1,9 @@
import logging
from threading import Lock from threading import Lock
from typing import Any from typing import Any
logger = logging.getLogger(__name__)
_tokenizer: Any = None _tokenizer: Any = None
_lock = Lock() _lock = Lock()
@@ -43,5 +46,6 @@ class GPT2Tokenizer:
base_path = abspath(__file__) base_path = abspath(__file__)
gpt2_tokenizer_path = join(dirname(base_path), "gpt2") gpt2_tokenizer_path = join(dirname(base_path), "gpt2")
_tokenizer = TransformerGPT2Tokenizer.from_pretrained(gpt2_tokenizer_path) _tokenizer = TransformerGPT2Tokenizer.from_pretrained(gpt2_tokenizer_path)
logger.info("Fallback to Transformers' GPT-2 tokenizer from tiktoken")
return _tokenizer return _tokenizer

View File

@@ -112,7 +112,7 @@ class ApiBasedToolSchemaParser:
llm_description=property.get("description", ""), llm_description=property.get("description", ""),
default=property.get("default", None), default=property.get("default", None),
placeholder=I18nObject( placeholder=I18nObject(
en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "") en_US=property.get("description", ""), zh_Hans=property.get("description", "")
), ),
) )

View File

@@ -513,7 +513,7 @@ TENCENT_VECTOR_DB_SHARD=1
TENCENT_VECTOR_DB_REPLICAS=2 TENCENT_VECTOR_DB_REPLICAS=2
# ElasticSearch configuration, only available when VECTOR_STORE is `elasticsearch` # ElasticSearch configuration, only available when VECTOR_STORE is `elasticsearch`
ELASTICSEARCH_HOST=elasticsearch ELASTICSEARCH_HOST=0.0.0.0
ELASTICSEARCH_PORT=9200 ELASTICSEARCH_PORT=9200
ELASTICSEARCH_USERNAME=elastic ELASTICSEARCH_USERNAME=elastic
ELASTICSEARCH_PASSWORD=elastic ELASTICSEARCH_PASSWORD=elastic

View File

@@ -409,7 +409,7 @@ services:
milvus-standalone: milvus-standalone:
container_name: milvus-standalone container_name: milvus-standalone
image: milvusdb/milvus:v2.3.1 image: milvusdb/milvus:v2.5.0-beta
profiles: profiles:
- milvus - milvus
command: [ 'milvus', 'run', 'standalone' ] command: [ 'milvus', 'run', 'standalone' ]
@@ -493,20 +493,28 @@ services:
container_name: elasticsearch container_name: elasticsearch
profiles: profiles:
- elasticsearch - elasticsearch
- elasticsearch-ja
restart: always restart: always
volumes: volumes:
- ./elasticsearch/docker-entrypoint.sh:/docker-entrypoint-mount.sh
- dify_es01_data:/usr/share/elasticsearch/data - dify_es01_data:/usr/share/elasticsearch/data
environment: environment:
ELASTIC_PASSWORD: ${ELASTICSEARCH_PASSWORD:-elastic} ELASTIC_PASSWORD: ${ELASTICSEARCH_PASSWORD:-elastic}
VECTOR_STORE: ${VECTOR_STORE:-}
cluster.name: dify-es-cluster cluster.name: dify-es-cluster
node.name: dify-es0 node.name: dify-es0
discovery.type: single-node discovery.type: single-node
xpack.license.self_generated.type: trial xpack.license.self_generated.type: basic
xpack.security.enabled: 'true' xpack.security.enabled: 'true'
xpack.security.enrollment.enabled: 'false' xpack.security.enrollment.enabled: 'false'
xpack.security.http.ssl.enabled: 'false' xpack.security.http.ssl.enabled: 'false'
ports: ports:
- ${ELASTICSEARCH_PORT:-9200}:9200 - ${ELASTICSEARCH_PORT:-9200}:9200
deploy:
resources:
limits:
memory: 2g
entrypoint: [ 'sh', '-c', "sh /docker-entrypoint-mount.sh" ]
healthcheck: healthcheck:
test: [ 'CMD', 'curl', '-s', 'http://localhost:9200/_cluster/health?pretty' ] test: [ 'CMD', 'curl', '-s', 'http://localhost:9200/_cluster/health?pretty' ]
interval: 30s interval: 30s

View File

@@ -1106,6 +1106,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<hr className='ml-0 mr-0' /> <hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/upload-file'
method='GET'
title='Get Upload File'
name='#get_upload_file'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
<Property name='document_id' type='string' key='document_id'>
Document ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/documents/{document_id}/upload-file"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "file_id",
"name": "file_name",
"size": 1024,
"extension": "txt",
"url": "preview_url",
"download_url": "download_url",
"mime_type": "text/plain",
"created_by": "user_id",
"created_at": 1728734540,
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading <Heading
url='/datasets/{dataset_id}/retrieve' url='/datasets/{dataset_id}/retrieve'
method='POST' method='POST'

View File

@@ -1107,6 +1107,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<hr className='ml-0 mr-0' /> <hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/upload-file'
method='GET'
title='获取上传文件'
name='#get_upload_file'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='document_id' type='string' key='document_id'>
文档 ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/documents/{document_id}/upload-file"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "file_id",
"name": "file_name",
"size": 1024,
"extension": "txt",
"url": "preview_url",
"download_url": "download_url",
"mime_type": "text/plain",
"created_by": "user_id",
"created_at": 1728734540,
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading <Heading
url='/datasets/{dataset_id}/retrieve' url='/datasets/{dataset_id}/retrieve'
method='POST' method='POST'

View File

@@ -1,29 +1,25 @@
'use client' 'use client'
import React, { useState } from 'react' import React from 'react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type IRemoveIconProps = { type IRemoveIconProps = {
className?: string className?: string
isHoverStatus?: boolean
onClick: () => void onClick: () => void
} } & React.HTMLAttributes<HTMLDivElement>
const RemoveIcon = ({ const RemoveIcon = ({
className, className,
isHoverStatus,
onClick, onClick,
...props
}: IRemoveIconProps) => { }: IRemoveIconProps) => {
const [isHovered, setIsHovered] = useState(false)
const computedIsHovered = isHoverStatus || isHovered
return ( return (
<div <div
className={cn(className, computedIsHovered && 'bg-[#FEE4E2]', 'flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-[#FEE4E2]')} className={cn('flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-state-destructive-hover text-text-tertiary hover:text-text-destructive', className)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={onClick} onClick={onClick}
{...props}
> >
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 6H14M6 8H18M16.6667 8L16.1991 15.0129C16.129 16.065 16.0939 16.5911 15.8667 16.99C15.6666 17.3412 15.3648 17.6235 15.0011 17.7998C14.588 18 14.0607 18 13.0062 18H10.9938C9.93927 18 9.41202 18 8.99889 17.7998C8.63517 17.6235 8.33339 17.3412 8.13332 16.99C7.90607 16.5911 7.871 16.065 7.80086 15.0129L7.33333 8M10.6667 11V14.3333M13.3333 11V14.3333" stroke={computedIsHovered ? '#D92D20' : '#667085'} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> <path d="M10 6H14M6 8H18M16.6667 8L16.1991 15.0129C16.129 16.065 16.0939 16.5911 15.8667 16.99C15.6666 17.3412 15.3648 17.6235 15.0011 17.7998C14.588 18 14.0607 18 13.0062 18H10.9938C9.93927 18 9.41202 18 8.99889 17.7998C8.63517 17.6235 8.33339 17.3412 8.13332 16.99C7.90607 16.5911 7.871 16.065 7.80086 15.0129L7.33333 8M10.6667 11V14.3333M13.3333 11V14.3333" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</div> </div>
) )

View File

@@ -1,12 +1,13 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/24/outline' import { PlusIcon } from '@heroicons/react/24/outline'
import { ReactSortable } from 'react-sortablejs' import { ReactSortable } from 'react-sortablejs'
import RemoveIcon from '../../base/icons/remove-icon' import RemoveIcon from '../../base/icons/remove-icon'
import s from './style.module.css' import s from './style.module.css'
import cn from '@/utils/classnames'
export type Options = string[] export type Options = string[]
export type IConfigSelectProps = { export type IConfigSelectProps = {
@@ -19,6 +20,8 @@ const ConfigSelect: FC<IConfigSelectProps> = ({
onChange, onChange,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [delBtnHoverIndex, setDelBtnHoverIndex] = useState(-1)
const [focusedIndex, setFocusedIndex] = useState(-1)
const optionList = options.map((content, index) => { const optionList = options.map((content, index) => {
return ({ return ({
@@ -36,48 +39,62 @@ const ConfigSelect: FC<IConfigSelectProps> = ({
list={optionList} list={optionList}
setList={list => onChange(list.map(item => item.name))} setList={list => onChange(list.map(item => item.name))}
handle='.handle' handle='.handle'
ghostClass="opacity-50" ghostClass="opacity-30"
animation={150} animation={150}
> >
{options.map((o, index) => ( {options.map((o, index) => {
<div className={`${s.inputWrap} relative`} key={index}> const delBtnHover = delBtnHoverIndex === index
<div className='handle flex items-center justify-center w-4 h-4 cursor-grab'> const inputFocused = focusedIndex === index
<svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg"> return (
<path fillRule="evenodd" clipRule="evenodd" d="M1 2C1.55228 2 2 1.55228 2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1C0 1.55228 0.447715 2 1 2ZM1 6C1.55228 6 2 5.55228 2 5C2 4.44772 1.55228 4 1 4C0.447715 4 0 4.44772 0 5C0 5.55228 0.447715 6 1 6ZM6 1C6 1.55228 5.55228 2 5 2C4.44772 2 4 1.55228 4 1C4 0.447715 4.44772 0 5 0C5.55228 0 6 0.447715 6 1ZM5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6ZM2 9C2 9.55229 1.55228 10 1 10C0.447715 10 0 9.55229 0 9C0 8.44771 0.447715 8 1 8C1.55228 8 2 8.44771 2 9ZM5 10C5.55228 10 6 9.55229 6 9C6 8.44771 5.55228 8 5 8C4.44772 8 4 8.44771 4 9C4 9.55229 4.44772 10 5 10Z" fill="#98A2B3" /> <div
</svg> className={cn(
</div> `${s.inputWrap} relative border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg`,
<input inputFocused && 'border-components-input-border-active bg-components-input-bg-active',
delBtnHover && 'bg-state-destructive-hover',
)}
key={index} key={index}
type="input" >
value={o || ''} <div className='handle flex items-center justify-center w-3.5 h-3.5 cursor-grab text-text-quaternary'>
onChange={(e) => { <svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
const value = e.target.value <path fillRule="evenodd" clipRule="evenodd" d="M1 2C1.55228 2 2 1.55228 2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1C0 1.55228 0.447715 2 1 2ZM1 6C1.55228 6 2 5.55228 2 5C2 4.44772 1.55228 4 1 4C0.447715 4 0 4.44772 0 5C0 5.55228 0.447715 6 1 6ZM6 1C6 1.55228 5.55228 2 5 2C4.44772 2 4 1.55228 4 1C4 0.447715 4.44772 0 5 0C5.55228 0 6 0.447715 6 1ZM5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6ZM2 9C2 9.55229 1.55228 10 1 10C0.447715 10 0 9.55229 0 9C0 8.44771 0.447715 8 1 8C1.55228 8 2 8.44771 2 9ZM5 10C5.55228 10 6 9.55229 6 9C6 8.44771 5.55228 8 5 8C4.44772 8 4 8.44771 4 9C4 9.55229 4.44772 10 5 10Z" fill="currentColor" />
onChange(options.map((item, i) => { </svg>
if (index === i) </div>
return value <input
key={index}
type="input"
value={o || ''}
onChange={(e) => {
const value = e.target.value
onChange(options.map((item, i) => {
if (index === i)
return value
return item return item
})) }))
}} }}
className={'w-full pl-1.5 pr-8 text-sm leading-9 text-gray-900 border-0 grow h-9 bg-transparent focus:outline-none cursor-pointer'} onFocus={() => { setFocusedIndex(index) }}
/> onBlur={() => { setFocusedIndex(-1) }}
<RemoveIcon className={'w-full pl-1 pr-8 system-sm-medium text-text-secondary border-0 grow h-8 bg-transparent group focus:outline-none cursor-pointer caret-[#295EFF]'}
className={`${s.deleteBtn} absolute top-1/2 translate-y-[-50%] right-1.5 items-center justify-center w-6 h-6 rounded-md cursor-pointer hover:bg-[#FEE4E2]`} />
onClick={() => { <RemoveIcon
onChange(options.filter((_, i) => index !== i)) className={`${s.deleteBtn} absolute top-1/2 translate-y-[-50%] right-1 items-center justify-center w-6 h-6 rounded-lg cursor-pointer`}
}} onClick={() => {
/> onChange(options.filter((_, i) => index !== i))
</div> }}
))} onMouseEnter={() => setDelBtnHoverIndex(index)}
onMouseLeave={() => setDelBtnHoverIndex(-1)}
/>
</div>)
})}
</ReactSortable> </ReactSortable>
</div> </div>
)} )}
<div <div
onClick={() => { onChange([...options, '']) }} onClick={() => { onChange([...options, '']) }}
className='flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100'> className='flex items-center h-8 px-2 gap-1 rounded-lg cursor-pointer bg-components-button-tertiary-bg'>
<PlusIcon width={16} height={16}></PlusIcon> <PlusIcon className='text-components-button-tertiary-text' width={16} height={16} />
<div className='text-gray-500 text-[13px]'>{t('appDebug.variableConfig.addOption')}</div> <div className='text-components-button-tertiary-text system-sm-medium'>{t('appDebug.variableConfig.addOption')}</div>
</div> </div>
</div> </div>
) )

View File

@@ -2,7 +2,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 8px; border-radius: 8px;
border: 1px solid #EAECF0;
padding-left: 10px; padding-left: 10px;
cursor: pointer; cursor: pointer;
} }

View File

@@ -32,9 +32,9 @@ const SelectTypeItem: FC<ISelectTypeItemProps> = ({
onClick={onClick} onClick={onClick}
> >
<div className='shrink-0'> <div className='shrink-0'>
<InputVarTypeIcon type={type} className='w-5 h-5' /> <InputVarTypeIcon type={type} className='w-5 h-5 text-text-secondary' />
</div> </div>
<span>{typeName}</span> <span className='text-text-secondary'>{typeName}</span>
</div> </div>
) )
} }

View File

@@ -6,6 +6,7 @@ import type { EChartsOption } from 'echarts'
import useSWR from 'swr' import useSWR from 'swr'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { get } from 'lodash-es' import { get } from 'lodash-es'
import Decimal from 'decimal.js'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { formatNumber } from '@/utils/format' import { formatNumber } from '@/utils/format'
import Basic from '@/app/components/app-sidebar/basic' import Basic from '@/app/components/app-sidebar/basic'
@@ -60,10 +61,8 @@ const CHART_TYPE_CONFIG: Record<string, IChartConfigType> = {
}, },
} }
const sum = (arr: number[]): number => { const sum = (arr: Decimal.Value[]): number => {
return arr.reduce((acr, cur) => { return Decimal.sum(...arr).toNumber()
return acr + cur
})
} }
const defaultPeriod = { const defaultPeriod = {

View File

@@ -306,8 +306,14 @@ const GenerationItem: FC<IGenerationItemProps> = ({
} }
<div className={`flex ${contentClassName}`}> <div className={`flex ${contentClassName}`}>
<div className='grow w-0'> <div className='grow w-0'>
{siteInfo && siteInfo.show_workflow_steps && workflowProcessData && ( {siteInfo && workflowProcessData && (
<WorkflowProcessItem data={workflowProcessData} expand={workflowProcessData.expand} hideProcessDetail={hideProcessDetail} /> <WorkflowProcessItem
data={workflowProcessData}
expand={workflowProcessData.expand}
hideProcessDetail={hideProcessDetail}
hideInfo={hideProcessDetail}
readonly={!siteInfo.show_workflow_steps}
/>
)} )}
{workflowProcessData && !isError && ( {workflowProcessData && !isError && (
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} /> <ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} />

View File

@@ -13,7 +13,7 @@ import AgentContent from './agent-content'
import BasicContent from './basic-content' import BasicContent from './basic-content'
import SuggestedQuestions from './suggested-questions' import SuggestedQuestions from './suggested-questions'
import More from './more' import More from './more'
import WorkflowProcess from './workflow-process' import WorkflowProcessItem from './workflow-process'
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
import Citation from '@/app/components/base/chat/chat/citation' import Citation from '@/app/components/base/chat/chat/citation'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
@@ -133,7 +133,7 @@ const Answer: FC<AnswerProps> = ({
{/** Render the normal steps */} {/** Render the normal steps */}
{ {
workflowProcess && !hideProcessDetail && ( workflowProcess && !hideProcessDetail && (
<WorkflowProcess <WorkflowProcessItem
data={workflowProcess} data={workflowProcess}
item={item} item={item}
hideProcessDetail={hideProcessDetail} hideProcessDetail={hideProcessDetail}
@@ -142,11 +142,12 @@ const Answer: FC<AnswerProps> = ({
} }
{/** Hide workflow steps by it's settings in siteInfo */} {/** Hide workflow steps by it's settings in siteInfo */}
{ {
workflowProcess && hideProcessDetail && appData && appData.site.show_workflow_steps && ( workflowProcess && hideProcessDetail && appData && (
<WorkflowProcess <WorkflowProcessItem
data={workflowProcess} data={workflowProcess}
item={item} item={item}
hideProcessDetail={hideProcessDetail} hideProcessDetail={hideProcessDetail}
readonly={!appData.site.show_workflow_steps}
/> />
) )
} }

View File

@@ -23,6 +23,7 @@ type WorkflowProcessProps = {
expand?: boolean expand?: boolean
hideInfo?: boolean hideInfo?: boolean
hideProcessDetail?: boolean hideProcessDetail?: boolean
readonly?: boolean
} }
const WorkflowProcessItem = ({ const WorkflowProcessItem = ({
data, data,
@@ -30,6 +31,7 @@ const WorkflowProcessItem = ({
expand = false, expand = false,
hideInfo = false, hideInfo = false,
hideProcessDetail = false, hideProcessDetail = false,
readonly = false,
}: WorkflowProcessProps) => { }: WorkflowProcessProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [collapse, setCollapse] = useState(!expand) const [collapse, setCollapse] = useState(!expand)
@@ -81,8 +83,8 @@ const WorkflowProcessItem = ({
}} }}
> >
<div <div
className={cn('flex items-center cursor-pointer', !collapse && 'px-1.5')} className={cn('flex items-center cursor-pointer', !collapse && 'px-1.5', readonly && 'cursor-default')}
onClick={() => setCollapse(!collapse)} onClick={() => !readonly && setCollapse(!collapse)}
> >
{ {
running && ( running && (
@@ -102,10 +104,10 @@ const WorkflowProcessItem = ({
<div className={cn('system-xs-medium text-text-secondary', !collapse && 'grow')}> <div className={cn('system-xs-medium text-text-secondary', !collapse && 'grow')}>
{t('workflow.common.workflowProcess')} {t('workflow.common.workflowProcess')}
</div> </div>
<RiArrowRightSLine className={`'ml-1 w-4 h-4 text-text-tertiary' ${collapse ? '' : 'rotate-90'}`} /> {!readonly && <RiArrowRightSLine className={`'ml-1 w-4 h-4 text-text-tertiary' ${collapse ? '' : 'rotate-90'}`} />}
</div> </div>
{ {
!collapse && ( !collapse && !readonly && (
<div className='mt-1.5'> <div className='mt-1.5'>
{ {
<TracingPanel <TracingPanel

View File

@@ -82,7 +82,8 @@ const BlockIcon: FC<BlockIconProps> = ({
}) => { }) => {
return ( return (
<div className={` <div className={`
flex items-center justify-center border-[0.5px] border-white/2 text-white flex items-center justify-center border-[0.5px] border-divider-subtle
text-text-primary-on-surface shadow-md shadow-shadow-shadow-5
${ICON_CONTAINER_CLASSNAME_SIZE_MAP[size]} ${ICON_CONTAINER_CLASSNAME_SIZE_MAP[size]}
${ICON_CONTAINER_BG_COLOR_MAP[type]} ${ICON_CONTAINER_BG_COLOR_MAP[type]}
${toolIcon && '!shadow-none'} ${toolIcon && '!shadow-none'}

View File

@@ -33,9 +33,10 @@ export const TitleInput = memo(({
value={localValue} value={localValue}
onChange={e => setLocalValue(e.target.value)} onChange={e => setLocalValue(e.target.value)}
className={` className={`
grow mr-2 px-1 h-6 text-base text-gray-900 font-semibold rounded-lg border border-transparent appearance-none outline-none grow mr-2 px-1 h-6 system-xl-semibold text-text-primary rounded-lg border border-transparent appearance-none outline-none
hover:bg-gray-50 placeholder:text-text-placeholder
focus:border-gray-300 focus:shadow-xs focus:bg-white caret-[#295EFF] bg-transparent hover:bg-state-base-hover
focus:border-components-input-border-active focus:shadow-xs shadow-shadow-shadow-3 focus:bg-components-input-active caret-[#295EFF]
min-w-0 min-w-0
`} `}
placeholder={t('workflow.common.addTitle') || ''} placeholder={t('workflow.common.addTitle') || ''}
@@ -66,8 +67,8 @@ export const DescriptionInput = memo(({
<div <div
className={` className={`
group flex px-2 py-[5px] max-h-[60px] rounded-lg overflow-y-auto group flex px-2 py-[5px] max-h-[60px] rounded-lg overflow-y-auto
border border-transparent hover:bg-gray-50 leading-0 border border-transparent bg-transparent hover:bg-state-base-hover leading-0
${focus && '!border-gray-300 shadow-xs !bg-gray-50'} ${focus && '!border-components-input-border-active shadow-xs shadow-shadow-shadow-3 !bg-components-input-bg-active'}
`} `}
> >
<Textarea <Textarea
@@ -77,9 +78,9 @@ export const DescriptionInput = memo(({
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
className={` className={`
w-full text-xs text-gray-900 leading-[18px] bg-transparent w-full text-xs text-text-primary leading-[18px] bg-transparent
appearance-none outline-none resize-none appearance-none outline-none resize-none
placeholder:text-gray-400 caret-[#295EFF] placeholder:text-text-placeholder caret-[#295EFF]
`} `}
placeholder={t('workflow.common.addDescription') || ''} placeholder={t('workflow.common.addDescription') || ''}
autoSize autoSize

View File

@@ -5,11 +5,11 @@ import { useBoolean, useHover } from 'ahooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
RiEditLine,
} from '@remixicon/react' } from '@remixicon/react'
import InputVarTypeIcon from '../../_base/components/input-var-type-icon' import InputVarTypeIcon from '../../_base/components/input-var-type-icon'
import type { InputVar, MoreInfo } from '@/app/components/workflow/types' import type { InputVar, MoreInfo } from '@/app/components/workflow/types'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { Edit03 } from '@/app/components/base/icons/src/vender/solid/general'
import Badge from '@/app/components/base/badge' import Badge from '@/app/components/base/badge'
import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal' import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal'
@@ -46,12 +46,12 @@ const VarItem: FC<Props> = ({
hideEditVarModal() hideEditVarModal()
}, [onChange, hideEditVarModal]) }, [onChange, hideEditVarModal])
return ( return (
<div ref={ref} className='flex items-center h-8 justify-between px-2.5 bg-white rounded-lg border border-gray-200 shadow-xs cursor-pointer hover:shadow-md'> <div ref={ref} className='flex items-center h-8 justify-between px-2.5 bg-components-panel-on-panel-item-bg rounded-lg border border-components-panel-border-subtle shadow-xs cursor-pointer hover:shadow-md shadow-shadow-shadow-3'>
<div className='flex items-center space-x-1 grow w-0'> <div className='flex items-center space-x-1 grow w-0'>
<Variable02 className='w-3.5 h-3.5 text-primary-500' /> <Variable02 className='w-3.5 h-3.5 text-text-accent' />
<div title={payload.variable} className='shrink-0 max-w-[130px] truncate text-[13px] font-medium text-gray-700'>{payload.variable}</div> <div title={payload.variable} className='shrink-0 max-w-[130px] truncate system-sm-medium text-text-secondary'>{payload.variable}</div>
{payload.label && (<><div className='shrink-0 text-xs font-medium text-gray-400'>·</div> {payload.label && (<><div className='shrink-0 system-xs-regular text-text-quaternary'>·</div>
<div title={payload.label as string} className='max-w-[130px] truncate text-[13px] font-medium text-gray-500'>{payload.label as string}</div> <div title={payload.label as string} className='max-w-[130px] truncate system-xs-medium text-text-tertiary'>{payload.label as string}</div>
</>)} </>)}
{showLegacyBadge && ( {showLegacyBadge && (
<Badge <Badge
@@ -66,18 +66,18 @@ const VarItem: FC<Props> = ({
? ( ? (
<> <>
{payload.required && ( {payload.required && (
<div className='mr-2 text-xs font-normal text-gray-500'>{t('workflow.nodes.start.required')}</div> <Badge className='mr-2' uppercase>{t('workflow.nodes.start.required')}</Badge>
)} )}
<InputVarTypeIcon type={payload.type} className='w-3.5 h-3.5 text-gray-500' /> <InputVarTypeIcon type={payload.type} className='w-3 h-3 text-text-tertiary' />
</> </>
) )
: (!readonly && ( : (!readonly && (
<> <>
<div onClick={showEditVarModal} className='mr-1 p-1 rounded-md cursor-pointer hover:bg-black/5'> <div onClick={showEditVarModal} className='mr-1 p-1 cursor-pointer'>
<Edit03 className='w-4 h-4 text-gray-500' /> <RiEditLine className='w-4 h-4 text-text-tertiary' />
</div> </div>
<div onClick={onRemove} className='p-1 rounded-md cursor-pointer hover:bg-black/5'> <div onClick={onRemove} className='p-1 cursor-pointer'>
<RiDeleteBinLine className='w-4 h-4 text-gray-500' /> <RiDeleteBinLine className='w-4 h-4 text-text-tertiary' />
</div> </div>
</> </>
))} ))}

View File

@@ -46,7 +46,7 @@ const VarList: FC<Props> = ({
if (list.length === 0) { if (list.length === 0) {
return ( return (
<div className='flex rounded-md bg-gray-50 items-center h-[42px] justify-center leading-[18px] text-xs font-normal text-gray-500'> <div className='flex rounded-md bg-background-section items-center h-10 justify-center system-xs-regular text-text-tertiary'>
{t('workflow.nodes.start.noVarTip')} {t('workflow.nodes.start.noVarTip')}
</div> </div>
) )

View File

@@ -20,15 +20,15 @@ const Node: FC<NodeProps<StartNodeType>> = ({
<div className='mb-1 px-3 py-1'> <div className='mb-1 px-3 py-1'>
<div className='space-y-0.5'> <div className='space-y-0.5'>
{variables.map(variable => ( {variables.map(variable => (
<div key={variable.variable} className='flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-700'> <div key={variable.variable} className='flex items-center h-6 justify-between bg-workflow-block-parma-bg rounded-md px-1 space-x-1'>
<div className='w-0 grow flex items-center space-x-1'> <div className='w-0 grow flex items-center space-x-1'>
<Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' /> <Variable02 className='shrink-0 w-3.5 h-3.5 text-text-accent' />
<span className='w-0 grow truncate text-xs font-normal text-gray-700'>{variable.variable}</span> <span className='w-0 grow truncate system-xs-regular text-text-secondary'>{variable.variable}</span>
</div> </div>
<div className='ml-1 flex items-center space-x-1'> <div className='ml-1 flex items-center space-x-1'>
{variable.required && <span className='text-xs font-normal text-gray-500 uppercase'>{t(`${i18nPrefix}.required`)}</span>} {variable.required && <span className='system-2xs-regular-uppercase text-text-tertiary'>{t(`${i18nPrefix}.required`)}</span>}
<InputVarTypeIcon type={variable.type} className='w-3 h-3 text-gray-500' /> <InputVarTypeIcon type={variable.type} className='w-3 h-3 text-text-tertiary' />
</div> </div>
</div> </div>
))} ))}

View File

@@ -64,7 +64,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
variable: 'sys.query', variable: 'sys.query',
} as any} } as any}
rightContent={ rightContent={
<div className='text-xs font-normal text-gray-500'> <div className='system-xs-regular text-text-tertiary'>
String String
</div> </div>
} }
@@ -78,7 +78,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
variable: 'sys.files', variable: 'sys.files',
} as any} } as any}
rightContent={ rightContent={
<div className='text-xs font-normal text-gray-500'> <div className='system-xs-regular text-text-tertiary'>
Array[File] Array[File]
</div> </div>
} }
@@ -92,7 +92,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
variable: 'sys.dialogue_count', variable: 'sys.dialogue_count',
} as any} } as any}
rightContent={ rightContent={
<div className='text-xs font-normal text-gray-500'> <div className='system-xs-regular text-text-tertiary'>
Number Number
</div> </div>
} }
@@ -103,7 +103,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
variable: 'sys.conversation_id', variable: 'sys.conversation_id',
} as any} } as any}
rightContent={ rightContent={
<div className='text-xs font-normal text-gray-500'> <div className='system-xs-regular text-text-tertiary'>
String String
</div> </div>
} }
@@ -117,7 +117,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
variable: 'sys.user_id', variable: 'sys.user_id',
} as any} } as any}
rightContent={ rightContent={
<div className='text-xs font-normal text-gray-500'> <div className='system-xs-regular text-text-tertiary'>
String String
</div> </div>
} }
@@ -128,7 +128,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
variable: 'sys.app_id', variable: 'sys.app_id',
} as any} } as any}
rightContent={ rightContent={
<div className='text-xs font-normal text-gray-500'> <div className='system-xs-regular text-text-tertiary'>
String String
</div> </div>
} }
@@ -139,7 +139,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
variable: 'sys.workflow_id', variable: 'sys.workflow_id',
} as any} } as any}
rightContent={ rightContent={
<div className='text-xs font-normal text-gray-500'> <div className='system-xs-regular text-text-tertiary'>
String String
</div> </div>
} }
@@ -150,7 +150,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
variable: 'sys.workflow_run_id', variable: 'sys.workflow_run_id',
} as any} } as any}
rightContent={ rightContent={
<div className='text-xs font-normal text-gray-500'> <div className='system-xs-regular text-text-tertiary'>
String String
</div> </div>
} }

View File

@@ -50,6 +50,7 @@
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"decimal.js": "^10.4.3",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"echarts-for-react": "^3.0.2", "echarts-for-react": "^3.0.2",
"elkjs": "^0.9.3", "elkjs": "^0.9.3",

View File

@@ -21,16 +21,23 @@ function waitUntilTokenRefreshed() {
}) })
} }
const isRefreshingSignAvailable = function (delta: number) {
const nowTime = new Date().getTime()
const lastTime = globalThis.localStorage.getItem('last_refresh_time') || '0'
return nowTime - parseInt(lastTime) <= delta
}
// only one request can send // only one request can send
async function getNewAccessToken(): Promise<void> { async function getNewAccessToken(timeout: number): Promise<void> {
try { try {
const isRefreshingSign = globalThis.localStorage.getItem(LOCAL_STORAGE_KEY) const isRefreshingSign = globalThis.localStorage.getItem(LOCAL_STORAGE_KEY)
if ((isRefreshingSign && isRefreshingSign === '1') || isRefreshing) { if ((isRefreshingSign && isRefreshingSign === '1' && isRefreshingSignAvailable(timeout)) || isRefreshing) {
await waitUntilTokenRefreshed() await waitUntilTokenRefreshed()
} }
else { else {
isRefreshing = true isRefreshing = true
globalThis.localStorage.setItem(LOCAL_STORAGE_KEY, '1') globalThis.localStorage.setItem(LOCAL_STORAGE_KEY, '1')
globalThis.localStorage.setItem('last_refresh_time', new Date().getTime().toString())
globalThis.addEventListener('beforeunload', releaseRefreshLock) globalThis.addEventListener('beforeunload', releaseRefreshLock)
const refresh_token = globalThis.localStorage.getItem('refresh_token') const refresh_token = globalThis.localStorage.getItem('refresh_token')
@@ -72,6 +79,7 @@ function releaseRefreshLock() {
if (isRefreshing) { if (isRefreshing) {
isRefreshing = false isRefreshing = false
globalThis.localStorage.removeItem(LOCAL_STORAGE_KEY) globalThis.localStorage.removeItem(LOCAL_STORAGE_KEY)
globalThis.localStorage.removeItem('last_refresh_time')
globalThis.removeEventListener('beforeunload', releaseRefreshLock) globalThis.removeEventListener('beforeunload', releaseRefreshLock)
} }
} }
@@ -80,5 +88,5 @@ export async function refreshAccessTokenOrRelogin(timeout: number) {
return Promise.race([new Promise<void>((resolve, reject) => setTimeout(() => { return Promise.race([new Promise<void>((resolve, reject) => setTimeout(() => {
releaseRefreshLock() releaseRefreshLock()
reject(new Error('request timeout')) reject(new Error('request timeout'))
}, timeout)), getNewAccessToken()]) }, timeout)), getNewAccessToken(timeout)])
} }

View File

@@ -5568,6 +5568,11 @@ decimal.js@^10.4.2:
resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz" resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz"
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
decimal.js@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
decode-named-character-reference@^1.0.0: decode-named-character-reference@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz" resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz"