Quellcode durchsuchen

Feat: Support re-segmentation (#114)

Co-authored-by: John Wang <takatost@gmail.com>
Co-authored-by: Jyong <718720800@qq.com>
Co-authored-by: 金伟强 <iamjoel007@gmail.com>
tags/0.3.2
KVOJJJin vor 2 Jahren
Ursprung
Commit
c67f626b66
Es ist kein Account mit der E-Mail-Adresse des Committers verbunden
61 geänderte Dateien mit 1169 neuen und 762 gelöschten Zeilen
  1. 7
    4
      api/controllers/console/datasets/datasets_document.py
  2. 138
    39
      api/services/dataset_service.py
  3. 1
    2
      api/tasks/clean_document_task.py
  4. 85
    0
      api/tasks/document_indexing_update_task.py
  5. 1
    2
      api/tasks/remove_document_from_index_task.py
  6. 1
    1
      web/app/(commonLayout)/apps/AppCard.tsx
  7. 5
    6
      web/app/(commonLayout)/apps/Apps.tsx
  8. 16
    0
      web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/[documentId]/settings/page.tsx
  9. 4
    1
      web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx
  10. 5
    8
      web/app/(commonLayout)/datasets/DatasetCard.tsx
  11. 7
    9
      web/app/(commonLayout)/datasets/Datasets.tsx
  12. 2
    2
      web/app/(commonLayout)/explore/apps/page.tsx
  13. 4
    3
      web/app/(commonLayout)/explore/installed/[appId]/page.tsx
  14. 0
    1
      web/app/(shareLayout)/chat/[token]/page.tsx
  15. 11
    11
      web/app/components/app/configuration/config-model/index.tsx
  16. 14
    15
      web/app/components/app/configuration/dataset-config/select-dataset/index.tsx
  17. 39
    35
      web/app/components/app/configuration/features/chat-group/opening-statement/index.tsx
  18. 48
    42
      web/app/components/app/configuration/prompt-value-panel/index.tsx
  19. 112
    106
      web/app/components/app/text-generate/item/index.tsx
  20. 2
    2
      web/app/components/base/app-icon/index.tsx
  21. 33
    32
      web/app/components/base/block-input/index.tsx
  22. 22
    21
      web/app/components/base/emoji-picker/index.tsx
  23. 72
    24
      web/app/components/datasets/create/step-two/index.tsx
  24. 41
    10
      web/app/components/datasets/documents/detail/embedding/index.tsx
  25. 90
    0
      web/app/components/datasets/documents/detail/settings/index.tsx
  26. 14
    9
      web/app/components/datasets/documents/list.tsx
  27. 1
    1
      web/app/components/datasets/documents/style.module.css
  28. 15
    13
      web/app/components/datasets/settings/form/index.tsx
  29. 1
    1
      web/app/components/develop/secret-key/input-copy.tsx
  30. 25
    23
      web/app/components/explore/app-list/index.tsx
  31. 15
    14
      web/app/components/explore/category.tsx
  32. 40
    41
      web/app/components/explore/create-app-modal/index.tsx
  33. 10
    8
      web/app/components/explore/index.tsx
  34. 14
    11
      web/app/components/explore/installed-app/index.tsx
  35. 11
    10
      web/app/components/explore/item-operation/index.tsx
  36. 5
    6
      web/app/components/explore/sidebar/app-nav-item/index.tsx
  37. 30
    32
      web/app/components/header/account-setting/provider-page/azure-provider/index.tsx
  38. 7
    7
      web/app/components/header/account-setting/provider-page/index.tsx
  39. 21
    24
      web/app/components/header/account-setting/provider-page/openai-provider/index.tsx
  40. 7
    7
      web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx
  41. 8
    9
      web/app/components/header/account-setting/provider-page/provider-input/index.tsx
  42. 18
    15
      web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts
  43. 30
    25
      web/app/components/header/account-setting/provider-page/provider-item/index.tsx
  44. 12
    12
      web/app/components/header/index.tsx
  45. 5
    4
      web/app/components/share/chat/sidebar/app-info/index.tsx
  46. 9
    9
      web/app/components/share/chat/sidebar/index.tsx
  47. 21
    19
      web/app/components/share/text-generation/config-scence/index.tsx
  48. 24
    24
      web/config/index.ts
  49. 2
    1
      web/context/dataset-detail.ts
  50. 1
    1
      web/context/explore-context.ts
  51. 1
    1
      web/i18n/lang/app.en.ts
  52. 1
    1
      web/i18n/lang/app.zh.ts
  53. 2
    0
      web/i18n/lang/dataset-creation.en.ts
  54. 2
    0
      web/i18n/lang/dataset-creation.zh.ts
  55. 7
    7
      web/i18n/lang/explore.en.ts
  56. 7
    7
      web/i18n/lang/explore.zh.ts
  57. 2
    2
      web/models/common.ts
  58. 20
    20
      web/models/explore.ts
  59. 6
    6
      web/service/explore.ts
  60. 9
    9
      web/service/share.ts
  61. 6
    7
      web/types/app.ts

+ 7
- 4
api/controllers/console/datasets/datasets_document.py Datei anzeigen

parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('indexing_technique', type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False, parser.add_argument('indexing_technique', type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False,
location='json') location='json')
parser.add_argument('data_source', type=dict, required=True, nullable=True, location='json')
parser.add_argument('process_rule', type=dict, required=True, nullable=True, location='json')
parser.add_argument('data_source', type=dict, required=False, location='json')
parser.add_argument('process_rule', type=dict, required=False, location='json')
parser.add_argument('duplicate', type=bool, nullable=False, location='json') parser.add_argument('duplicate', type=bool, nullable=False, location='json')
parser.add_argument('original_document_id', type=str, required=False, location='json')
args = parser.parse_args() args = parser.parse_args()


if not dataset.indexing_technique and not args['indexing_technique']: if not dataset.indexing_technique and not args['indexing_technique']:


completed_segments = DocumentSegment.query \ completed_segments = DocumentSegment.query \
.filter(DocumentSegment.completed_at.isnot(None), .filter(DocumentSegment.completed_at.isnot(None),
DocumentSegment.document_id == str(document_id)) \
DocumentSegment.document_id == str(document_id),
DocumentSegment.status != 're_segment') \
.count() .count()
total_segments = DocumentSegment.query \ total_segments = DocumentSegment.query \
.filter_by(document_id=str(document_id)) \
.filter(DocumentSegment.document_id == str(document_id),
DocumentSegment.status != 're_segment') \
.count() .count()


document.completed_segments = completed_segments document.completed_segments = completed_segments

+ 138
- 39
api/services/dataset_service.py Datei anzeigen

from events.document_event import document_was_deleted from events.document_event import document_was_deleted
from extensions.ext_database import db from extensions.ext_database import db
from models.account import Account from models.account import Account
from models.dataset import Dataset, Document, DatasetQuery, DatasetProcessRule, AppDatasetJoin
from models.dataset import Dataset, Document, DatasetQuery, DatasetProcessRule, AppDatasetJoin, DocumentSegment
from models.model import UploadFile from models.model import UploadFile
from services.errors.account import NoPermissionError from services.errors.account import NoPermissionError
from services.errors.dataset import DatasetNameDuplicateError from services.errors.dataset import DatasetNameDuplicateError
from services.errors.file import FileNotExistsError from services.errors.file import FileNotExistsError
from tasks.deal_dataset_vector_index_task import deal_dataset_vector_index_task from tasks.deal_dataset_vector_index_task import deal_dataset_vector_index_task
from tasks.document_indexing_task import document_indexing_task from tasks.document_indexing_task import document_indexing_task
from tasks.document_indexing_update_task import document_indexing_update_task




class DatasetService: class DatasetService:


return document return document


@staticmethod
def get_document_by_id(document_id: str) -> Optional[Document]:
document = db.session.query(Document).filter(
Document.id == document_id
).first()

return document

@staticmethod @staticmethod
def get_document_file_detail(file_id: str): def get_document_file_detail(file_id: str):
file_detail = db.session.query(UploadFile). \ file_detail = db.session.query(UploadFile). \
if dataset.indexing_technique == 'high_quality': if dataset.indexing_technique == 'high_quality':
IndexBuilder.get_default_service_context(dataset.tenant_id) IndexBuilder.get_default_service_context(dataset.tenant_id)


if 'original_document_id' in document_data and document_data["original_document_id"]:
document = DocumentService.update_document_with_dataset_id(dataset, document_data, account)
else:
# save process rule
if not dataset_process_rule:
process_rule = document_data["process_rule"]
if process_rule["mode"] == "custom":
dataset_process_rule = DatasetProcessRule(
dataset_id=dataset.id,
mode=process_rule["mode"],
rules=json.dumps(process_rule["rules"]),
created_by=account.id
)
elif process_rule["mode"] == "automatic":
dataset_process_rule = DatasetProcessRule(
dataset_id=dataset.id,
mode=process_rule["mode"],
rules=json.dumps(DatasetProcessRule.AUTOMATIC_RULES),
created_by=account.id
)
db.session.add(dataset_process_rule)
db.session.commit()

file_name = ''
data_source_info = {}
if document_data["data_source"]["type"] == "upload_file":
file_id = document_data["data_source"]["info"]
file = db.session.query(UploadFile).filter(
UploadFile.tenant_id == dataset.tenant_id,
UploadFile.id == file_id
).first()

# raise error if file not found
if not file:
raise FileNotExistsError()

file_name = file.name
data_source_info = {
"upload_file_id": file_id,
}

# save document
position = DocumentService.get_documents_position(dataset.id)
document = Document(
tenant_id=dataset.tenant_id,
dataset_id=dataset.id,
position=position,
data_source_type=document_data["data_source"]["type"],
data_source_info=json.dumps(data_source_info),
dataset_process_rule_id=dataset_process_rule.id,
batch=time.strftime('%Y%m%d%H%M%S') + str(random.randint(100000, 999999)),
name=file_name,
created_from=created_from,
created_by=account.id,
# created_api_request_id = db.Column(UUID, nullable=True)
)

db.session.add(document)
db.session.commit()

# trigger async task
document_indexing_task.delay(document.dataset_id, document.id)
return document

@staticmethod
def update_document_with_dataset_id(dataset: Dataset, document_data: dict,
account: Account, dataset_process_rule: Optional[DatasetProcessRule] = None,
created_from: str = 'web'):
document = DocumentService.get_document(dataset.id, document_data["original_document_id"])
if document.display_status != 'available':
raise ValueError("Document is not available")
# save process rule # save process rule
if not dataset_process_rule:
if 'process_rule' in document_data and document_data['process_rule']:
process_rule = document_data["process_rule"] process_rule = document_data["process_rule"]
if process_rule["mode"] == "custom": if process_rule["mode"] == "custom":
dataset_process_rule = DatasetProcessRule( dataset_process_rule = DatasetProcessRule(
) )
db.session.add(dataset_process_rule) db.session.add(dataset_process_rule)
db.session.commit() db.session.commit()

file_name = ''
data_source_info = {}
if document_data["data_source"]["type"] == "upload_file":
file_id = document_data["data_source"]["info"]
file = db.session.query(UploadFile).filter(
UploadFile.tenant_id == dataset.tenant_id,
UploadFile.id == file_id
).first()

# raise error if file not found
if not file:
raise FileNotExistsError()

file_name = file.name
data_source_info = {
"upload_file_id": file_id,
}

# save document
position = DocumentService.get_documents_position(dataset.id)
document = Document(
tenant_id=dataset.tenant_id,
dataset_id=dataset.id,
position=position,
data_source_type=document_data["data_source"]["type"],
data_source_info=json.dumps(data_source_info),
dataset_process_rule_id=dataset_process_rule.id,
batch=time.strftime('%Y%m%d%H%M%S') + str(random.randint(100000, 999999)),
name=file_name,
created_from=created_from,
created_by=account.id,
# created_api_request_id = db.Column(UUID, nullable=True)
)

document.dataset_process_rule_id = dataset_process_rule.id
# update document data source
if 'data_source' in document_data and document_data['data_source']:
file_name = ''
data_source_info = {}
if document_data["data_source"]["type"] == "upload_file":
file_id = document_data["data_source"]["info"]
file = db.session.query(UploadFile).filter(
UploadFile.tenant_id == dataset.tenant_id,
UploadFile.id == file_id
).first()

# raise error if file not found
if not file:
raise FileNotExistsError()

file_name = file.name
data_source_info = {
"upload_file_id": file_id,
}
document.data_source_type = document_data["data_source"]["type"]
document.data_source_info = json.dumps(data_source_info)
document.name = file_name
# update document to be waiting
document.indexing_status = 'waiting'
document.completed_at = None
document.processing_started_at = None
document.parsing_completed_at = None
document.cleaning_completed_at = None
document.splitting_completed_at = None
document.updated_at = datetime.datetime.utcnow()
document.created_from = created_from
db.session.add(document) db.session.add(document)
db.session.commit() db.session.commit()

# update document segment
update_params = {
DocumentSegment.status: 're_segment'
}
DocumentSegment.query.filter_by(document_id=document.id).update(update_params)
db.session.commit()
# trigger async task # trigger async task
document_indexing_task.delay(document.dataset_id, document.id)
document_indexing_update_task.delay(document.dataset_id, document.id)


return document return document




@classmethod @classmethod
def document_create_args_validate(cls, args: dict): def document_create_args_validate(cls, args: dict):
if 'original_document_id' not in args or not args['original_document_id']:
DocumentService.data_source_args_validate(args)
DocumentService.process_rule_args_validate(args)
else:
if ('data_source' not in args and not args['data_source'])\
and ('process_rule' not in args and not args['process_rule']):
raise ValueError("Data source or Process rule is required")
else:
if 'data_source' in args and args['data_source']:
DocumentService.data_source_args_validate(args)
if 'process_rule' in args and args['process_rule']:
DocumentService.process_rule_args_validate(args)

@classmethod
def data_source_args_validate(cls, args: dict):
if 'data_source' not in args or not args['data_source']: if 'data_source' not in args or not args['data_source']:
raise ValueError("Data source is required") raise ValueError("Data source is required")


if 'info' not in args['data_source'] or not args['data_source']['info']: if 'info' not in args['data_source'] or not args['data_source']['info']:
raise ValueError("Data source info is required") raise ValueError("Data source info is required")


@classmethod
def process_rule_args_validate(cls, args: dict):
if 'process_rule' not in args or not args['process_rule']: if 'process_rule' not in args or not args['process_rule']:
raise ValueError("Process rule is required") raise ValueError("Process rule is required")



+ 1
- 2
api/tasks/clean_document_task.py Datei anzeigen

index_node_ids = [segment.index_node_id for segment in segments] index_node_ids = [segment.index_node_id for segment in segments]


# delete from vector index # delete from vector index
if dataset.indexing_technique == "high_quality":
vector_index.del_nodes(index_node_ids)
vector_index.del_nodes(index_node_ids)


# delete from keyword index # delete from keyword index
if index_node_ids: if index_node_ids:

+ 85
- 0
api/tasks/document_indexing_update_task.py Datei anzeigen

import datetime
import logging
import time

import click
from celery import shared_task
from werkzeug.exceptions import NotFound

from core.index.keyword_table_index import KeywordTableIndex
from core.index.vector_index import VectorIndex
from core.indexing_runner import IndexingRunner, DocumentIsPausedException
from core.llm.error import ProviderTokenNotInitError
from extensions.ext_database import db
from models.dataset import Document, Dataset, DocumentSegment


@shared_task
def document_indexing_update_task(dataset_id: str, document_id: str):
"""
Async update document
:param dataset_id:
:param document_id:

Usage: document_indexing_update_task.delay(dataset_id, document_id)
"""
logging.info(click.style('Start update document: {}'.format(document_id), fg='green'))
start_at = time.perf_counter()

document = db.session.query(Document).filter(
Document.id == document_id,
Document.dataset_id == dataset_id
).first()

if not document:
raise NotFound('Document not found')

document.indexing_status = 'parsing'
document.processing_started_at = datetime.datetime.utcnow()
db.session.commit()

# delete all document segment and index
try:
dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id).first()
if not dataset:
raise Exception('Dataset not found')

vector_index = VectorIndex(dataset=dataset)
keyword_table_index = KeywordTableIndex(dataset=dataset)

segments = db.session.query(DocumentSegment).filter(DocumentSegment.document_id == document_id).all()
index_node_ids = [segment.index_node_id for segment in segments]

# delete from vector index
vector_index.del_nodes(index_node_ids)

# delete from keyword index
if index_node_ids:
keyword_table_index.del_nodes(index_node_ids)

for segment in segments:
db.session.delete(segment)

end_at = time.perf_counter()
logging.info(
click.style('Cleaned document when document update data source or process rule: {} latency: {}'.format(document_id, end_at - start_at), fg='green'))
except Exception:
logging.exception("Cleaned document when document update data source or process rule failed")
try:
indexing_runner = IndexingRunner()
indexing_runner.run(document)
end_at = time.perf_counter()
logging.info(click.style('update document: {} latency: {}'.format(document.id, end_at - start_at), fg='green'))
except DocumentIsPausedException:
logging.info(click.style('Document update paused, document id: {}'.format(document.id), fg='yellow'))
except ProviderTokenNotInitError as e:
document.indexing_status = 'error'
document.error = str(e.description)
document.stopped_at = datetime.datetime.utcnow()
db.session.commit()
except Exception as e:
logging.exception("consume update document failed")
document.indexing_status = 'error'
document.error = str(e)
document.stopped_at = datetime.datetime.utcnow()
db.session.commit()

+ 1
- 2
api/tasks/remove_document_from_index_task.py Datei anzeigen

keyword_table_index = KeywordTableIndex(dataset=dataset) keyword_table_index = KeywordTableIndex(dataset=dataset)


# delete from vector index # delete from vector index
if dataset.indexing_technique == "high_quality":
vector_index.del_doc(document.id)
vector_index.del_doc(document.id)


# delete from keyword index # delete from keyword index
segments = db.session.query(DocumentSegment).filter(DocumentSegment.document_id == document.id).all() segments = db.session.query(DocumentSegment).filter(DocumentSegment.document_id == document.id).all()

+ 1
- 1
web/app/(commonLayout)/apps/AppCard.tsx Datei anzeigen



const AppCard = ({ const AppCard = ({
app, app,
onDelete
onDelete,
}: AppCardProps) => { }: AppCardProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)

+ 5
- 6
web/app/(commonLayout)/apps/Apps.tsx Datei anzeigen

import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import useSWRInfinite from 'swr/infinite' import useSWRInfinite from 'swr/infinite'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import AppCard from './AppCard' import AppCard from './AppCard'
import NewAppCard from './NewAppCard' import NewAppCard from './NewAppCard'
import { AppListResponse } from '@/models/app'
import type { AppListResponse } from '@/models/app'
import { fetchAppList } from '@/service/apps' import { fetchAppList } from '@/service/apps'
import { useSelector } from '@/context/app-context' import { useSelector } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useTranslation } from 'react-i18next'


const getKey = (pageIndex: number, previousPageData: AppListResponse) => { const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
if (!pageIndex || previousPageData.has_more) if (!pageIndex || previousPageData.has_more)
const anchorRef = useRef<HTMLAnchorElement>(null) const anchorRef = useRef<HTMLAnchorElement>(null)


useEffect(() => { useEffect(() => {
document.title = `${t('app.title')} - Dify`;
if(localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
document.title = `${t('app.title')} - Dify`
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate() mutate()
} }
if (!loadingStateRef.current) { if (!loadingStateRef.current) {
const { scrollTop, clientHeight } = pageContainerRef.current! const { scrollTop, clientHeight } = pageContainerRef.current!
const anchorOffset = anchorRef.current!.offsetTop const anchorOffset = anchorRef.current!.offsetTop
if (anchorOffset - scrollTop - clientHeight < 100) {
if (anchorOffset - scrollTop - clientHeight < 100)
setSize(size => size + 1) setSize(size => size + 1)
}
} }
}, 50) }, 50)



+ 16
- 0
web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/[documentId]/settings/page.tsx Datei anzeigen

import React from 'react'
import Settings from '@/app/components/datasets/documents/detail/settings'

export type IProps = {
params: { datasetId: string; documentId: string }
}

const DocumentSettings = async ({
params: { datasetId, documentId },
}: IProps) => {
return (
<Settings datasetId={datasetId} documentId={documentId} />
)
}

export default DocumentSettings

+ 4
- 1
web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx Datei anzeigen

extraInfo={<ExtraInfo />} extraInfo={<ExtraInfo />}
iconType='dataset' iconType='dataset'
/>} />}
<DatasetDetailContext.Provider value={{ indexingTechnique: datasetRes?.indexing_technique }}>
<DatasetDetailContext.Provider value={{
indexingTechnique: datasetRes?.indexing_technique,
dataset: datasetRes,
}}>
<div className="bg-white grow">{children}</div> <div className="bg-white grow">{children}</div>
</DatasetDetailContext.Provider> </DatasetDetailContext.Provider>
</div> </div>

+ 5
- 8
web/app/(commonLayout)/datasets/DatasetCard.tsx Datei anzeigen

'use client' 'use client'


import { useContext, useContextSelector } from 'use-context-selector'
import { useContext } from 'use-context-selector'
import Link from 'next/link' import Link from 'next/link'
import useSWR from 'swr'
import type { MouseEventHandler } from 'react' import type { MouseEventHandler } from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import style from '../list.module.css' import style from '../list.module.css'
import type { App } from '@/types/app'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import { deleteDataset, fetchDatasets } from '@/service/datasets'
import { deleteDataset } from '@/service/datasets'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import AppsContext from '@/context/app-context'
import { DataSet } from '@/models/datasets'
import classNames from 'classnames'
import type { DataSet } from '@/models/datasets'


export type DatasetCardProps = { export type DatasetCardProps = {
dataset: DataSet dataset: DataSet


const DatasetCard = ({ const DatasetCard = ({
dataset, dataset,
onDelete
onDelete,
}: DatasetCardProps) => { }: DatasetCardProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)

+ 7
- 9
web/app/(commonLayout)/datasets/Datasets.tsx Datei anzeigen



import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import useSWRInfinite from 'swr/infinite' import useSWRInfinite from 'swr/infinite'
import { debounce } from 'lodash-es';
import { DataSetListResponse } from '@/models/datasets';
import { debounce } from 'lodash-es'
import NewDatasetCard from './NewDatasetCard' import NewDatasetCard from './NewDatasetCard'
import DatasetCard from './DatasetCard';
import { fetchDatasets } from '@/service/datasets';
import { useSelector } from '@/context/app-context';
import DatasetCard from './DatasetCard'
import type { DataSetListResponse } from '@/models/datasets'
import { fetchDatasets } from '@/service/datasets'
import { useSelector } from '@/context/app-context'


const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
if (!pageIndex || previousPageData.has_more) if (!pageIndex || previousPageData.has_more)
if (!loadingStateRef.current) { if (!loadingStateRef.current) {
const { scrollTop, clientHeight } = pageContainerRef.current! const { scrollTop, clientHeight } = pageContainerRef.current!
const anchorOffset = anchorRef.current!.offsetTop const anchorOffset = anchorRef.current!.offsetTop
if (anchorOffset - scrollTop - clientHeight < 100) {
if (anchorOffset - scrollTop - clientHeight < 100)
setSize(size => size + 1) setSize(size => size + 1)
}
} }
}, 50) }, 50)


return ( return (
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 lg:grid-cols-4 grow shrink-0'> <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 lg:grid-cols-4 grow shrink-0'>
{data?.map(({ data: datasets }) => datasets.map(dataset => ( {data?.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onDelete={mutate} />)
<DatasetCard key={dataset.id} dataset={dataset} onDelete={mutate} />),
))} ))}
<NewDatasetCard ref={anchorRef} /> <NewDatasetCard ref={anchorRef} />
</nav> </nav>
} }


export default Datasets export default Datasets


+ 2
- 2
web/app/(commonLayout)/explore/apps/page.tsx Datei anzeigen

import AppList from "@/app/components/explore/app-list"
import React from 'react' import React from 'react'
import AppList from '@/app/components/explore/app-list'


const Apps = ({ }) => {
const Apps = () => {
return <AppList /> return <AppList />
} }



+ 4
- 3
web/app/(commonLayout)/explore/installed/[appId]/page.tsx Datei anzeigen

import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import Main from '@/app/components/explore/installed-app' import Main from '@/app/components/explore/installed-app'


export interface IInstalledAppProps {
export type IInstalledAppProps = {
params: { params: {
appId: string appId: string
} }
} }


const InstalledApp: FC<IInstalledAppProps> = ({ params: {appId} }) => {
const InstalledApp: FC<IInstalledAppProps> = ({ params: { appId } }) => {
return ( return (
<Main id={appId} /> <Main id={appId} />
) )

+ 0
- 1
web/app/(shareLayout)/chat/[token]/page.tsx Datei anzeigen

import Main from '@/app/components/share/chat' import Main from '@/app/components/share/chat'


const Chat: FC<IMainProps> = () => { const Chat: FC<IMainProps> = () => {

return ( return (
<Main /> <Main />
) )

+ 11
- 11
web/app/components/app/configuration/config-model/index.tsx Datei anzeigen

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useEffect } from 'react'
import cn from 'classnames' import cn from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean, useClickAway } from 'ahooks' import { useBoolean, useClickAway } from 'ahooks'
import { ChevronDownIcon, Cog8ToothIcon, InformationCircleIcon } from '@heroicons/react/24/outline'
import ParamItem from './param-item' import ParamItem from './param-item'
import Radio from '@/app/components/base/radio' import Radio from '@/app/components/base/radio'
import Panel from '@/app/components/base/panel' import Panel from '@/app/components/base/panel'
import type { CompletionParams } from '@/models/debug' import type { CompletionParams } from '@/models/debug'
import { Cog8ToothIcon, InformationCircleIcon, ChevronDownIcon } from '@heroicons/react/24/outline'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
import { TONE_LIST } from '@/config' import { TONE_LIST } from '@/config'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const isChatApp = mode === AppType.chat const isChatApp = mode === AppType.chat
const availableModels = options.filter((item) => item.type === mode)
const availableModels = options.filter(item => item.type === mode)
const [isShowConfig, { setFalse: hideConfig, toggle: toogleShowConfig }] = useBoolean(false) const [isShowConfig, { setFalse: hideConfig, toggle: toogleShowConfig }] = useBoolean(false)
const configContentRef = React.useRef(null) const configContentRef = React.useRef(null)
useClickAway(() => { useClickAway(() => {
onShowUseGPT4Confirm() onShowUseGPT4Confirm()
return return
} }
if(id !== 'gpt-4' && completionParams.max_tokens > 4000) {
if (id !== 'gpt-4' && completionParams.max_tokens > 4000) {
Toast.notify({ Toast.notify({
type: 'warning', type: 'warning',
message: t('common.model.params.setToCurrentModelMaxTokenTip')
message: t('common.model.params.setToCurrentModelMaxTokenTip'),
}) })
onCompletionParamsChange({ onCompletionParamsChange({
...completionParams, ...completionParams,
max_tokens: 4000
max_tokens: 4000,
}) })
} }
setModelId(id) setModelId(id)
setToneId(id) setToneId(id)
onCompletionParamsChange({ onCompletionParamsChange({
...tone.config, ...tone.config,
max_tokens: completionParams.max_tokens
max_tokens: completionParams.max_tokens,
} as CompletionParams) } as CompletionParams)
} }
} }
return ( return (
<div className='relative' ref={configContentRef}> <div className='relative' ref={configContentRef}>
<div <div
className={cn(`flex items-center border h-8 px-2.5 space-x-2 rounded-lg`, disabled ? diabledStyle : ableStyle)}
className={cn('flex items-center border h-8 px-2.5 space-x-2 rounded-lg', disabled ? diabledStyle : ableStyle)}
onClick={() => !disabled && toogleShowConfig()} onClick={() => !disabled && toogleShowConfig()}
> >
<ModelIcon /> <ModelIcon />
<div className="flex items-center justify-between my-5 h-9"> <div className="flex items-center justify-between my-5 h-9">
<div>{t('appDebug.modelConfig.model')}</div> <div>{t('appDebug.modelConfig.model')}</div>
{/* model selector */} {/* model selector */}
<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 ")}>
<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 /> <ModelIcon />
<div className="text-sm gray-900">{selectedModel?.name}</div> <div className="text-sm gray-900">{selectedModel?.name}</div>
{!selectModelDisabled && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />} {!selectModelDisabled && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />}
</div> </div>
{isShowOption && ( {isShowOption && (
<div className={cn(isChatApp ? 'w-[159px]' : 'w-[179px]', "absolute right-0 bg-gray-50 rounded-lg shadow")}>
<div className={cn(isChatApp ? 'w-[159px]' : 'w-[179px]', 'absolute right-0 bg-gray-50 rounded-lg shadow')}>
{availableModels.map(item => ( {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"> <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' /> <ModelIcon className='mr-2' />

+ 14
- 15
web/app/components/app/configuration/dataset-config/select-dataset/index.tsx Datei anzeigen

'use client' 'use client'
import React, { FC, useEffect } from 'react'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import cn from 'classnames' import cn from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { DataSet } from '@/models/datasets'
import Link from 'next/link'
import TypeIcon from '../type-icon' import TypeIcon from '../type-icon'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
import type { DataSet } from '@/models/datasets'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { fetchDatasets } from '@/service/datasets' import { fetchDatasets } from '@/service/datasets'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { formatNumber } from '@/utils/format' import { formatNumber } from '@/utils/format'
import Link from 'next/link'

import s from './style.module.css'


export interface ISelectDataSetProps {
export type ISelectDataSetProps = {
isShow: boolean isShow: boolean
onClose: () => void onClose: () => void
selectedIds: string[] selectedIds: string[]
const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } }) const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } })
setDataSets(data) setDataSets(data)
setLoaded(true) setLoaded(true)
setSelected(data.filter((item) => selectedIds.includes(item.id)))
setSelected(data.filter(item => selectedIds.includes(item.id)))
})() })()
}, []) }, [])
const toggleSelect = (dataSet: DataSet) => { const toggleSelect = (dataSet: DataSet) => {
const isSelected = selected.some((item) => item.id === dataSet.id)
const isSelected = selected.some(item => item.id === dataSet.id)
if (isSelected) { if (isSelected) {
setSelected(selected.filter((item) => item.id !== dataSet.id))
setSelected(selected.filter(item => item.id !== dataSet.id))
} }
else { else {
if (canSelectMulti) {
if (canSelectMulti)
setSelected([...selected, dataSet]) setSelected([...selected, dataSet])
} else {
else
setSelected([dataSet]) setSelected([dataSet])
}
} }
} }


<div className='flex items-center justify-center mt-6 rounded-lg space-x-1 h-[128px] text-[13px] border' <div className='flex items-center justify-center mt-6 rounded-lg space-x-1 h-[128px] text-[13px] border'
style={{ style={{
background: 'rgba(0, 0, 0, 0.02)', background: 'rgba(0, 0, 0, 0.02)',
borderColor: 'rgba(0, 0, 0, 0.02'
borderColor: 'rgba(0, 0, 0, 0.02',
}} }}
> >
<span className='text-gray-500'>{t('appDebug.feature.dataSet.noDataSet')}</span> <span className='text-gray-500'>{t('appDebug.feature.dataSet.noDataSet')}</span>
{datasets && datasets?.length > 0 && ( {datasets && datasets?.length > 0 && (
<> <>
<div className='mt-7 space-y-1 max-h-[286px] overflow-y-auto'> <div className='mt-7 space-y-1 max-h-[286px] overflow-y-auto'>
{datasets.map((item) => (
{datasets.map(item => (
<div <div
key={item.id} key={item.id}
className={cn(s.item, selected.some(i => i.id === item.id) && s.selected, 'flex justify-between items-center h-10 px-2 rounded-lg bg-white border border-gray-200 cursor-pointer')} className={cn(s.item, selected.some(i => i.id === item.id) && s.selected, 'flex justify-between items-center h-10 px-2 rounded-lg bg-white border border-gray-200 cursor-pointer')}

+ 39
- 35
web/app/components/app/configuration/features/chat-group/opening-statement/index.tsx Datei anzeigen

/* eslint-disable multiline-ternary */
'use client' 'use client'
import React, { FC, useEffect, useRef, useState } from 'react'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import cn from 'classnames' import cn from 'classnames'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import ConfigContext from '@/context/debug-configuration'
import produce from 'immer' import produce from 'immer'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import ConfigContext from '@/context/debug-configuration'
import Panel from '@/app/components/app/configuration/base/feature-panel' import Panel from '@/app/components/app/configuration/base/feature-panel'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import OperationBtn from '@/app/components/app/configuration/base/operation-btn' import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
import { getNewVar } from '@/utils/var' import { getNewVar } from '@/utils/var'
import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight' import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight'


export interface IOpeningStatementProps {
export type IOpeningStatementProps = {
promptTemplate: string promptTemplate: string
value: string value: string
onChange: (value: string) => void onChange: (value: string) => void


const OpeningStatement: FC<IOpeningStatementProps> = ({ const OpeningStatement: FC<IOpeningStatementProps> = ({
value = '', value = '',
onChange
onChange,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>` .replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />') .replace(/\n/g, '<br />')



const handleEdit = () => { const handleEdit = () => {
setFocus() setFocus()


const handleConfirm = () => { const handleConfirm = () => {
const keys = getInputKeys(tempValue) const keys = getInputKeys(tempValue)
const promptKeys = promptVariables.map((item) => item.key)
const promptKeys = promptVariables.map(item => item.key)
let notIncludeKeys: string[] = [] let notIncludeKeys: string[] = []


if (promptKeys.length === 0) { if (promptKeys.length === 0) {
if (keys.length > 0) {
if (keys.length > 0)
notIncludeKeys = keys notIncludeKeys = keys
}
} else {
notIncludeKeys = keys.filter((key) => !promptKeys.includes(key))
}
else {
notIncludeKeys = keys.filter(key => !promptKeys.includes(key))
} }


if (notIncludeKeys.length > 0) { if (notIncludeKeys.length > 0) {


const autoAddVar = () => { const autoAddVar = () => {
const newModelConfig = produce(modelConfig, (draft) => { const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...notIncludeKeys.map((key) => getNewVar(key))]
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...notIncludeKeys.map(key => getNewVar(key))]
}) })
onChange(tempValue) onChange(tempValue)
setModelConfig(newModelConfig) setModelConfig(newModelConfig)
isFocus={isFocus} isFocus={isFocus}
> >
<div className='text-gray-700 text-sm'> <div className='text-gray-700 text-sm'>
{(hasValue || (!hasValue && isFocus)) ? (
<>
{isFocus ? (
<textarea
ref={inputRef}
value={tempValue}
rows={3}
onChange={e => setTempValue(e.target.value)}
className="w-full px-0 text-sm border-0 bg-transparent focus:outline-none "
placeholder={t('appDebug.openingStatement.placeholder') as string}
>
</textarea>
) : (
<div dangerouslySetInnerHTML={{
__html: coloredContent
}}></div>
)}

{/* Operation Bar */}
{isFocus && (
{(hasValue || (!hasValue && isFocus))
? (
<>
{isFocus
? (
<textarea
ref={inputRef}
value={tempValue}
rows={3}
onChange={e => setTempValue(e.target.value)}
className="w-full px-0 text-sm border-0 bg-transparent focus:outline-none "
placeholder={t('appDebug.openingStatement.placeholder') as string}
>
</textarea>
)
: (
<div dangerouslySetInnerHTML={{
__html: coloredContent,
}}></div>
)}

{/* Operation Bar */}
{isFocus
&& (
<div className='mt-2 flex items-center justify-between'> <div className='mt-2 flex items-center justify-between'>
<div className='text-xs text-gray-500'>{t('appDebug.openingStatement.varTip')}</div> <div className='text-xs text-gray-500'>{t('appDebug.openingStatement.varTip')}</div>


</div> </div>
)} )}


</>) : (
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.openingStatement.noDataPlaceHolder')}</div>
)}
</>) : (
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.openingStatement.noDataPlaceHolder')}</div>
)}


{isShowConfirmAddVar && ( {isShowConfirmAddVar && (
<ConfirmAddVar <ConfirmAddVar

+ 48
- 42
web/app/components/app/configuration/prompt-value-panel/index.tsx Datei anzeigen

import { import {
PlayIcon, PlayIcon,
} from '@heroicons/react/24/solid' } from '@heroicons/react/24/solid'
import VarIcon from '../base/icons/var-icon'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import type { PromptVariable } from '@/models/debug' import type { PromptVariable } from '@/models/debug'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
import Select from '@/app/components/base/select' import Select from '@/app/components/base/select'
import { DEFAULT_VALUE_MAX_LEN } from '@/config' import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import VarIcon from '../base/icons/var-icon'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'


export type IPromptValuePanelProps = { export type IPromptValuePanelProps = {
</div> </div>
<div className='mt-2 leading-normal'> <div className='mt-2 leading-normal'>
{ {
(promptTemplate && promptTemplate?.trim()) ? (
<div
className="max-h-48 overflow-y-auto text-sm text-gray-700 break-all"
dangerouslySetInnerHTML={{
__html: format(replaceStringWithValuesWithFormat(promptTemplate.replace(/</g, '&lt;').replace(/>/g, '&gt;'), promptVariables, inputs)),
}}
>
</div>
) : (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noPrompt')}</div>
)
(promptTemplate && promptTemplate?.trim())
? (
<div
className="max-h-48 overflow-y-auto text-sm text-gray-700 break-all"
dangerouslySetInnerHTML={{
__html: format(replaceStringWithValuesWithFormat(promptTemplate.replace(/</g, '&lt;').replace(/>/g, '&gt;'), promptVariables, inputs)),
}}
>
</div>
)
: (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noPrompt')}</div>
)
} }
</div> </div>
</div> </div>
)} )}
</div> </div>
{ {
promptVariables.length > 0 ? (
<div className="space-y-3 ">
{promptVariables.map(({ key, name, type, options, max_length, required }) => (
<div key={key} className="flex items-center justify-between">
<div className="mr-1 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
{type === 'select' ? (
<Select
className='w-full'
defaultValue={inputs[key] as string}
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
items={(options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
) : (
<input
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
type="text"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
promptVariables.length > 0
? (
<div className="space-y-3 ">
{promptVariables.map(({ key, name, type, options, max_length, required }) => (
<div key={key} className="flex items-center justify-between">
<div className="mr-1 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
{type === 'select'
? (
<Select
className='w-full'
defaultValue={inputs[key] as string}
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
items={(options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
)
: (
<input
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
type="text"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}


</div>
))}
</div>
) : (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
)
</div>
))}
</div>
)
: (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
)
} }
</div> </div>



+ 112
- 106
web/app/components/app/text-generate/item/index.tsx Datei anzeigen

'use client' 'use client'
import React, { FC, useState } from 'react'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from 'classnames' import cn from 'classnames'
import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import Toast from '@/app/components/base/toast'
import { Feedbacktype } from '@/app/components/app/chat'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline' import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import type { Feedbacktype } from '@/app/components/app/chat'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share' import { fetchMoreLikeThis, updateFeedback } from '@/service/share'


const MAX_DEPTH = 3 const MAX_DEPTH = 3
export interface IGenerationItemProps {
export type IGenerationItemProps = {
className?: string className?: string
content: string content: string
messageId?: string | null messageId?: string | null
onFeedback?: (feedback: Feedbacktype) => void onFeedback?: (feedback: Feedbacktype) => void
onSave?: (messageId: string) => void onSave?: (messageId: string) => void
isMobile?: boolean isMobile?: boolean
isInstalledApp: boolean,
installedAppId?: string,
isInstalledApp: boolean
installedAppId?: string
} }


export const SimpleBtn = ({ className, onClick, children }: { export const SimpleBtn = ({ className, onClick, children }: {
className?: string className?: string
onClick?: () => void,
onClick?: () => void
children: React.ReactNode children: React.ReactNode
}) => ( }) => (
<div <div
const [childMessageId, setChildMessageId] = useState<string | null>(null) const [childMessageId, setChildMessageId] = useState<string | null>(null)
const hasChild = !!childMessageId const hasChild = !!childMessageId
const [childFeedback, setChildFeedback] = useState<Feedbacktype>({ const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
rating: null
rating: null,
}) })


const handleFeedback = async (childFeedback: Feedbacktype) => { const handleFeedback = async (childFeedback: Feedbacktype) => {
} }


const mainStyle = (() => { const mainStyle = (() => {
const res: any = !isTop ? {
background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff'
} : {}
const res: any = !isTop
? {
background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff',
}
: {}


if (hasChild) {
if (hasChild)
res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)' res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)'
}
return res return res
})() })()
return ( return (
<div className={cn(className, isTop ? 'rounded-xl border border-gray-200 bg-white' : 'rounded-br-xl !mt-0')} <div className={cn(className, isTop ? 'rounded-xl border border-gray-200 bg-white' : 'rounded-br-xl !mt-0')}
style={isTop ? {
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'
} : {}}
style={isTop
? {
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
}
: {}}
> >
{isLoading ? (
<div className='flex items-center h-10'><Loading type='area' /></div>
) : (
<div
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4')}
style={mainStyle}
>
<Markdown content={content} />
{messageId && (
<div className='flex items-center justify-between mt-3'>
<div className='flex items-center'>
<SimpleBtn
className={cn(isMobile && '!px-1.5', 'space-x-1')}
onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
{copyIcon}
{!isMobile && <div>{t('common.operation.copy')}</div>}
</SimpleBtn>
{isInWebApp && (
<>
<SimpleBtn
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={() => { onSave?.(messageId as string) }}
>
{saveIcon}
{!isMobile && <div>{t('common.operation.save')}</div>}
</SimpleBtn>
{(moreLikeThis && depth < MAX_DEPTH) && (
{isLoading
? (
<div className='flex items-center h-10'><Loading type='area' /></div>
)
: (
<div
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4')}
style={mainStyle}
>
<Markdown content={content} />
{messageId && (
<div className='flex items-center justify-between mt-3'>
<div className='flex items-center'>
<SimpleBtn
className={cn(isMobile && '!px-1.5', 'space-x-1')}
onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
{copyIcon}
{!isMobile && <div>{t('common.operation.copy')}</div>}
</SimpleBtn>
{isInWebApp && (
<>
<SimpleBtn <SimpleBtn
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')} className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={handleMoreLikeThis}
onClick={() => { onSave?.(messageId as string) }}
> >
{moreLikeThisIcon}
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
</SimpleBtn>)}
<div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
{!feedback?.rating && (
<SimpleBtn className="!px-0">
<>
<div
onClick={() => {
onFeedback?.({
rating: 'like'
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbUpIcon width={16} height={16} />
</div>
<div
onClick={() => {
onFeedback?.({
rating: 'dislike'
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbDownIcon width={16} height={16} />
</div>
</>
{saveIcon}
{!isMobile && <div>{t('common.operation.save')}</div>}
</SimpleBtn> </SimpleBtn>
)}
{feedback?.rating === 'like' && (
<div
onClick={() => {
onFeedback?.({
rating: null
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
<HandThumbUpIcon width={16} height={16} />
</div>
)}
{feedback?.rating === 'dislike' && (
<div
onClick={() => {
onFeedback?.({
rating: null
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
<HandThumbDownIcon width={16} height={16} />
</div>
)}
</>
)}
{(moreLikeThis && depth < MAX_DEPTH) && (
<SimpleBtn
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={handleMoreLikeThis}
>
{moreLikeThisIcon}
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
</SimpleBtn>)}
<div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
{!feedback?.rating && (
<SimpleBtn className="!px-0">
<>
<div
onClick={() => {
onFeedback?.({
rating: 'like',
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbUpIcon width={16} height={16} />
</div>
<div
onClick={() => {
onFeedback?.({
rating: 'dislike',
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbDownIcon width={16} height={16} />
</div>
</>
</SimpleBtn>
)}
{feedback?.rating === 'like' && (
<div
onClick={() => {
onFeedback?.({
rating: null,
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
<HandThumbUpIcon width={16} height={16} />
</div>
)}
{feedback?.rating === 'dislike' && (
<div
onClick={() => {
onFeedback?.({
rating: null,
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
<HandThumbDownIcon width={16} height={16} />
</div>
)}
</>
)}
</div>
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
</div> </div>
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
</div>
)}

</div>
)}
)}


</div>
)}


{((childMessageId || isQuerying) && depth < 3) && ( {((childMessageId || isQuerying) && depth < 3) && (
<div className='pl-4'> <div className='pl-4'>

+ 2
- 2
web/app/components/base/app-icon/index.tsx Datei anzeigen

import type { FC } from 'react' import type { FC } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import style from './style.module.css'


import data from '@emoji-mart/data' import data from '@emoji-mart/data'
import { init } from 'emoji-mart' import { init } from 'emoji-mart'
import style from './style.module.css'


init({ data }) init({ data })


}} }}
onClick={onClick} onClick={onClick}
> >
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id='🤖' />}
{innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />)}
</span> </span>
) )
} }

+ 33
- 32
web/app/components/base/block-input/index.tsx Datei anzeigen

import type { ChangeEvent, FC } from 'react' import type { ChangeEvent, FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { checkKeys } from '@/utils/var'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Toast from '../toast' import Toast from '../toast'
import { varHighlightHTML } from '../../app/configuration/base/var-highlight' import { varHighlightHTML } from '../../app/configuration/base/var-highlight'
import Button from '@/app/components/base/button'
import { checkKeys } from '@/utils/var'


// regex to match the {{}} and replace it with a span // regex to match the {{}} and replace it with a span
const regex = /\{\{([^}]+)\}\}/g const regex = /\{\{([^}]+)\}\}/g
useEffect(() => { useEffect(() => {
if (isEditing && contentEditableRef.current) { if (isEditing && contentEditableRef.current) {
// TODO: Focus at the click positon // TODO: Focus at the click positon
if (currentValue) {
if (currentValue)
contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length) contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length)
}
contentEditableRef.current.focus() contentEditableRef.current.focus()
} }
}, [isEditing]) }, [isEditing])
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>` .replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />') .replace(/\n/g, '<br />')


// Not use useCallback. That will cause out callback get old data. // Not use useCallback. That will cause out callback get old data.
const handleSubmit = () => { const handleSubmit = () => {
if (!isValid) { if (!isValid) {
Toast.notify({ Toast.notify({
type: 'error', type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey })
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
}) })
return return
} }
value={currentValue} value={currentValue}
onBlur={() => { onBlur={() => {
blur() blur()
if (!isContentChanged) {
if (!isContentChanged)
setIsEditing(false) setIsEditing(false)
}
// click confirm also make blur. Then outter value is change. So below code has problem. // click confirm also make blur. Then outter value is change. So below code has problem.
// setTimeout(() => { // setTimeout(() => {
// handleCancel() // handleCancel()
{textAreaContent} {textAreaContent}
{/* footer */} {/* footer */}
<div className='flex item-center h-14 px-4'> <div className='flex item-center h-14 px-4'>
{isContentChanged ? (
<div className='flex items-center justify-between w-full'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue.length}</div>
<div className='flex space-x-2'>
<Button
onClick={handleCancel}
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.cancel')}
</Button>
<Button
onClick={handleSubmit}
type="primary"
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.confirm')}
</Button>
</div>
{isContentChanged
? (
<div className='flex items-center justify-between w-full'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue.length}</div>
<div className='flex space-x-2'>
<Button
onClick={handleCancel}
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.cancel')}
</Button>
<Button
onClick={handleSubmit}
type="primary"
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.confirm')}
</Button>
</div>


</div>
) : (
<p className="leading-5 text-xs text-gray-500">
{t('appDebug.promptTip')}
</p>
)}
</div>
)
: (
<p className="leading-5 text-xs text-gray-500">
{t('appDebug.promptTip')}
</p>
)}
</div> </div>


</div> </div>

+ 22
- 21
web/app/components/base/emoji-picker/index.tsx Datei anzeigen

/* eslint-disable multiline-ternary */
'use client' 'use client'
import React from 'react'
import { useState, FC, ChangeEvent } from 'react'
import type { ChangeEvent, FC } from 'react'
import React, { useState } from 'react'
import data from '@emoji-mart/data' import data from '@emoji-mart/data'
import { init, SearchIndex } from 'emoji-mart'
import { SearchIndex, init } from 'emoji-mart'
import cn from 'classnames' import cn from 'classnames'
import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'
import s from './style.module.css'
import { import {
MagnifyingGlassIcon
MagnifyingGlassIcon,
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'


import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import { useTranslation } from 'react-i18next'


declare global { declare global {
namespace JSX { namespace JSX {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IntrinsicElements { interface IntrinsicElements {
'em-emoji': React.DetailedHTMLProps< 'em-emoji': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
>;
React.HTMLAttributes<HTMLElement>,
HTMLElement
>
} }
} }
} }
'#ECE9FE', '#ECE9FE',
'#FFE4E8', '#FFE4E8',
] ]
interface IEmojiPickerProps {
type IEmojiPickerProps = {
isModal?: boolean isModal?: boolean
onSelect?: (emoji: string, background: string) => void onSelect?: (emoji: string, background: string) => void
onClose?: () => void onClose?: () => void
const EmojiPicker: FC<IEmojiPickerProps> = ({ const EmojiPicker: FC<IEmojiPickerProps> = ({
isModal = true, isModal = true,
onSelect, onSelect,
onClose
onClose,


}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
onChange={async (e: ChangeEvent<HTMLInputElement>) => { onChange={async (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.value === '') { if (e.target.value === '') {
setIsSearching(false) setIsSearching(false)
return
} else {
}
else {
setIsSearching(true) setIsSearching(true)
const emojis = await search(e.target.value) const emojis = await search(e.target.value)
setSearchedEmojis(emojis) setSearchedEmojis(emojis)


<div className="w-full max-h-[200px] overflow-x-hidden overflow-y-auto px-3"> <div className="w-full max-h-[200px] overflow-x-hidden overflow-y-auto px-3">
{isSearching && <> {isSearching && <>
<div key={`category-search`} className='flex flex-col'>
<div key={'category-search'} className='flex flex-col'>
<p className='font-medium uppercase text-xs text-[#101828] mb-1'>Search</p> <p className='font-medium uppercase text-xs text-[#101828] mb-1'>Search</p>
<div className='w-full h-full grid grid-cols-8 gap-1'> <div className='w-full h-full grid grid-cols-8 gap-1'>
{searchedEmojis.map((emoji: string, index: number) => { {searchedEmojis.map((emoji: string, index: number) => {
</div> </div>
</>} </>}



{categories.map((category: any, index: number) => { {categories.map((category: any, index: number) => {
return <div key={`category-${index}`} className='flex flex-col'> return <div key={`category-${index}`} className='flex flex-col'>
<p className='font-medium uppercase text-xs text-[#101828] mb-1'>{category.id}</p> <p className='font-medium uppercase text-xs text-[#101828] mb-1'>{category.id}</p>
</div> </div>


{/* Color Select */} {/* Color Select */}
<div className={cn('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> <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'> <div className='w-full h-full grid grid-cols-8 gap-1'>
{backgroundColors.map((color) => { {backgroundColors.map((color) => {
className={ className={
cn( cn(
'cursor-pointer', 'cursor-pointer',
`hover:ring-1 ring-offset-1`,
'hover:ring-1 ring-offset-1',
'inline-flex w-10 h-10 rounded-lg items-center justify-center', 'inline-flex w-10 h-10 rounded-lg items-center justify-center',
color === selectedBackground ? `ring-1 ring-gray-300` : '',
color === selectedBackground ? 'ring-1 ring-gray-300' : '',
)} )}
onClick={() => { onClick={() => {
setSelectedBackground(color) setSelectedBackground(color)
{t('app.emoji.cancel')} {t('app.emoji.cancel')}
</Button> </Button>
<Button <Button
disabled={selectedEmoji == ''}
disabled={selectedEmoji === ''}
type="primary" type="primary"
className='w-full' className='w-full'
onClick={() => { onClick={() => {

+ 72
- 24
web/app/components/datasets/create/step-two/index.tsx Datei anzeigen

createDocument, createDocument,
fetchFileIndexingEstimate as didFetchFileIndexingEstimate, fetchFileIndexingEstimate as didFetchFileIndexingEstimate,
} from '@/service/datasets' } from '@/service/datasets'
import type { CreateDocumentReq, createDocumentResponse } from '@/models/datasets'
import type { CreateDocumentReq, createDocumentResponse, FullDocumentDetail } from '@/models/datasets'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import PreviewItem from './preview-item' import PreviewItem from './preview-item'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { formatNumber } from '@/utils/format' import { formatNumber } from '@/utils/format'


type StepTwoProps = { type StepTwoProps = {
isSetting?: boolean,
documentDetail?: FullDocumentDetail
hasSetAPIKEY: boolean, hasSetAPIKEY: boolean,
onSetting: () => void, onSetting: () => void,
datasetId?: string, datasetId?: string,
indexingType?: string, indexingType?: string,
file?: File, file?: File,
onStepChange: (delta: number) => void,
updateIndexingTypeCache: (type: string) => void,
updateResultCache: (res: createDocumentResponse) => void
onStepChange?: (delta: number) => void,
updateIndexingTypeCache?: (type: string) => void,
updateResultCache?: (res: createDocumentResponse) => void
onSave?: () => void
onCancel?: () => void
} }


enum SegmentType { enum SegmentType {
} }


const StepTwo = ({ const StepTwo = ({
isSetting,
documentDetail,
hasSetAPIKEY, hasSetAPIKEY,
onSetting, onSetting,
datasetId, datasetId,
onStepChange, onStepChange,
updateIndexingTypeCache, updateIndexingTypeCache,
updateResultCache, updateResultCache,
onSave,
onCancel,
}: StepTwoProps) => { }: StepTwoProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null)
} }


const getCreationParams = () => { const getCreationParams = () => {
const params = {
data_source: {
type: 'upload_file',
info: file?.id,
name: file?.name,
},
indexing_technique: getIndexing_technique(),
process_rule: getProcessRule(),
} as CreateDocumentReq
let params
if (isSetting) {
params = {
original_document_id: documentDetail?.id,
process_rule: getProcessRule(),
} as CreateDocumentReq
} else {
params = {
data_source: {
type: 'upload_file',
info: file?.id,
name: file?.name,
},
indexing_technique: getIndexing_technique(),
process_rule: getProcessRule(),
} as CreateDocumentReq
}
return params return params
} }


console.log(err) console.log(err)
} }
} }

const getRulesFromDetail = () => {
if (documentDetail) {
const rules = documentDetail.dataset_process_rule.rules
const separator = rules.segmentation.separator
const max = rules.segmentation.max_tokens
setSegmentIdentifier(separator === '\n' ? '\\n' : separator || '\\n')
setMax(max)
setRules(rules.pre_processing_rules)
setDefaultConfig(rules)
}
}

const getDefaultMode = () => {
if (documentDetail) {
setSegmentationType(documentDetail.dataset_process_rule.mode)
}
}

const createHandle = async () => { const createHandle = async () => {
try { try {
let res; let res;
res = await createFirstDocument({ res = await createFirstDocument({
body: params body: params
}) })
updateIndexingTypeCache(indexType)
updateResultCache(res)
updateIndexingTypeCache && updateIndexingTypeCache(indexType)
updateResultCache && updateResultCache(res)
} else { } else {
res = await createDocument({ res = await createDocument({
datasetId, datasetId,
body: params body: params
}) })
updateIndexingTypeCache(indexType)
updateResultCache({
updateIndexingTypeCache && updateIndexingTypeCache(indexType)
updateResultCache && updateResultCache({
document: res, document: res,
}) })
} }
onStepChange(+1)
onStepChange && onStepChange(+1)
isSetting && onSave && onSave()
} }
catch (err) { catch (err) {
Toast.notify({ Toast.notify({


useEffect(() => { useEffect(() => {
// fetch rules // fetch rules
getRules()
if (!isSetting) {
getRules()
} else {
getRulesFromDetail()
getDefaultMode()
}
}, []) }, [])


useEffect(() => { useEffect(() => {
</div> </div>
</div> </div>
</div> </div>
<div className='flex items-center mt-8 py-2'>
<Button onClick={() => onStepChange(-1)}>{t('datasetCreation.stepTwo.lastStep')}</Button>
<div className={s.divider} />
<Button type='primary' onClick={createHandle}>{t('datasetCreation.stepTwo.nextStep')}</Button>
</div>
{!isSetting ? (
<div className='flex items-center mt-8 py-2'>
<Button onClick={() => onStepChange && onStepChange(-1)}>{t('datasetCreation.stepTwo.lastStep')}</Button>
<div className={s.divider} />
<Button type='primary' onClick={createHandle}>{t('datasetCreation.stepTwo.nextStep')}</Button>
</div>
) : (
<div className='flex items-center mt-8 py-2'>
<Button type='primary' onClick={createHandle}>{t('datasetCreation.stepTwo.save')}</Button>
<Button className='ml-2' onClick={onCancel}>{t('datasetCreation.stepTwo.cancel')}</Button>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

+ 41
- 10
web/app/components/datasets/documents/detail/embedding/index.tsx Datei anzeigen

import type { CommonResponse } from '@/models/common' import type { CommonResponse } from '@/models/common'
import { asyncRunSafe } from '@/utils' import { asyncRunSafe } from '@/utils'
import { formatNumber } from '@/utils/format' import { formatNumber } from '@/utils/format'
import { fetchIndexingEstimate, fetchIndexingStatus, fetchProcessRule, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'
import { fetchIndexingEstimate, fetchProcessRule, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'
import DatasetDetailContext from '@/context/dataset-detail' import DatasetDetailContext from '@/context/dataset-detail'
import StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal' import StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal'


const localDocumentId = docId ?? documentId const localDocumentId = docId ?? documentId
const localIndexingTechnique = indexingType ?? indexingTechnique const localIndexingTechnique = indexingType ?? indexingTechnique


const { data: indexingStatusDetail, error: indexingStatusErr, mutate: statusMutate } = useSWR({
action: 'fetchIndexingStatus',
datasetId: localDatasetId,
documentId: localDocumentId,
}, apiParams => fetchIndexingStatus(omit(apiParams, 'action')), {
refreshInterval: 5000,
revalidateOnFocus: false,
})
// const { data: indexingStatusDetailFromApi, error: indexingStatusErr, mutate: statusMutate } = useSWR({
// action: 'fetchIndexingStatus',
// datasetId: localDatasetId,
// documentId: localDocumentId,
// }, apiParams => fetchIndexingStatus(omit(apiParams, 'action')), {
// refreshInterval: 2500,
// revalidateOnFocus: false,
// })

const [indexingStatusDetail, setIndexingStatusDetail, getIndexingStatusDetail] = useGetState<any>(null)
const fetchIndexingStatus = async () => {
const status = await doFetchIndexingStatus({ datasetId: localDatasetId, documentId: localDocumentId })
setIndexingStatusDetail(status)
}

const [runId, setRunId, getRunId] = useGetState<any>(null)
const startQueryStatus = () => {
const runId = setInterval(() => {
const indexingStatusDetail = getIndexingStatusDetail()
if (indexingStatusDetail?.indexing_status === 'completed') {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
stopQueryStatus()
return
}
fetchIndexingStatus()
}, 2500)
setRunId(runId)
}
const stopQueryStatus = () => {
clearInterval(getRunId())
}

useEffect(() => {
fetchIndexingStatus()
startQueryStatus()
return () => {
stopQueryStatus()
}
}, [])


const { data: indexingEstimateDetail, error: indexingEstimateErr } = useSWR({ const { data: indexingEstimateDetail, error: indexingEstimateErr } = useSWR({
action: 'fetchIndexingEstimate', action: 'fetchIndexingEstimate',
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId: localDatasetId, documentId: localDocumentId }) as Promise<CommonResponse>) const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId: localDatasetId, documentId: localDocumentId }) as Promise<CommonResponse>)
if (!e) { if (!e) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
statusMutate()
setIndexingStatusDetail(null)
} }
else { else {
notify({ type: 'error', message: t('common.actionMsg.modificationFailed') }) notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })

+ 90
- 0
web/app/components/datasets/documents/detail/settings/index.tsx Datei anzeigen

'use client'
import React, { useState, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { useContext } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import DatasetDetailContext from '@/context/dataset-detail'
import type { FullDocumentDetail } from '@/models/datasets'
import { fetchTenantInfo } from '@/service/common'
import { fetchDocumentDetail, MetadataType } from '@/service/datasets'

import Loading from '@/app/components/base/loading'
import StepTwo from '@/app/components/datasets/create/step-two'
import AccountSetting from '@/app/components/header/account-setting'
import AppUnavailable from '@/app/components/base/app-unavailable'

type DocumentSettingsProps = {
datasetId: string;
documentId: string;
}

const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
const { t } = useTranslation()
const router = useRouter()
const [hasSetAPIKEY, setHasSetAPIKEY] = useState(true)
const [isShowSetAPIKey, { setTrue: showSetAPIKey, setFalse: hideSetAPIkey }] = useBoolean()
const [hasError, setHasError] = useState(false)
const { indexingTechnique, dataset } = useContext(DatasetDetailContext)

const saveHandler = () => router.push(`/datasets/${datasetId}/documents/${documentId}`)

const cancelHandler = () => router.back()

const checkAPIKey = async () => {
const data = await fetchTenantInfo({ url: '/info' })
const hasSetKey = data.providers.some(({ is_valid }) => is_valid)
setHasSetAPIKEY(hasSetKey)
}

useEffect(() => {
checkAPIKey()
}, [])

const [documentDetail, setDocumentDetail] = useState<FullDocumentDetail | null>(null)
useEffect(() => {
(async () => {
try {
const detail = await fetchDocumentDetail({
datasetId,
documentId,
params: { metadata: 'without' as MetadataType }
})
setDocumentDetail(detail)
} catch (e) {
setHasError(true)
}
})()
}, [datasetId, documentId])

if (hasError) {
return <AppUnavailable code={500} unknownReason={t('datasetCreation.error.unavailable') as string} />
}

return (
<div className='flex' style={{ height: 'calc(100vh - 56px)' }}>
<div className="grow bg-white">
{!documentDetail && <Loading type='app' />}
{dataset && documentDetail && (
<StepTwo
hasSetAPIKEY={hasSetAPIKEY}
onSetting={showSetAPIKey}
datasetId={datasetId}
indexingType={indexingTechnique || ''}
isSetting
documentDetail={documentDetail}
file={documentDetail.data_source_info.upload_file}
onSave={saveHandler}
onCancel={cancelHandler}
/>
)}
</div>
{isShowSetAPIKey && <AccountSetting activeTab="provider" onCancel={async () => {
await checkAPIKey()
hideSetAPIkey()
}} />}
</div>
)
}

export default DocumentSettings

+ 14
- 9
web/app/components/datasets/documents/list.tsx Datei anzeigen

const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter()


const isListScene = scene === 'list' const isListScene = scene === 'list'


</div> </div>
<Divider /> <Divider />
</>} </>}
{/* <div className={s.actionItem}>
<SettingsIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
</div>
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/create`)}>
<FilePlusIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.uploadFile')}</span>
</div>
<Divider className='my-1' /> */}
{!archived && (
<>
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
<SettingsIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
</div>
{/* <div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/create`)}>
<FilePlusIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.uploadFile')}</span>
</div> */}
<Divider className='my-1' />
</>
)}
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}> {!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
<ArchiveIcon /> <ArchiveIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span> <span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>

+ 1
- 1
web/app/components/datasets/documents/style.module.css Datei anzeigen

.txtIcon { .txtIcon {
background-image: url(./assets/txt.svg); background-image: url(./assets/txt.svg);
} }
.mdIcon {
.markdownIcon {
background-image: url(./assets/md.svg); background-image: url(./assets/md.svg);
} }
.statusItemDetail { .statusItemDetail {

+ 15
- 13
web/app/components/datasets/settings/form/index.tsx Datei anzeigen

'use client' 'use client'
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { BookOpenIcon } from '@heroicons/react/24/outline' import { BookOpenIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ToastContext } from '@/app/components/base/toast'
import PermissionsRadio from '../permissions-radio' import PermissionsRadio from '../permissions-radio'
import IndexMethodRadio from '../index-method-radio' import IndexMethodRadio from '../index-method-radio'
import { ToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { updateDatasetSetting, fetchDataDetail } from '@/service/datasets'
import { DataSet } from '@/models/datasets'
import { fetchDataDetail, updateDatasetSetting } from '@/service/datasets'
import type { DataSet } from '@/models/datasets'


const rowClass = ` const rowClass = `
flex justify-between py-4 flex justify-between py-4
const inputClass = ` const inputClass = `
w-[480px] px-3 bg-gray-100 text-sm text-gray-800 rounded-lg outline-none appearance-none w-[480px] px-3 bg-gray-100 text-sm text-gray-800 rounded-lg outline-none appearance-none
` `

const useInitialValue = <T,>(depend: T, dispatch: Dispatch<SetStateAction<T>>) => {
const useInitialValue = (depend: any, dispatch: any) => {
useEffect(() => { useEffect(() => {
dispatch(depend) dispatch(depend)
}, [depend]) }, [depend])
} }


const Form = ({ const Form = ({
datasetId
datasetId,
}: Props) => { }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique) const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)


const handleSave = async () => { const handleSave = async () => {
if (loading) return
if (loading)
return
if (!name?.trim()) { if (!name?.trim()) {
notify({ type: 'error', message: t('datasetSettings.form.nameError') }) notify({ type: 'error', message: t('datasetSettings.form.nameError') })
return return
name, name,
description, description,
permission, permission,
indexing_technique: indexMethod
}
indexing_technique: indexMethod,
},
}) })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
await mutateDatasets() await mutateDatasets()
} catch (e) {
}
catch (e) {
notify({ type: 'error', message: t('common.actionMsg.modificationFailed') }) notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })
} finally {
}
finally {
setLoading(false) setLoading(false)
} }
} }
) )
} }


export default Form
export default Form

+ 1
- 1
web/app/components/develop/secret-key/input-copy.tsx Datei anzeigen

'use client' 'use client'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import Tooltip from '@/app/components/base/tooltip'
import { t } from 'i18next' import { t } from 'i18next'
import s from './style.module.css' import s from './style.module.css'
import Tooltip from '@/app/components/base/tooltip'


type IInputCopyProps = { type IInputCopyProps = {
value?: string value?: string

+ 25
- 23
web/app/components/explore/app-list/index.tsx Datei anzeigen

'use client' 'use client'
import React, { FC, useEffect } from 'react'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import Toast from '../../base/toast'
import s from './style.module.css'
import ExploreContext from '@/context/explore-context' import ExploreContext from '@/context/explore-context'
import { App } from '@/models/explore'
import type { App } from '@/models/explore'
import Category from '@/app/components/explore/category' import Category from '@/app/components/explore/category'
import AppCard from '@/app/components/explore/app-card' import AppCard from '@/app/components/explore/app-card'
import { fetchAppList, installApp, fetchAppDetail } from '@/service/explore'
import { fetchAppDetail, fetchAppList, installApp } from '@/service/explore'
import { createApp } from '@/service/apps' import { createApp } from '@/service/apps'
import CreateAppModal from '@/app/components/explore/create-app-modal' import CreateAppModal from '@/app/components/explore/create-app-modal'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { NEED_REFRESH_APP_LIST_KEY } from '@/config'


import s from './style.module.css'
import Toast from '../../base/toast'

const Apps: FC = ({ }) => {
const Apps: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const { setControlUpdateInstalledApps, hasEditPermission } = useContext(ExploreContext) const { setControlUpdateInstalledApps, hasEditPermission } = useContext(ExploreContext)
const [isLoaded, setIsLoaded] = React.useState(false) const [isLoaded, setIsLoaded] = React.useState(false)


const currList = (() => { const currList = (() => {
if(currCategory === '') return allList
if (currCategory === '')
return allList
return allList.filter(item => item.category === currCategory) return allList.filter(item => item.category === currCategory)
})() })()
const [categories, setCategories] = React.useState([]) const [categories, setCategories] = React.useState([])
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const {categories, recommended_apps}:any = await fetchAppList()
const { categories, recommended_apps }: any = await fetchAppList()
setCategories(categories) setCategories(categories)
setAllList(recommended_apps) setAllList(recommended_apps)
setIsLoaded(true) setIsLoaded(true)


const [currApp, setCurrApp] = React.useState<App | null>(null) const [currApp, setCurrApp] = React.useState<App | null>(null)
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false) const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
const onCreate = async ({name, icon, icon_background}: any) => {
const onCreate = async ({ name, icon, icon_background }: any) => {
const { app_model_config: model_config } = await fetchAppDetail(currApp?.app.id as string) const { app_model_config: model_config } = await fetchAppDetail(currApp?.app.id as string)
try { try {
const app = await createApp({ const app = await createApp({
name, name,
}) })
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
router.push(`/app/${app.id}/overview`) router.push(`/app/${app.id}/overview`)
} catch (e) {
}
catch (e) {
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
} }
} }


if(!isLoaded) {
if (!isLoaded) {
return ( return (
<div className='flex h-full items-center'> <div className='flex h-full items-center'>
<Loading type='area' /> <Loading type='area' />
value={currCategory} value={currCategory}
onChange={setCurrCategory} onChange={setCurrCategory}
/> />
<div
<div
className='flex mt-6 flex-col overflow-auto bg-gray-100 shrink-0 grow' className='flex mt-6 flex-col overflow-auto bg-gray-100 shrink-0 grow'
style={{ style={{
maxHeight: 'calc(100vh - 243px)'
maxHeight: 'calc(100vh - 243px)',
}} }}
> >
<nav <nav
className={`${s.appList} grid content-start grid-cols-1 gap-4 px-12 pb-10grow shrink-0`}> className={`${s.appList} grid content-start grid-cols-1 gap-4 px-12 pb-10grow shrink-0`}>
{currList.map(app => ( {currList.map(app => (
<AppCard
<AppCard
key={app.app_id} key={app.app_id}
app={app} app={app}
canCreate={hasEditPermission} canCreate={hasEditPermission}
</div> </div>


{isShowCreateModal && ( {isShowCreateModal && (
<CreateAppModal
appName={currApp?.app.name || ''}
show={isShowCreateModal}
onConfirm={onCreate}
onHide={() => setIsShowCreateModal(false)}
/>
)}
<CreateAppModal
appName={currApp?.app.name || ''}
show={isShowCreateModal}
onConfirm={onCreate}
onHide={() => setIsShowCreateModal(false)}
/>
)}
</div> </div>
) )
} }

+ 15
- 14
web/app/components/explore/category.tsx Datei anzeigen

'use client' 'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import exploreI18n from '@/i18n/lang/explore.en'
import cn from 'classnames' import cn from 'classnames'
import exploreI18n from '@/i18n/lang/explore.en'


const categoryI18n = exploreI18n.category const categoryI18n = exploreI18n.category


export interface ICategoryProps {
export type ICategoryProps = {
className?: string className?: string
list: string[] list: string[]
value: string value: string
className, className,
list, list,
value, value,
onChange
onChange,
}) => { }) => {
const { t } = useTranslation() 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)'} : {}
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 ( return (
<div className={cn(className, 'flex space-x-1 text-[13px]')}> <div className={cn(className, 'flex space-x-1 text-[13px]')}>
<div
className={itemClassName('' === value)}
style={itemStyle('' === value)}
onClick={() => onChange('')}
>
{t('explore.apps.allCategories')}
</div>
<div
className={itemClassName(value === '')}
style={itemStyle(value === '')}
onClick={() => onChange('')}
>
{t('explore.apps.allCategories')}
</div>
{list.map(name => ( {list.map(name => (
<div
<div
key={name} key={name}
className={itemClassName(name === value)} className={itemClassName(name === value)}
style={itemStyle(name === value)} style={itemStyle(name === value)}

+ 40
- 41
web/app/components/explore/create-app-modal/index.tsx Datei anzeigen

import React, { useState } from 'react' import React, { useState } from 'react'
import cn from 'classnames' import cn from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import EmojiPicker from '@/app/components/base/emoji-picker' import EmojiPicker from '@/app/components/base/emoji-picker'


import s from './style.module.css'

type IProps = { type IProps = {
appName: string,
show: boolean,
onConfirm: (info: any) => void,
onHide: () => void,
appName: string
show: boolean
onConfirm: (info: any) => void
onHide: () => void
} }


const CreateAppModal = ({ const CreateAppModal = ({
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' }) const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })


const submit = () => { const submit = () => {
if(!name.trim()) {
if (!name.trim()) {
Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') }) Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') })
return return
} }


return ( 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'
/>
<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>
</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)
}}
/>}
<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)
}}
/>}
</> </>
) )
} }



+ 10
- 8
web/app/components/explore/index.tsx Datei anzeigen

'use client' 'use client'
import React, { FC, useEffect, useState } from 'react'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ExploreContext from '@/context/explore-context' import ExploreContext from '@/context/explore-context'
import Sidebar from '@/app/components/explore/sidebar' import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { fetchMembers } from '@/service/common' import { fetchMembers } from '@/service/common'
import { InstalledApp } from '@/models/explore'
import { useTranslation } from 'react-i18next'
import type { InstalledApp } from '@/models/explore'


export interface IExploreProps {
export type IExploreProps = {
children: React.ReactNode children: React.ReactNode
} }


const Explore: FC<IExploreProps> = ({ const Explore: FC<IExploreProps> = ({
children
children,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0) const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
useEffect(() => { useEffect(() => {
document.title = `${t('explore.title')} - Dify`; document.title = `${t('explore.title')} - Dify`;
(async () => { (async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {}})
if(!accounts) return
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
if (!accounts)
return
const currUser = accounts.find(account => account.id === userProfile.id) const currUser = accounts.find(account => account.id === userProfile.id)
setHasEditPermission(currUser?.role !== 'normal') setHasEditPermission(currUser?.role !== 'normal')
})() })()
setControlUpdateInstalledApps, setControlUpdateInstalledApps,
hasEditPermission, hasEditPermission,
installedApps, installedApps,
setInstalledApps
setInstalledApps,
} }
} }
> >

+ 14
- 11
web/app/components/explore/installed-app/index.tsx Datei anzeigen

'use client' 'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import ExploreContext from '@/context/explore-context' import ExploreContext from '@/context/explore-context'
import ChatApp from '@/app/components/share/chat' import ChatApp from '@/app/components/share/chat'
import TextGenerationApp from '@/app/components/share/text-generation' import TextGenerationApp from '@/app/components/share/text-generation'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'


export interface IInstalledAppProps {
export type IInstalledAppProps = {
id: string id: string
} }


id, id,
}) => { }) => {
const { installedApps } = useContext(ExploreContext) const { installedApps } = useContext(ExploreContext)
const installedApp = installedApps.find(item => item.id === id)
if(!installedApp) {
const installedApp = installedApps.find(item => item.id === id)
if (!installedApp) {
return ( return (
<div className='flex h-full items-center'> <div className='flex h-full items-center'>
<Loading type='area' /> <Loading type='area' />
</div> </div>
) )
} }
return ( return (
<div className='h-full p-2'> <div className='h-full p-2'>
{installedApp?.app.mode === 'chat' ? (
<ChatApp isInstalledApp installedAppInfo={installedApp}/>
): (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
)}
{installedApp?.app.mode === 'chat'
? (
<ChatApp isInstalledApp installedAppInfo={installedApp}/>
)
: (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
)}
</div> </div>
) )
} }

+ 11
- 10
web/app/components/explore/item-operation/index.tsx Datei anzeigen

'use client' 'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames' import cn from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Popover from '@/app/components/base/popover'
import { TrashIcon } from '@heroicons/react/24/outline' import { TrashIcon } from '@heroicons/react/24/outline'


import s from './style.module.css' import s from './style.module.css'
import Popover from '@/app/components/base/popover'


const PinIcon = ( const PinIcon = (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
</svg> </svg>
) )


export interface IItemOperationProps {
export type IItemOperationProps = {
className?: string className?: string
isPinned: boolean isPinned: boolean
isShowDelete: boolean isShowDelete: boolean
isPinned, isPinned,
isShowDelete, isShowDelete,
togglePin, togglePin,
onDelete
onDelete,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()


</div> </div>
{isShowDelete && ( {isShowDelete && (
<div className={cn(s.actionItem, s.deleteActionItem, 'hover:bg-gray-50 group')} onClick={onDelete} > <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>
<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> </div>
} }
trigger='click' trigger='click'
position='br' position='br'
btnElement={<div />} 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`}
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'}
/> />
) )
} }

+ 5
- 6
web/app/components/explore/sidebar/app-nav-item/index.tsx Datei anzeigen

'use client' 'use client'
import cn from 'classnames' import cn from 'classnames'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import s from './style.module.css'
import ItemOperation from '@/app/components/explore/item-operation' import ItemOperation from '@/app/components/explore/item-operation'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'


import s from './style.module.css'

export interface IAppNavItemProps {
export type IAppNavItemProps = {
name: string name: string
id: string id: string
icon: string icon: string
}: IAppNavItemProps) { }: IAppNavItemProps) {
const router = useRouter() const router = useRouter()
const url = `/explore/installed/${id}` const url = `/explore/installed/${id}`
return ( return (
<div <div
key={id} key={id}
isSelected ? s.active : 'hover:bg-gray-200', isSelected ? s.active : 'hover:bg-gray-200',
'flex h-8 justify-between px-2 rounded-lg text-sm font-normal ', 'flex h-8 justify-between px-2 rounded-lg text-sm font-normal ',
)} )}
onClick={() => {
onClick={() => {
router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation(). router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation().
}} }}
> >
borderColor: '0.5px solid rgba(0, 0, 0, 0.05)' borderColor: '0.5px solid rgba(0, 0, 0, 0.05)'
}} }}
/> */} /> */}
<AppIcon size='tiny' icon={icon} background={icon_background} />
<AppIcon size='tiny' icon={icon} background={icon_background} />
<div className='overflow-hidden text-ellipsis whitespace-nowrap'>{name}</div> <div className='overflow-hidden text-ellipsis whitespace-nowrap'>{name}</div>
</div> </div>
{ {

+ 30
- 32
web/app/components/header/account-setting/provider-page/azure-provider/index.tsx Datei anzeigen

import type { Provider, ProviderAzureToken } from '@/models/common'
import { ProviderName } from '@/models/common'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Link from 'next/link' import Link from 'next/link'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
import { useState, useEffect } from 'react'
import { useEffect, useState } from 'react'
import ProviderInput from '../provider-input' import ProviderInput from '../provider-input'
import useValidateToken, { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
ValidatedErrorOnAzureOpenaiTip,
ValidatedSuccessIcon, ValidatedSuccessIcon,
ValidatingTip, ValidatingTip,
ValidatedErrorOnAzureOpenaiTip
} from '../provider-input/Validate' } from '../provider-input/Validate'
import { ProviderName } from '@/models/common'
import type { Provider, ProviderAzureToken } from '@/models/common'


interface IAzureProviderProps {
type IAzureProviderProps = {
provider: Provider provider: Provider
onValidatedStatus: (status?: ValidatedStatusState) => void onValidatedStatus: (status?: ValidatedStatusState) => void
onTokenChange: (token: ProviderAzureToken) => void onTokenChange: (token: ProviderAzureToken) => void
} }
const AzureProvider = ({ const AzureProvider = ({
provider,
provider,
onTokenChange, onTokenChange,
onValidatedStatus
onValidatedStatus,
}: IAzureProviderProps) => { }: IAzureProviderProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [token, setToken] = useState<ProviderAzureToken>(provider.provider_name === ProviderName.AZURE_OPENAI ? {...provider.token}: {})
const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name)
const [token, setToken] = useState<ProviderAzureToken>(provider.provider_name === ProviderName.AZURE_OPENAI ? { ...provider.token } : {})
const [validating, validatedStatus, setValidatedStatus, validate] = useValidateToken(provider.provider_name)
const handleFocus = (type: keyof ProviderAzureToken) => { const handleFocus = (type: keyof ProviderAzureToken) => {
if (token[type] === (provider?.token as ProviderAzureToken)[type]) { if (token[type] === (provider?.token as ProviderAzureToken)[type]) {
token[type] = '' token[type] = ''
setToken({...token})
onTokenChange({...token})
setToken({ ...token })
onTokenChange({ ...token })
setValidatedStatus({}) setValidatedStatus({})
} }
} }
const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => { const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => {
token[type] = v token[type] = v
setToken({...token})
onTokenChange({...token})
validate({...token}, {
setToken({ ...token })
onTokenChange({ ...token })
validate({ ...token }, {
beforeValidating: () => { beforeValidating: () => {
if (!token.openai_api_base || !token.openai_api_key) { if (!token.openai_api_base || !token.openai_api_key) {
setValidatedStatus({}) setValidatedStatus({})
return false return false
} }
return true return true
}
},
}) })
} }
const getValidatedIcon = () => { const getValidatedIcon = () => {
if (validatedStatus.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) {
if (validatedStatus.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
return <ValidatedErrorIcon /> return <ValidatedErrorIcon />
}
if (validatedStatus.status === ValidatedStatus.Success) {
if (validatedStatus.status === ValidatedStatus.Success)
return <ValidatedSuccessIcon /> return <ValidatedSuccessIcon />
}
} }
const getValidatedTip = () => { const getValidatedTip = () => {
if (validating) {
if (validating)
return <ValidatingTip /> return <ValidatingTip />
}
if (validatedStatus.status === ValidatedStatus.Error) {
if (validatedStatus.status === ValidatedStatus.Error)
return <ValidatedErrorOnAzureOpenaiTip errorMessage={validatedStatus.message ?? ''} /> return <ValidatedErrorOnAzureOpenaiTip errorMessage={validatedStatus.message ?? ''} />
}
} }
useEffect(() => { useEffect(() => {
if (typeof onValidatedStatus === 'function') {
if (typeof onValidatedStatus === 'function')
onValidatedStatus(validatedStatus) onValidatedStatus(validatedStatus)
}
}, [validatedStatus]) }, [validatedStatus])


return ( return (
<div className='px-4 py-3'> <div className='px-4 py-3'>
<ProviderInput
<ProviderInput
className='mb-4' className='mb-4'
name={t('common.provider.azure.apiBase')} name={t('common.provider.azure.apiBase')}
placeholder={t('common.provider.azure.apiBasePlaceholder')} placeholder={t('common.provider.azure.apiBasePlaceholder')}
value={token.openai_api_base} value={token.openai_api_base}
onChange={(v) => handleChange('openai_api_base', v, validate)}
onChange={v => handleChange('openai_api_base', v, validate)}
onFocus={() => handleFocus('openai_api_base')} onFocus={() => handleFocus('openai_api_base')}
validatedIcon={getValidatedIcon()} validatedIcon={getValidatedIcon()}
/> />
<ProviderInput
<ProviderInput
className='mb-4' className='mb-4'
name={t('common.provider.azure.apiKey')} name={t('common.provider.azure.apiKey')}
placeholder={t('common.provider.azure.apiKeyPlaceholder')} placeholder={t('common.provider.azure.apiKeyPlaceholder')}
value={token.openai_api_key} value={token.openai_api_key}
onChange={(v) => handleChange('openai_api_key', v, validate)}
onChange={v => handleChange('openai_api_key', v, validate)}
onFocus={() => handleFocus('openai_api_key')} onFocus={() => handleFocus('openai_api_key')}
validatedIcon={getValidatedIcon()} validatedIcon={getValidatedIcon()}
validatedTip={getValidatedTip()} validatedTip={getValidatedTip()}

+ 7
- 7
web/app/components/header/account-setting/provider-page/index.tsx Datei anzeigen

import { useState } from 'react' import { useState } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { fetchProviders } from '@/service/common'
import ProviderItem from './provider-item'
import OpenaiHostedProvider from './openai-hosted-provider'
import type { ProviderHosted } from '@/models/common'
import { LockClosedIcon } from '@heroicons/react/24/solid' import { LockClosedIcon } from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Link from 'next/link' import Link from 'next/link'
import ProviderItem from './provider-item'
import OpenaiHostedProvider from './openai-hosted-provider'
import type { ProviderHosted } from '@/models/common'
import { fetchProviders } from '@/service/common'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'


const providersMap: {[k: string]: any} = {
const providersMap: { [k: string]: any } = {
'openai-custom': { 'openai-custom': {
icon: 'openai', icon: 'openai',
name: 'OpenAI', name: 'OpenAI',
'azure_openai-custom': { 'azure_openai-custom': {
icon: 'azure', icon: 'azure',
name: 'Azure OpenAI Service', name: 'Azure OpenAI Service',
}
},
} }


// const providersList = [ // const providersList = [
const { t } = useTranslation() const { t } = useTranslation()
const [activeProviderId, setActiveProviderId] = useState('') const [activeProviderId, setActiveProviderId] = useState('')
const { data, mutate } = useSWR({ url: '/workspaces/current/providers' }, fetchProviders) const { data, mutate } = useSWR({ url: '/workspaces/current/providers' }, fetchProviders)
const providers = data?.filter(provider => providersMap[`${provider.provider_name}-${provider.provider_type}`])?.map(provider => {
const providers = data?.filter(provider => providersMap[`${provider.provider_name}-${provider.provider_type}`])?.map((provider) => {
const providerKey = `${provider.provider_name}-${provider.provider_type}` const providerKey = `${provider.provider_name}-${provider.provider_type}`
return { return {
provider, provider,

+ 21
- 24
web/app/components/header/account-setting/provider-page/openai-provider/index.tsx Datei anzeigen

import type { Provider } from '@/models/common'
import { useState, useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ProviderInput from '../provider-input'
import Link from 'next/link' import Link from 'next/link'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
import useValidateToken, { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
import ProviderInput from '../provider-input'
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
ValidatedErrorOnOpenaiTip,
ValidatedSuccessIcon, ValidatedSuccessIcon,
ValidatingTip, ValidatingTip,
ValidatedExceedOnOpenaiTip,
ValidatedErrorOnOpenaiTip
} from '../provider-input/Validate' } from '../provider-input/Validate'
import type { Provider } from '@/models/common'


interface IOpenaiProviderProps {
type IOpenaiProviderProps = {
provider: Provider provider: Provider
onValidatedStatus: (status?: ValidatedStatusState) => void onValidatedStatus: (status?: ValidatedStatusState) => void
onTokenChange: (token: string) => void onTokenChange: (token: string) => void
const OpenaiProvider = ({ const OpenaiProvider = ({
provider, provider,
onValidatedStatus, onValidatedStatus,
onTokenChange
onTokenChange,
}: IOpenaiProviderProps) => { }: IOpenaiProviderProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [token, setToken] = useState(provider.token as string || '') const [token, setToken] = useState(provider.token as string || '')
const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name)
const [validating, validatedStatus, setValidatedStatus, validate] = useValidateToken(provider.provider_name)
const handleFocus = () => { const handleFocus = () => {
if (token === provider.token) { if (token === provider.token) {
setToken('') setToken('')
return false return false
} }
return true return true
}
},
}) })
} }
useEffect(() => { useEffect(() => {
if (typeof onValidatedStatus === 'function') {
if (typeof onValidatedStatus === 'function')
onValidatedStatus(validatedStatus) onValidatedStatus(validatedStatus)
}
}, [validatedStatus]) }, [validatedStatus])


const getValidatedIcon = () => { const getValidatedIcon = () => {
if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) {
if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
return <ValidatedErrorIcon /> return <ValidatedErrorIcon />
}
if (validatedStatus.status === ValidatedStatus.Success) {
if (validatedStatus.status === ValidatedStatus.Success)
return <ValidatedSuccessIcon /> return <ValidatedSuccessIcon />
}
} }
const getValidatedTip = () => { const getValidatedTip = () => {
if (validating) {
if (validating)
return <ValidatingTip /> return <ValidatingTip />
}
if (validatedStatus?.status === ValidatedStatus.Error) {
if (validatedStatus?.status === ValidatedStatus.Error)
return <ValidatedErrorOnOpenaiTip errorMessage={validatedStatus.message ?? ''} /> return <ValidatedErrorOnOpenaiTip errorMessage={validatedStatus.message ?? ''} />
}
} }


return ( return (
<div className='px-4 pt-3 pb-4'> <div className='px-4 pt-3 pb-4'>
<ProviderInput
<ProviderInput
value={token} value={token}
name={t('common.provider.apiKey')} name={t('common.provider.apiKey')}
placeholder={t('common.provider.enterYourKey')} placeholder={t('common.provider.enterYourKey')}
) )
} }


export default OpenaiProvider
export default OpenaiProvider

+ 7
- 7
web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx Datei anzeigen

export const ValidatingTip = () => { export const ValidatingTip = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className={`mt-2 text-primary-600 text-xs font-normal`}>
<div className={'mt-2 text-primary-600 text-xs font-normal'}>
{t('common.provider.validating')} {t('common.provider.validating')}
</div> </div>
) )
const { locale } = useContext(I18n) const { locale } = useContext(I18n)


return ( return (
<div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
{t('common.provider.apiKeyExceedBill')}&nbsp; {t('common.provider.apiKeyExceedBill')}&nbsp;
<Link
<Link
className='underline' className='underline'
href="https://platform.openai.com/account/api-keys"
href="https://platform.openai.com/account/api-keys"
target={'_blank'}> target={'_blank'}>
{locale === 'en' ? 'this link' : '这篇文档'} {locale === 'en' ? 'this link' : '这篇文档'}
</Link> </Link>
const { t } = useTranslation() const { t } = useTranslation()


return ( return (
<div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
{t('common.provider.validatedError')}{errorMessage} {t('common.provider.validatedError')}{errorMessage}
</div> </div>
) )
const { t } = useTranslation() const { t } = useTranslation()


return ( return (
<div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
{t('common.provider.validatedError')}{errorMessage} {t('common.provider.validatedError')}{errorMessage}
</div> </div>
) )
}
}

+ 8
- 9
web/app/components/header/account-setting/provider-page/provider-input/index.tsx Datei anzeigen

import { ChangeEvent } from 'react'
import { ReactElement } from 'react-markdown/lib/react-markdown'
import type { ChangeEvent } from 'react'
import type { ReactElement } from 'react-markdown/lib/react-markdown'


interface IProviderInputProps {
type IProviderInputProps = {
value?: string value?: string
name: string name: string
placeholder: string placeholder: string
onChange, onChange,
onFocus, onFocus,
validatedIcon, validatedIcon,
validatedTip
validatedTip,
}: IProviderInputProps) => { }: IProviderInputProps) => {

const handleChange = (e: ChangeEvent<HTMLInputElement>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value const inputValue = e.target.value
onChange(inputValue) onChange(inputValue)
flex items-center px-3 bg-white rounded-lg flex items-center px-3 bg-white rounded-lg
shadow-[0_1px_2px_rgba(16,24,40,0.05)] shadow-[0_1px_2px_rgba(16,24,40,0.05)]
'> '>
<input
<input
className=' className='
w-full py-[9px] w-full py-[9px]
text-xs font-medium text-gray-700 leading-[18px] text-xs font-medium text-gray-700 leading-[18px]
appearance-none outline-none bg-transparent
'
appearance-none outline-none bg-transparent
'
value={value} value={value}
placeholder={placeholder} placeholder={placeholder}
onChange={handleChange} onChange={handleChange}
) )
} }


export default ProviderInput
export default ProviderInput

+ 18
- 15
web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts Datei anzeigen

import { useState, useCallback, SetStateAction, Dispatch } from 'react'
import type { Dispatch, SetStateAction } from 'react'
import { useCallback, useState } from 'react'
import debounce from 'lodash-es/debounce' import debounce from 'lodash-es/debounce'
import { DebouncedFunc } from 'lodash-es'
import type { DebouncedFunc } from 'lodash-es'
import { validateProviderKey } from '@/service/common' import { validateProviderKey } from '@/service/common'


export enum ValidatedStatus { export enum ValidatedStatus {
Success = 'success', Success = 'success',
Error = 'error', Error = 'error',
Exceed = 'exceed'
Exceed = 'exceed',
} }
export type ValidatedStatusState = { export type ValidatedStatusState = {
status?: ValidatedStatus,
status?: ValidatedStatus
message?: string message?: string
} }
// export type ValidatedStatusState = ValidatedStatus | undefined | ValidatedError // export type ValidatedStatusState = ValidatedStatus | undefined | ValidatedError
export type SetValidatedStatus = Dispatch<SetStateAction<ValidatedStatusState>> export type SetValidatedStatus = Dispatch<SetStateAction<ValidatedStatusState>>
export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void> export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void>
type ValidateTokenReturn = [ type ValidateTokenReturn = [
boolean,
ValidatedStatusState,
boolean,
ValidatedStatusState,
SetValidatedStatus, SetValidatedStatus,
ValidateFn
ValidateFn,
] ]
export type ValidateFnConfig = { export type ValidateFnConfig = {
beforeValidating: (token: any) => boolean beforeValidating: (token: any) => boolean
const [validating, setValidating] = useState(false) const [validating, setValidating] = useState(false)
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({}) const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => { const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => {
if (!config.beforeValidating(token)) {
if (!config.beforeValidating(token))
return false return false
}
setValidating(true) setValidating(true)
try { try {
const res = await validateProviderKey({ url: `/workspaces/current/providers/${providerName}/token-validate`, body: { token } }) const res = await validateProviderKey({ url: `/workspaces/current/providers/${providerName}/token-validate`, body: { token } })
setValidatedStatus( setValidatedStatus(
res.result === 'success'
? { status: ValidatedStatus.Success }
res.result === 'success'
? { status: ValidatedStatus.Success }
: { status: ValidatedStatus.Error, message: res.error }) : { status: ValidatedStatus.Error, message: res.error })
} catch (e: any) {
}
catch (e: any) {
setValidatedStatus({ status: ValidatedStatus.Error, message: e.message }) setValidatedStatus({ status: ValidatedStatus.Error, message: e.message })
} finally {
}
finally {
setValidating(false) setValidating(false)
} }
}, 500), []) }, 500), [])
validating, validating,
validatedStatus, validatedStatus,
setValidatedStatus, setValidatedStatus,
validate
validate,
] ]
} }


export default useValidateToken
export default useValidateToken

+ 30
- 25
web/app/components/header/account-setting/provider-page/provider-item/index.tsx Datei anzeigen

import { useState } from 'react' import { useState } from 'react'
import cn from 'classnames' import cn from 'classnames'
import s from './index.module.css'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import Indicator from '../../../indicator'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { Provider, ProviderAzureToken } from '@/models/common'
import { ProviderName } from '@/models/common'
import Indicator from '../../../indicator'
import OpenaiProvider from '../openai-provider' import OpenaiProvider from '../openai-provider'
import AzureProvider from '../azure-provider' import AzureProvider from '../azure-provider'
import { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken'
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
import { ValidatedStatus } from '../provider-input/useValidateToken'
import s from './index.module.css'
import type { Provider, ProviderAzureToken } from '@/models/common'
import { ProviderName } from '@/models/common'
import { updateProviderAIKey } from '@/service/common' import { updateProviderAIKey } from '@/service/common'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'


interface IProviderItemProps {
type IProviderItemProps = {
icon: string icon: string
name: string name: string
provider: Provider provider: Provider
name, name,
provider, provider,
onActive, onActive,
onSave
onSave,
}: IProviderItemProps) => { }: IProviderItemProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>() const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const [token, setToken] = useState<ProviderAzureToken | string>( const [token, setToken] = useState<ProviderAzureToken | string>(
provider.provider_name === 'azure_openai'
provider.provider_name === 'azure_openai'
? { openai_api_base: '', openai_api_key: '' } ? { openai_api_base: '', openai_api_key: '' }
: ''
)
: '',
)
const id = `${provider.provider_name}-${provider.provider_type}` const id = `${provider.provider_name}-${provider.provider_type}`
const isOpen = id === activeId const isOpen = id === activeId
const comingSoon = false const comingSoon = false


const providerTokenHasSetted = () => { const providerTokenHasSetted = () => {
if (provider.provider_name === ProviderName.AZURE_OPENAI) { if (provider.provider_name === ProviderName.AZURE_OPENAI) {
return provider.token && provider.token.openai_api_base && provider.token.openai_api_key ? {
openai_api_base: provider.token.openai_api_base,
openai_api_key: provider.token.openai_api_key
}: undefined
return (provider.token && provider.token.openai_api_base && provider.token.openai_api_key)
? {
openai_api_base: provider.token.openai_api_base,
openai_api_key: provider.token.openai_api_key,
}
: undefined
} }
if (provider.provider_name === ProviderName.OPENAI) {
if (provider.provider_name === ProviderName.OPENAI)
return provider.token return provider.token
}
} }
const handleUpdateToken = async () => { const handleUpdateToken = async () => {
if (loading) return
if (loading)
return
if (validatedStatus?.status === ValidatedStatus.Success) { if (validatedStatus?.status === ValidatedStatus.Success) {
try { try {
setLoading(true) setLoading(true)
await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } }) await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onActive('') onActive('')
} catch (e) {
}
catch (e) {
notify({ type: 'error', message: t('common.provider.saveFailed') }) notify({ type: 'error', message: t('common.provider.saveFailed') })
} finally {
}
finally {
setLoading(false) setLoading(false)
onSave() onSave()
} }
</div> </div>
{ {
provider.provider_name === ProviderName.OPENAI && isOpen && ( provider.provider_name === ProviderName.OPENAI && isOpen && (
<OpenaiProvider
provider={provider}
onValidatedStatus={v => setValidatedStatus(v)}
<OpenaiProvider
provider={provider}
onValidatedStatus={v => setValidatedStatus(v)}
onTokenChange={v => setToken(v)} onTokenChange={v => setToken(v)}
/> />
) )
} }
{ {
provider.provider_name === ProviderName.AZURE_OPENAI && isOpen && ( provider.provider_name === ProviderName.AZURE_OPENAI && isOpen && (
<AzureProvider
provider={provider}
onValidatedStatus={v => setValidatedStatus(v)}
<AzureProvider
provider={provider}
onValidatedStatus={v => setValidatedStatus(v)}
onTokenChange={v => setToken(v)} onTokenChange={v => setToken(v)}
/> />
) )

+ 12
- 12
web/app/components/header/index.tsx Datei anzeigen

import type { FC } from 'react' import type { FC } from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelectedLayoutSegment, useRouter } from 'next/navigation'
import { useRouter, useSelectedLayoutSegment } from 'next/navigation'
import classNames from 'classnames' import classNames from 'classnames'
import { CircleStackIcon, PuzzlePieceIcon } from '@heroicons/react/24/outline' import { CircleStackIcon, PuzzlePieceIcon } from '@heroicons/react/24/outline'
import { CommandLineIcon, Squares2X2Icon } from '@heroicons/react/24/solid' import { CommandLineIcon, Squares2X2Icon } from '@heroicons/react/24/solid'
import { WorkspaceProvider } from '@/context/workspace-context' import { WorkspaceProvider } from '@/context/workspace-context'
import { useDatasetsContext } from '@/context/datasets-context' import { useDatasetsContext } from '@/context/datasets-context'


const BuildAppsIcon = ({isSelected}: {isSelected: boolean}) => (
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"> <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"/>
<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> </svg>
) )


<div className={classNames( <div className={classNames(
'sticky top-0 left-0 right-0 z-20 flex bg-gray-100 grow-0 shrink-0 basis-auto h-14', 'sticky top-0 left-0 right-0 z-20 flex bg-gray-100 grow-0 shrink-0 basis-auto h-14',
s.header, s.header,
isBordered ? 'border-b border-gray-200' : ''
isBordered ? 'border-b border-gray-200' : '',
)}> )}>
<div className={classNames( <div className={classNames(
s[`header-${langeniusVersionInfo.current_env}`], s[`header-${langeniusVersionInfo.current_env}`],
'flex flex-1 items-center justify-between px-4'
'flex flex-1 items-center justify-between px-4',
)}> )}>
<div className='flex items-center'> <div className='flex items-center'>
<Link href="/apps" className='flex items-center mr-3'> <Link href="/apps" className='flex items-center mr-3'>
<div className={s['logo']} />
<div className={s.logo} />
</Link> </Link>
{/* Add it when has many stars */} {/* Add it when has many stars */}
<div className=' <div className='
flex items-center h-[26px] px-2 bg-white flex items-center h-[26px] px-2 bg-white
border border-solid border-[#E5E7EB] rounded-l-[6px] rounded-r-[6px] border border-solid border-[#E5E7EB] rounded-l-[6px] rounded-r-[6px]
'> '>
<div className={s['alpha']} />
<div className={s.alpha} />
<div className='ml-1 text-xs font-semibold text-gray-700'>{t('common.menus.status')}</div> <div className='ml-1 text-xs font-semibold text-gray-700'>{t('common.menus.status')}</div>
</div> </div>
</div> </div>
<Link href="/explore/apps" className={classNames( <Link href="/explore/apps" className={classNames(
navClassName, 'group', 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 && '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'
isExplore ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700',
)}> )}>
<Squares2X2Icon className='mr-1 w-[18px] h-[18px]' /> <Squares2X2Icon className='mr-1 w-[18px] h-[18px]' />
{t('common.menus.explore')} {t('common.menus.explore')}
text={t('common.menus.apps')} text={t('common.menus.apps')}
activeSegment={['apps', 'app']} activeSegment={['apps', 'app']}
link='/apps' link='/apps'
curNav={curApp && { id: curApp.id, name: curApp.name ,icon: curApp.icon, icon_background: curApp.icon_background}}
curNav={curApp && { id: curApp.id, name: curApp.name, icon: curApp.icon, icon_background: curApp.icon_background }}
navs={appItems.map(item => ({ navs={appItems.map(item => ({
id: item.id, id: item.id,
name: item.name, name: item.name,
link: `/app/${item.id}/overview`, link: `/app/${item.id}/overview`,
icon: item.icon, icon: item.icon,
icon_background: item.icon_background
icon_background: item.icon_background,
}))} }))}
createText={t('common.menus.newApp')} createText={t('common.menus.newApp')}
onCreate={() => setShowNewAppDialog(true)} onCreate={() => setShowNewAppDialog(true)}
<Link href="/plugins-coming-soon" className={classNames( <Link href="/plugins-coming-soon" className={classNames(
navClassName, 'group', navClassName, 'group',
isPluginsComingSoon && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)]', isPluginsComingSoon && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)]',
isPluginsComingSoon ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700'
isPluginsComingSoon ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700',
)}> )}>
<PuzzlePieceIcon className='mr-1 w-[18px] h-[18px]' /> <PuzzlePieceIcon className='mr-1 w-[18px] h-[18px]' />
{t('common.menus.plugins')} {t('common.menus.plugins')}
name: dataset.name, name: dataset.name,
link: `/datasets/${dataset.id}/documents`, link: `/datasets/${dataset.id}/documents`,
icon: dataset.icon, icon: dataset.icon,
icon_background: dataset.icon_background
icon_background: dataset.icon_background,
}))} }))}
createText={t('common.menus.newDataset')} createText={t('common.menus.newDataset')}
onCreate={() => router.push('/datasets/create')} onCreate={() => router.push('/datasets/create')}

+ 5
- 4
web/app/components/share/chat/sidebar/app-info/index.tsx Datei anzeigen

'use client' 'use client'
import React, { FC } from 'react'
import cn from 'classnames'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import { appDefaultIconBackground } from '@/config/index' import { appDefaultIconBackground } from '@/config/index'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'


export interface IAppInfoProps {
export type IAppInfoProps = {
className?: string className?: string
icon: string icon: string
icon_background?: string icon_background?: string
className, className,
icon, icon,
icon_background, icon_background,
name
name,
}) => { }) => {
return ( return (
<div className={cn(className, 'flex items-center space-x-3')}> <div className={cn(className, 'flex items-center space-x-3')}>

+ 9
- 9
web/app/components/share/chat/sidebar/index.tsx Datei anzeigen

import React, { useEffect, useRef } from 'react'
import React, { useRef } from 'react'
import type { FC } from 'react' import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
ChatBubbleOvalLeftEllipsisIcon, ChatBubbleOvalLeftEllipsisIcon,
PencilSquareIcon
PencilSquareIcon,
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon, } from '@heroicons/react/24/solid'
import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid'
import { useInfiniteScroll } from 'ahooks'
import Button from '../../../base/button' import Button from '../../../base/button'
import AppInfo from '@/app/components/share/chat/sidebar/app-info' import AppInfo from '@/app/components/share/chat/sidebar/app-info'
// import Card from './card' // import Card from './card'
import type { ConversationItem, SiteInfo } from '@/models/share' import type { ConversationItem, SiteInfo } from '@/models/share'
import { useInfiniteScroll } from 'ahooks'
import { fetchConversations } from '@/service/share' import { fetchConversations } from '@/service/share'


function classNames(...classes: any[]) { function classNames(...classes: any[]) {
isInstalledApp: boolean isInstalledApp: boolean
installedAppId?: string installedAppId?: string
siteInfo: SiteInfo siteInfo: SiteInfo
onMoreLoaded: (res: {data: ConversationItem[], has_more: boolean}) => void
onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void
isNoMore: boolean isNoMore: boolean
} }




useInfiniteScroll( useInfiniteScroll(
async () => { async () => {
if(!isNoMore) {
if (!isNoMore) {
const lastId = list[list.length - 1].id const lastId = list[list.length - 1].id
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId) const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId)
onMoreLoaded({ data: conversations, has_more }) onMoreLoaded({ data: conversations, has_more })
} }
return {list: []}
return { list: [] }
}, },
{ {
target: listRef, target: listRef,
isNoMore: () => { isNoMore: () => {
return isNoMore return isNoMore
}, },
reloadDeps: [isNoMore]
reloadDeps: [isNoMore],
}, },
) )


className={ className={
classNames( classNames(
isInstalledApp ? 'tablet:h-[calc(100vh_-_74px)]' : 'tablet:h-[calc(100vh_-_3rem)]', 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"
'shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 mobile:h-screen',
) )
} }
> >

+ 21
- 19
web/app/components/share/text-generation/config-scence/index.tsx Datei anzeigen

<div className='w-full mt-4' key={item.key}> <div className='w-full mt-4' key={item.key}>
<label className='text-gray-900 text-sm font-medium'>{item.name}</label> <label className='text-gray-900 text-sm font-medium'>{item.name}</label>
<div className='mt-2'> <div className='mt-2'>
{item.type === 'select' ? (
<Select
className='w-full'
defaultValue={inputs[item.key]}
onSelect={(i) => { onInputsChange({ ...inputs, [item.key]: i.value }) }}
items={(item.options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
) : (
<input
type="text"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs[item.key]}
onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
{item.type === 'select'
? (
<Select
className='w-full'
defaultValue={inputs[item.key]}
onSelect={(i) => { onInputsChange({ ...inputs, [item.key]: i.value }) }}
items={(item.options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
)
: (
<input
type="text"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs[item.key]}
onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
</div> </div>
</div> </div>
))} ))}

+ 24
- 24
web/config/index.ts Datei anzeigen

const isDevelopment = process.env.NODE_ENV === 'development';
/* eslint-disable import/no-mutable-exports */
const isDevelopment = process.env.NODE_ENV === 'development'


export let apiPrefix = '';
let publicApiPrefix = '';
export let apiPrefix = ''
export let publicApiPrefix = ''


// NEXT_PUBLIC_API_PREFIX=/console/api NEXT_PUBLIC_PUBLIC_API_PREFIX=/api npm run start // NEXT_PUBLIC_API_PREFIX=/console/api NEXT_PUBLIC_PUBLIC_API_PREFIX=/api npm run start
if (process.env.NEXT_PUBLIC_API_PREFIX && process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX) { if (process.env.NEXT_PUBLIC_API_PREFIX && process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX) {
apiPrefix = process.env.NEXT_PUBLIC_API_PREFIX;
publicApiPrefix = process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX;
} else if (
globalThis.document?.body?.getAttribute('data-api-prefix') &&
globalThis.document?.body?.getAttribute('data-pubic-api-prefix')
apiPrefix = process.env.NEXT_PUBLIC_API_PREFIX
publicApiPrefix = process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX
}
else if (
globalThis.document?.body?.getAttribute('data-api-prefix')
&& globalThis.document?.body?.getAttribute('data-pubic-api-prefix')
) { ) {
// Not bulild can not get env from process.env.NEXT_PUBLIC_ in browser https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser // Not bulild can not get env from process.env.NEXT_PUBLIC_ in browser https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser
apiPrefix = globalThis.document.body.getAttribute('data-api-prefix') as string apiPrefix = globalThis.document.body.getAttribute('data-api-prefix') as string
publicApiPrefix = globalThis.document.body.getAttribute('data-pubic-api-prefix') as string publicApiPrefix = globalThis.document.body.getAttribute('data-pubic-api-prefix') as string
} else {
}
else {
if (isDevelopment) { if (isDevelopment) {
apiPrefix = 'https://cloud.dify.dev/console/api';
publicApiPrefix = 'https://dev.udify.app/api';
} else {
apiPrefix = 'https://cloud.dify.dev/console/api'
publicApiPrefix = 'https://dev.udify.app/api'
}
else {
// const domainParts = globalThis.location?.host?.split('.'); // const domainParts = globalThis.location?.host?.split('.');
// in production env, the host is dify.app . In other env, the host is [dev].dify.app // in production env, the host is dify.app . In other env, the host is [dev].dify.app
// const env = domainParts.length === 2 ? 'ai' : domainParts?.[0]; // const env = domainParts.length === 2 ? 'ai' : domainParts?.[0];
apiPrefix = '/console/api';
publicApiPrefix = `/api`; // avoid browser private mode api cross origin
apiPrefix = '/console/api'
publicApiPrefix = '/api' // avoid browser private mode api cross origin
} }
} }



export const API_PREFIX: string = apiPrefix;
export const PUBLIC_API_PREFIX: string = publicApiPrefix;
export const API_PREFIX: string = apiPrefix
export const PUBLIC_API_PREFIX: string = publicApiPrefix


const EDITION = process.env.NEXT_PUBLIC_EDITION || globalThis.document?.body?.getAttribute('data-public-edition') const EDITION = process.env.NEXT_PUBLIC_EDITION || globalThis.document?.body?.getAttribute('data-public-edition')
export const IS_CE_EDITION = EDITION === 'SELF_HOSTED' export const IS_CE_EDITION = EDITION === 'SELF_HOSTED'


export const DEFAULT_VALUE_MAX_LEN = 48 export const DEFAULT_VALUE_MAX_LEN = 48


export const zhRegex = /^[\u4e00-\u9fa5]$/m
export const zhRegex = /^[\u4E00-\u9FA5]$/m
export const emojiRegex = /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/m export const emojiRegex = /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/m
export const emailRegex = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$/m export const emailRegex = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$/m
const MAX_ZN_VAR_NAME_LENGHT = 8 const MAX_ZN_VAR_NAME_LENGHT = 8
const MAX_EN_VAR_VALUE_LENGHT = 16 const MAX_EN_VAR_VALUE_LENGHT = 16
export const getMaxVarNameLength = (value: string) => { export const getMaxVarNameLength = (value: string) => {
if (zhRegex.test(value)) {
if (zhRegex.test(value))
return MAX_ZN_VAR_NAME_LENGHT return MAX_ZN_VAR_NAME_LENGHT
}
return MAX_EN_VAR_VALUE_LENGHT return MAX_EN_VAR_VALUE_LENGHT
} }


name: '', name: '',
type: 'string', type: 'string',
max_length: DEFAULT_VALUE_MAX_LEN, max_length: DEFAULT_VALUE_MAX_LEN,
required: true
required: true,
} }


export const appDefaultIconBackground = '#D5F5F6' export const appDefaultIconBackground = '#D5F5F6'


export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList' export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList'




+ 2
- 1
web/context/dataset-detail.ts Datei anzeigen

import { createContext } from 'use-context-selector' import { createContext } from 'use-context-selector'
import type { DataSet } from '@/models/datasets'


const DatasetDetailContext = createContext<{ indexingTechnique?: string; }>({})
const DatasetDetailContext = createContext<{ indexingTechnique?: string; dataset?: DataSet }>({})


export default DatasetDetailContext export default DatasetDetailContext

+ 1
- 1
web/context/explore-context.ts Datei anzeigen

import { createContext } from 'use-context-selector' import { createContext } from 'use-context-selector'
import { InstalledApp } from '@/models/explore'
import type { InstalledApp } from '@/models/explore'


type IExplore = { type IExplore = {
controlUpdateInstalledApps: number controlUpdateInstalledApps: number

+ 1
- 1
web/i18n/lang/app.en.ts Datei anzeigen

emoji: { emoji: {
ok: 'OK', ok: 'OK',
cancel: 'Cancel', cancel: 'Cancel',
}
},
} }


export default translation export default translation

+ 1
- 1
web/i18n/lang/app.zh.ts Datei anzeigen

emoji: { emoji: {
ok: '确认', ok: '确认',
cancel: '取消', cancel: '取消',
}
},
} }


export default translation export default translation

+ 2
- 0
web/i18n/lang/dataset-creation.en.ts Datei anzeigen

fileName: 'Preprocess document', fileName: 'Preprocess document',
lastStep: 'Last step', lastStep: 'Last step',
nextStep: 'Save & Process', nextStep: 'Save & Process',
save: 'Save & Process',
cancel: 'Cancel',
sideTipTitle: 'Why segment and preprocess?', sideTipTitle: 'Why segment and preprocess?',
sideTipP1: 'When processing text data, segmentation and cleaning are two important preprocessing steps.', sideTipP1: 'When processing text data, segmentation and cleaning are two important preprocessing steps.',
sideTipP2: 'Segmentation splits long text into paragraphs so models can understand better. This improves the quality and relevance of model results.', sideTipP2: 'Segmentation splits long text into paragraphs so models can understand better. This improves the quality and relevance of model results.',

+ 2
- 0
web/i18n/lang/dataset-creation.zh.ts Datei anzeigen

fileName: '预处理文档', fileName: '预处理文档',
lastStep: '上一步', lastStep: '上一步',
nextStep: '保存并处理', nextStep: '保存并处理',
save: '保存并处理',
cancel: '取消',
sideTipTitle: '为什么要分段和预处理?', sideTipTitle: '为什么要分段和预处理?',
sideTipP1: '在处理文本数据时,分段和清洗是两个重要的预处理步骤。', sideTipP1: '在处理文本数据时,分段和清洗是两个重要的预处理步骤。',
sideTipP2: '分段的目的是将长文本拆分成较小的段落,以便模型更有效地处理和理解。这有助于提高模型生成的结果的质量和相关性。', sideTipP2: '分段的目的是将长文本拆分成较小的段落,以便模型更有效地处理和理解。这有助于提高模型生成的结果的质量和相关性。',

+ 7
- 7
web/i18n/lang/explore.en.ts Datei anzeigen

delete: { delete: {
title: 'Delete app', title: 'Delete app',
content: 'Are you sure you want to delete this app?', content: 'Are you sure you want to delete this app?',
}
},
}, },
apps: { apps: {
title: 'Explore Apps by Dify', title: 'Explore Apps by Dify',
nameRequired: 'App name is required', nameRequired: 'App name is required',
}, },
category: { category: {
'Assistant': 'Assistant',
'Writing': 'Writing',
'Translate': 'Translate',
'Programming': 'Programming',
'HR': 'HR',
}
Assistant: 'Assistant',
Writing: 'Writing',
Translate: 'Translate',
Programming: 'Programming',
HR: 'HR',
},
} }


export default translation export default translation

+ 7
- 7
web/i18n/lang/explore.zh.ts Datei anzeigen

delete: { delete: {
title: '删除程序', title: '删除程序',
content: '您确定要删除此程序吗?', content: '您确定要删除此程序吗?',
}
},
}, },
apps: { apps: {
title: '探索 Dify 的应用', title: '探索 Dify 的应用',
nameRequired: '应用程序名称不能为空', nameRequired: '应用程序名称不能为空',
}, },
category: { category: {
'Assistant': '助手',
'Writing': '写作',
'Translate': '翻译',
'Programming': '编程',
'HR': '人力资源',
}
Assistant: '助手',
Writing: '写作',
Translate: '翻译',
Programming: '编程',
HR: '人力资源',
},
} }


export default translation export default translation

+ 2
- 2
web/models/common.ts Datei anzeigen



export enum ProviderName { export enum ProviderName {
OPENAI = 'openai', OPENAI = 'openai',
AZURE_OPENAI = 'azure_openai'
AZURE_OPENAI = 'azure_openai',
} }
export type ProviderAzureToken = { export type ProviderAzureToken = {
openai_api_base?: string openai_api_base?: string
link: string link: string
} }


export interface IWorkspace {
export type IWorkspace = {
id: string id: string
name: string name: string
plan: string plan: string

+ 20
- 20
web/models/explore.ts Datei anzeigen

import { AppMode } from "./app";
import type { AppMode } from './app'


export type AppBasicInfo = { export type AppBasicInfo = {
id: string;
name: string;
mode: AppMode;
icon: string;
icon_background: string;
id: string
name: string
mode: AppMode
icon: string
icon_background: string
} }


export type App = { 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;
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 = { export type InstalledApp = {
app: AppBasicInfo;
id: string;
app: AppBasicInfo
id: string
uninstallable: boolean uninstallable: boolean
is_pinned: boolean is_pinned: boolean
}
}

+ 6
- 6
web/service/explore.ts Datei anzeigen

import { get, post, del, patch } from './base'
import { del, get, patch, post } from './base'


export const fetchAppList = () => { export const fetchAppList = () => {
return get('/explore/apps') return get('/explore/apps')
} }


export const fetchAppDetail = (id: string) : Promise<any> => {
export const fetchAppDetail = (id: string): Promise<any> => {
return get(`/explore/apps/${id}`) return get(`/explore/apps/${id}`)
} }


export const installApp = (id: string) => { export const installApp = (id: string) => {
return post('/installed-apps', { return post('/installed-apps', {
body: { body: {
app_id: id
}
app_id: id,
},
}) })
} }


export const updatePinStatus = (id: string, isPinned: boolean) => { export const updatePinStatus = (id: string, isPinned: boolean) => {
return patch(`/installed-apps/${id}`, { return patch(`/installed-apps/${id}`, {
body: { body: {
is_pinned: isPinned
}
is_pinned: isPinned,
},
}) })
} }

+ 9
- 9
web/service/share.ts Datei anzeigen

import type { IOnCompleted, IOnData, IOnError } from './base' import type { IOnCompleted, IOnData, IOnError } from './base'
import {
get as consoleGet, post as consolePost, del as consoleDel,
getPublic as get, postPublic as post, ssePost, delPublic as del
import {
del as consoleDel, get as consoleGet, post as consolePost,
delPublic as del, getPublic as get, postPublic as post, ssePost,
} from './base' } from './base'
import type { Feedbacktype } from '@/app/components/app/chat' import type { Feedbacktype } from '@/app/components/app/chat'


export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, getAbortController }: { export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, getAbortController }: {
onData: IOnData onData: IOnData
onCompleted: IOnCompleted onCompleted: IOnCompleted
onError: IOnError,
onError: IOnError
getAbortController?: (abortController: AbortController) => void getAbortController?: (abortController: AbortController) => void
}, isInstalledApp: boolean, installedAppId = '') => { }, isInstalledApp: boolean, installedAppId = '') => {
return ssePost(getUrl('chat-messages', isInstalledApp, installedAppId), { return ssePost(getUrl('chat-messages', isInstalledApp, installedAppId), {
return get('/site') return get('/site')
} }


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 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, isInstalledApp: boolean, installedAppId='') => {
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: '' } }) return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } })
} }


return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/more-like-this`, isInstalledApp, installedAppId), { return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/more-like-this`, isInstalledApp, installedAppId), {
params: { params: {
response_mode: 'blocking', response_mode: 'blocking',
}
},
}) })
} }


} }


export const fetchSavedMessage = async (isInstalledApp: boolean, installedAppId = '') => { export const fetchSavedMessage = async (isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl(`/saved-messages`, isInstalledApp, installedAppId))
return (getAction('get', isInstalledApp))(getUrl('/saved-messages', isInstalledApp, installedAppId))
} }


export const removeMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => { export const removeMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => {

+ 6
- 7
web/types/app.ts Datei anzeigen

} }


export type TextTypeFormItem = { export type TextTypeFormItem = {
label: string,
variable: string,
label: string
variable: string
required: boolean required: boolean
max_length: number max_length: number
} }


export type SelectTypeFormItem = { export type SelectTypeFormItem = {
label: string,
variable: string,
required: boolean,
label: string
variable: string
required: boolean
options: string[] options: string[]
} }
/** /**
'select': SelectTypeFormItem 'select': SelectTypeFormItem
} }



export type ToolItem = { export type ToolItem = {
dataset: { dataset: {
enabled: boolean enabled: boolean
icon: string icon: string
/** Icon Background */ /** Icon Background */
icon_background: string icon_background: string
/** Mode */ /** Mode */
mode: AppMode mode: AppMode
/** Enable web app */ /** Enable web app */

Laden…
Abbrechen
Speichern