Procházet zdrojové kódy

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>
KVOJJJin před 2 roky
rodič
revize
c67f626b66
61 změnil soubory, kde provedl 1169 přidání a 762 odebrání
  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

@@ -208,9 +208,10 @@ class DatasetDocumentListApi(Resource):
         parser = reqparse.RequestParser()
         parser.add_argument('indexing_technique', type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False,
                             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('original_document_id', type=str, required=False, location='json')
         args = parser.parse_args()
 
         if not dataset.indexing_technique and not args['indexing_technique']:
@@ -347,10 +348,12 @@ class DocumentIndexingStatusApi(DocumentResource):
 
         completed_segments = DocumentSegment.query \
             .filter(DocumentSegment.completed_at.isnot(None),
-                    DocumentSegment.document_id == str(document_id)) \
+                    DocumentSegment.document_id == str(document_id),
+                    DocumentSegment.status != 're_segment') \
             .count()
         total_segments = DocumentSegment.query \
-            .filter_by(document_id=str(document_id)) \
+            .filter(DocumentSegment.document_id == str(document_id),
+                    DocumentSegment.status != 're_segment') \
             .count()
 
         document.completed_segments = completed_segments

+ 138 - 39
api/services/dataset_service.py

@@ -12,7 +12,7 @@ from events.dataset_event import dataset_was_deleted
 from events.document_event import document_was_deleted
 from extensions.ext_database import db
 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 services.errors.account import NoPermissionError
 from services.errors.dataset import DatasetNameDuplicateError
@@ -20,6 +20,7 @@ from services.errors.document import DocumentIndexingError
 from services.errors.file import FileNotExistsError
 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_update_task import document_indexing_update_task
 
 
 class DatasetService:
@@ -277,6 +278,14 @@ class DocumentService:
         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
     def get_document_file_detail(file_id: str):
         file_detail = db.session.query(UploadFile). \
             filter(UploadFile.id == file_id). \
@@ -355,8 +364,79 @@ class DocumentService:
         if dataset.indexing_technique == 'high_quality':
             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
-        if not dataset_process_rule:
+        if 'process_rule' in document_data and document_data['process_rule']:
             process_rule = document_data["process_rule"]
             if process_rule["mode"] == "custom":
                 dataset_process_rule = DatasetProcessRule(
@@ -374,46 +454,48 @@ class DocumentService:
                 )
             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)
-        )
-
+            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.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
-        document_indexing_task.delay(document.dataset_id, document.id)
+        document_indexing_update_task.delay(document.dataset_id, document.id)
 
         return document
 
@@ -443,6 +525,21 @@ class DocumentService:
 
     @classmethod
     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']:
             raise ValueError("Data source is required")
 
@@ -459,6 +556,8 @@ class DocumentService:
             if 'info' not in args['data_source'] or not args['data_source']['info']:
                 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']:
             raise ValueError("Process rule is required")
 

+ 1 - 2
api/tasks/clean_document_task.py

@@ -35,8 +35,7 @@ def clean_document_task(document_id: str, dataset_id: str):
         index_node_ids = [segment.index_node_id for segment in segments]
 
         # 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
         if index_node_ids:

+ 85 - 0
api/tasks/document_indexing_update_task.py

@@ -0,0 +1,85 @@
+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

@@ -42,8 +42,7 @@ def remove_document_from_index_task(document_id: str):
         keyword_table_index = KeywordTableIndex(dataset=dataset)
 
         # 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
         segments = db.session.query(DocumentSegment).filter(DocumentSegment.document_id == document.id).all()

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

@@ -21,7 +21,7 @@ export type AppCardProps = {
 
 const AppCard = ({
   app,
-  onDelete
+  onDelete,
 }: AppCardProps) => {
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)

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

@@ -3,13 +3,13 @@
 import { useEffect, useRef } from 'react'
 import useSWRInfinite from 'swr/infinite'
 import { debounce } from 'lodash-es'
+import { useTranslation } from 'react-i18next'
 import AppCard from './AppCard'
 import NewAppCard from './NewAppCard'
-import { AppListResponse } from '@/models/app'
+import type { AppListResponse } from '@/models/app'
 import { fetchAppList } from '@/service/apps'
 import { useSelector } from '@/context/app-context'
 import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
-import { useTranslation } from 'react-i18next'
 
 const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
   if (!pageIndex || previousPageData.has_more)
@@ -25,8 +25,8 @@ const Apps = () => {
   const anchorRef = useRef<HTMLAnchorElement>(null)
 
   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)
       mutate()
     }
@@ -41,9 +41,8 @@ const Apps = () => {
       if (!loadingStateRef.current) {
         const { scrollTop, clientHeight } = pageContainerRef.current!
         const anchorOffset = anchorRef.current!.offsetTop
-        if (anchorOffset - scrollTop - clientHeight < 100) {
+        if (anchorOffset - scrollTop - clientHeight < 100)
           setSize(size => size + 1)
-        }
       }
     }, 50)
 

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

@@ -0,0 +1,16 @@
+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

@@ -164,7 +164,10 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
         extraInfo={<ExtraInfo />}
         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>
       </DatasetDetailContext.Provider>
     </div>

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

@@ -1,20 +1,17 @@
 'use client'
 
-import { useContext, useContextSelector } from 'use-context-selector'
+import { useContext } from 'use-context-selector'
 import Link from 'next/link'
-import useSWR from 'swr'
 import type { MouseEventHandler } from 'react'
 import { useCallback, useState } from 'react'
 import { useTranslation } from 'react-i18next'
+import classNames from 'classnames'
 import style from '../list.module.css'
-import type { App } from '@/types/app'
 import Confirm from '@/app/components/base/confirm'
 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 AppsContext from '@/context/app-context'
-import { DataSet } from '@/models/datasets'
-import classNames from 'classnames'
+import type { DataSet } from '@/models/datasets'
 
 export type DatasetCardProps = {
   dataset: DataSet
@@ -23,7 +20,7 @@ export type DatasetCardProps = {
 
 const DatasetCard = ({
   dataset,
-  onDelete
+  onDelete,
 }: DatasetCardProps) => {
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)

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

@@ -2,12 +2,12 @@
 
 import { useEffect, useRef } from 'react'
 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 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) => {
   if (!pageIndex || previousPageData.has_more)
@@ -30,9 +30,8 @@ const Datasets = () => {
       if (!loadingStateRef.current) {
         const { scrollTop, clientHeight } = pageContainerRef.current!
         const anchorOffset = anchorRef.current!.offsetTop
-        if (anchorOffset - scrollTop - clientHeight < 100) {
+        if (anchorOffset - scrollTop - clientHeight < 100)
           setSize(size => size + 1)
-        }
       }
     }, 50)
 
@@ -43,7 +42,7 @@ const Datasets = () => {
   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'>
       {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} />
     </nav>
@@ -51,4 +50,3 @@ const Datasets = () => {
 }
 
 export default Datasets
-

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

@@ -1,7 +1,7 @@
-import AppList from "@/app/components/explore/app-list"
 import React from 'react'
+import AppList from '@/app/components/explore/app-list'
 
-const Apps = ({ }) => {
+const Apps = () => {
   return <AppList />
 }
 

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

@@ -1,13 +1,14 @@
-import React, { FC } from 'react'
+import type { FC } from 'react'
+import React from 'react'
 import Main from '@/app/components/explore/installed-app'
 
-export interface IInstalledAppProps { 
+export type IInstalledAppProps = {
   params: {
     appId: string
   }
 }
 
-const InstalledApp: FC<IInstalledAppProps> = ({ params: {appId} }) => {
+const InstalledApp: FC<IInstalledAppProps> = ({ params: { appId } }) => {
   return (
     <Main id={appId} />
   )

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

@@ -5,7 +5,6 @@ import type { IMainProps } from '@/app/components/share/chat'
 import Main from '@/app/components/share/chat'
 
 const Chat: FC<IMainProps> = () => {
-
   return (
     <Main />
   )

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

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

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

@@ -1,19 +1,19 @@
 'use client'
-import React, { FC, useEffect } from 'react'
+import type { FC } from 'react'
+import React, { useEffect } from 'react'
 import cn from 'classnames'
 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 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 { fetchDatasets } from '@/service/datasets'
 import Loading from '@/app/components/base/loading'
 import { formatNumber } from '@/utils/format'
-import Link from 'next/link'
-
-import s from './style.module.css'
 
-export interface ISelectDataSetProps {
+export type ISelectDataSetProps = {
   isShow: boolean
   onClose: () => void
   selectedIds: string[]
@@ -37,20 +37,19 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
       const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } })
       setDataSets(data)
       setLoaded(true)
-      setSelected(data.filter((item) => selectedIds.includes(item.id)))
+      setSelected(data.filter(item => selectedIds.includes(item.id)))
     })()
   }, [])
   const toggleSelect = (dataSet: DataSet) => {
-    const isSelected = selected.some((item) => item.id === dataSet.id)
+    const isSelected = selected.some(item => item.id === dataSet.id)
     if (isSelected) {
-      setSelected(selected.filter((item) => item.id !== dataSet.id))
+      setSelected(selected.filter(item => item.id !== dataSet.id))
     }
     else {
-      if (canSelectMulti) {
+      if (canSelectMulti)
         setSelected([...selected, dataSet])
-      } else {
+      else
         setSelected([dataSet])
-      }
     }
   }
 
@@ -74,7 +73,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
         <div className='flex items-center justify-center mt-6 rounded-lg space-x-1  h-[128px] text-[13px] border'
           style={{
             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>
@@ -85,7 +84,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
       {datasets && datasets?.length > 0 && (
         <>
           <div className='mt-7 space-y-1 max-h-[286px] overflow-y-auto'>
-            {datasets.map((item) => (
+            {datasets.map(item => (
               <div
                 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')}

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

@@ -1,11 +1,13 @@
+/* eslint-disable multiline-ternary */
 '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 { useContext } from 'use-context-selector'
-import ConfigContext from '@/context/debug-configuration'
 import produce from 'immer'
 import { useTranslation } from 'react-i18next'
 import { useBoolean } from 'ahooks'
+import ConfigContext from '@/context/debug-configuration'
 import Panel from '@/app/components/app/configuration/base/feature-panel'
 import Button from '@/app/components/base/button'
 import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
@@ -14,7 +16,7 @@ import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/conf
 import { getNewVar } from '@/utils/var'
 import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight'
 
-export interface IOpeningStatementProps {
+export type IOpeningStatementProps = {
   promptTemplate: string
   value: string
   onChange: (value: string) => void
@@ -25,7 +27,7 @@ const regex = /\{\{([^}]+)\}\}/g
 
 const OpeningStatement: FC<IOpeningStatementProps> = ({
   value = '',
-  onChange
+  onChange,
 }) => {
   const { t } = useTranslation()
   const {
@@ -60,8 +62,6 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
     .replace(/>/g, '&gt;')
     .replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
     .replace(/\n/g, '<br />')
-    
-
 
   const handleEdit = () => {
     setFocus()
@@ -76,15 +76,15 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
 
   const handleConfirm = () => {
     const keys = getInputKeys(tempValue)
-    const promptKeys = promptVariables.map((item) => item.key)
+    const promptKeys = promptVariables.map(item => item.key)
     let notIncludeKeys: string[] = []
 
     if (promptKeys.length === 0) {
-      if (keys.length > 0) {
+      if (keys.length > 0)
         notIncludeKeys = keys
-      }
-    } else {
-      notIncludeKeys = keys.filter((key) => !promptKeys.includes(key))
+    }
+    else {
+      notIncludeKeys = keys.filter(key => !promptKeys.includes(key))
     }
 
     if (notIncludeKeys.length > 0) {
@@ -104,7 +104,7 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
 
   const autoAddVar = () => {
     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)
     setModelConfig(newModelConfig)
@@ -130,26 +130,30 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
       isFocus={isFocus}
     >
       <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='text-xs text-gray-500'>{t('appDebug.openingStatement.varTip')}</div>
 
@@ -160,9 +164,9 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
               </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 && (
           <ConfirmAddVar

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

@@ -6,12 +6,12 @@ import { useContext } from 'use-context-selector'
 import {
   PlayIcon,
 } from '@heroicons/react/24/solid'
+import VarIcon from '../base/icons/var-icon'
 import ConfigContext from '@/context/debug-configuration'
 import type { PromptVariable } from '@/models/debug'
 import { AppType } from '@/types/app'
 import Select from '@/app/components/base/select'
 import { DEFAULT_VALUE_MAX_LEN } from '@/config'
-import VarIcon from '../base/icons/var-icon'
 import Button from '@/app/components/base/button'
 
 export type IPromptValuePanelProps = {
@@ -71,17 +71,19 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
         </div>
         <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>
@@ -105,37 +107,41 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
           )}
         </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>
 

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

@@ -1,18 +1,19 @@
 'use client'
-import React, { FC, useState } from 'react'
+import type { FC } from 'react'
+import React, { useState } from 'react'
 import { useTranslation } from 'react-i18next'
 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 Toast from '@/app/components/base/toast'
-import { Feedbacktype } from '@/app/components/app/chat'
 import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
 import { useBoolean } from 'ahooks'
+import { 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'
 
 const MAX_DEPTH = 3
-export interface IGenerationItemProps {
+export type IGenerationItemProps = {
   className?: string
   content: string
   messageId?: string | null
@@ -24,13 +25,13 @@ export interface IGenerationItemProps {
   onFeedback?: (feedback: Feedbacktype) => void
   onSave?: (messageId: string) => void
   isMobile?: boolean
-  isInstalledApp: boolean,
-  installedAppId?: string,
+  isInstalledApp: boolean
+  installedAppId?: string
 }
 
 export const SimpleBtn = ({ className, onClick, children }: {
   className?: string
-  onClick?: () => void,
+  onClick?: () => void
   children: React.ReactNode
 }) => (
   <div
@@ -88,7 +89,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
   const [childMessageId, setChildMessageId] = useState<string | null>(null)
   const hasChild = !!childMessageId
   const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
-    rating: null
+    rating: null,
   })
 
   const handleFeedback = async (childFeedback: Feedbacktype) => {
@@ -126,115 +127,120 @@ const GenerationItem: FC<IGenerationItemProps> = ({
   }
 
   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)'
-    }
+
     return res
   })()
   return (
     <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
                         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>
-                    )}
-                    {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 className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
-            </div>
-          )}
-
-        </div>
-      )}
+            )}
 
+          </div>
+        )}
 
       {((childMessageId || isQuerying) && depth < 3) && (
         <div className='pl-4'>

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

@@ -1,9 +1,9 @@
 import type { FC } from 'react'
 import classNames from 'classnames'
-import style from './style.module.css'
 
 import data from '@emoji-mart/data'
 import { init } from 'emoji-mart'
+import style from './style.module.css'
 
 init({ data })
 
@@ -39,7 +39,7 @@ const AppIcon: FC<AppIconProps> = ({
       }}
       onClick={onClick}
     >
-      {innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id='🤖' />}
+      {innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />)}
     </span>
   )
 }

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

@@ -3,11 +3,11 @@
 import type { ChangeEvent, FC } from 'react'
 import React, { useCallback, useEffect, useRef, useState } from 'react'
 import classNames from 'classnames'
-import { checkKeys } from '@/utils/var'
 import { useTranslation } from 'react-i18next'
-import Button from '@/app/components/base/button'
 import Toast from '../toast'
 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
 const regex = /\{\{([^}]+)\}\}/g
@@ -55,9 +55,9 @@ const BlockInput: FC<IBlockInputProps> = ({
   useEffect(() => {
     if (isEditing && contentEditableRef.current) {
       // TODO: Focus at the click positon
-      if (currentValue) {
+      if (currentValue)
         contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length)
-      }
+
       contentEditableRef.current.focus()
     }
   }, [isEditing])
@@ -72,7 +72,6 @@ const BlockInput: FC<IBlockInputProps> = ({
     .replace(/>/g, '&gt;')
     .replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
     .replace(/\n/g, '<br />')
-    
 
   // Not use useCallback. That will cause out callback get old data.
   const handleSubmit = () => {
@@ -83,7 +82,7 @@ const BlockInput: FC<IBlockInputProps> = ({
       if (!isValid) {
         Toast.notify({
           type: 'error',
-          message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey })
+          message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
         })
         return
       }
@@ -125,9 +124,9 @@ const BlockInput: FC<IBlockInputProps> = ({
             value={currentValue}
             onBlur={() => {
               blur()
-              if (!isContentChanged) {
+              if (!isContentChanged)
                 setIsEditing(false)
-              }
+
               // click confirm also make blur. Then outter value is change. So below code has problem.
               // setTimeout(() => {
               //   handleCancel()
@@ -143,31 +142,33 @@ const BlockInput: FC<IBlockInputProps> = ({
       {textAreaContent}
       {/* footer */}
       <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>

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

@@ -1,26 +1,28 @@
+/* eslint-disable multiline-ternary */
 '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 { init, SearchIndex } from 'emoji-mart'
+import { SearchIndex, init } from 'emoji-mart'
 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 {
-  MagnifyingGlassIcon
+  MagnifyingGlassIcon,
 } 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 { useTranslation } from 'react-i18next'
 
 declare global {
   namespace JSX {
+    // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
     interface IntrinsicElements {
       'em-emoji': React.DetailedHTMLProps<
-        React.HTMLAttributes<HTMLElement>,
-        HTMLElement
-      >;
+      React.HTMLAttributes<HTMLElement>,
+      HTMLElement
+      >
     }
   }
 }
@@ -57,7 +59,7 @@ const backgroundColors = [
   '#ECE9FE',
   '#FFE4E8',
 ]
-interface IEmojiPickerProps {
+type IEmojiPickerProps = {
   isModal?: boolean
   onSelect?: (emoji: string, background: string) => void
   onClose?: () => void
@@ -66,7 +68,7 @@ interface IEmojiPickerProps {
 const EmojiPicker: FC<IEmojiPickerProps> = ({
   isModal = true,
   onSelect,
-  onClose
+  onClose,
 
 }) => {
   const { t } = useTranslation()
@@ -97,8 +99,8 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
           onChange={async (e: ChangeEvent<HTMLInputElement>) => {
             if (e.target.value === '') {
               setIsSearching(false)
-              return
-            } else {
+            }
+            else {
               setIsSearching(true)
               const emojis = await search(e.target.value)
               setSearchedEmojis(emojis)
@@ -111,7 +113,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
 
     <div className="w-full max-h-[200px] overflow-x-hidden overflow-y-auto px-3">
       {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>
           <div className='w-full h-full grid grid-cols-8 gap-1'>
             {searchedEmojis.map((emoji: string, index: number) => {
@@ -131,7 +133,6 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
         </div>
       </>}
 
-
       {categories.map((category: any, index: number) => {
         return <div key={`category-${index}`} className='flex flex-col'>
           <p className='font-medium uppercase text-xs text-[#101828] mb-1'>{category.id}</p>
@@ -156,7 +157,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
     </div>
 
     {/* 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>
       <div className='w-full h-full grid grid-cols-8 gap-1'>
         {backgroundColors.map((color) => {
@@ -165,9 +166,9 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
             className={
               cn(
                 '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',
-                color === selectedBackground ? `ring-1 ring-gray-300` : '',
+                color === selectedBackground ? 'ring-1 ring-gray-300' : '',
               )}
             onClick={() => {
               setSelectedBackground(color)
@@ -191,7 +192,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
         {t('app.emoji.cancel')}
       </Button>
       <Button
-        disabled={selectedEmoji == ''}
+        disabled={selectedEmoji === ''}
         type="primary"
         className='w-full'
         onClick={() => {

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

@@ -9,7 +9,7 @@ import {
   createDocument,
   fetchFileIndexingEstimate as didFetchFileIndexingEstimate,
 } 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 PreviewItem from './preview-item'
 import Loading from '@/app/components/base/loading'
@@ -22,14 +22,18 @@ import Toast from '@/app/components/base/toast'
 import { formatNumber } from '@/utils/format'
 
 type StepTwoProps = {
+  isSetting?: boolean,
+  documentDetail?: FullDocumentDetail
   hasSetAPIKEY: boolean,
   onSetting: () => void,
   datasetId?: string,
   indexingType?: string,
   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 {
@@ -42,6 +46,8 @@ enum IndexingType {
 }
 
 const StepTwo = ({
+  isSetting,
+  documentDetail,
   hasSetAPIKEY,
   onSetting,
   datasetId,
@@ -50,6 +56,8 @@ const StepTwo = ({
   onStepChange,
   updateIndexingTypeCache,
   updateResultCache,
+  onSave,
+  onCancel,
 }: StepTwoProps) => {
   const { t } = useTranslation()
   const scrollRef = useRef<HTMLDivElement>(null)
@@ -171,15 +179,23 @@ const StepTwo = ({
   }
 
   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
   }
 
@@ -196,6 +212,25 @@ const StepTwo = ({
       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 () => {
     try {
       let res;
@@ -204,19 +239,20 @@ const StepTwo = ({
         res = await createFirstDocument({
           body: params
         })
-        updateIndexingTypeCache(indexType)
-        updateResultCache(res)
+        updateIndexingTypeCache && updateIndexingTypeCache(indexType)
+        updateResultCache && updateResultCache(res)
       } else {
         res = await createDocument({
           datasetId,
           body: params
         })
-        updateIndexingTypeCache(indexType)
-        updateResultCache({
+        updateIndexingTypeCache && updateIndexingTypeCache(indexType)
+        updateResultCache && updateResultCache({
           document: res,
         })
       }
-      onStepChange(+1)
+      onStepChange && onStepChange(+1)
+      isSetting && onSave && onSave()
     }
     catch (err) {
       Toast.notify({
@@ -228,7 +264,12 @@ const StepTwo = ({
 
   useEffect(() => {
     // fetch rules
-    getRules()
+    if (!isSetting) {
+      getRules()
+    } else {
+      getRulesFromDetail()
+      getDefaultMode()
+    }
   }, [])
 
   useEffect(() => {
@@ -444,11 +485,18 @@ const StepTwo = ({
                 </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>

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

@@ -19,7 +19,7 @@ import type { FullDocumentDetail, ProcessRuleResponse } from '@/models/datasets'
 import type { CommonResponse } from '@/models/common'
 import { asyncRunSafe } from '@/utils'
 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 StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal'
 
@@ -118,14 +118,45 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
   const localDocumentId = docId ?? documentId
   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({
     action: 'fetchIndexingEstimate',
@@ -168,7 +199,7 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
     const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId: localDatasetId, documentId: localDocumentId }) as Promise<CommonResponse>)
     if (!e) {
       notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
-      statusMutate()
+      setIndexingStatusDetail(null)
     }
     else {
       notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })

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

@@ -0,0 +1,90 @@
+'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

@@ -95,6 +95,7 @@ export const OperationAction: FC<{
   const [showModal, setShowModal] = useState(false)
   const { notify } = useContext(ToastContext)
   const { t } = useTranslation()
+  const router = useRouter()
 
   const isListScene = scene === 'list'
 
@@ -166,15 +167,19 @@ export const OperationAction: FC<{
             </div>
             <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')}>
             <ArchiveIcon />
             <span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>

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

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

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

@@ -1,15 +1,15 @@
 'use client'
-import { Dispatch, SetStateAction, useEffect, useState } from 'react'
+import { useEffect, useState } from 'react'
 import useSWR from 'swr'
 import { useContext } from 'use-context-selector'
 import { BookOpenIcon } from '@heroicons/react/24/outline'
 import { useTranslation } from 'react-i18next'
-import { ToastContext } from '@/app/components/base/toast'
 import PermissionsRadio from '../permissions-radio'
 import IndexMethodRadio from '../index-method-radio'
+import { ToastContext } from '@/app/components/base/toast'
 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 = `
   flex justify-between py-4
@@ -20,8 +20,7 @@ const labelClass = `
 const inputClass = `
   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(() => {
     dispatch(depend)
   }, [depend])
@@ -32,7 +31,7 @@ type Props = {
 }
 
 const Form = ({
-  datasetId
+  datasetId,
 }: Props) => {
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
@@ -44,7 +43,8 @@ const Form = ({
   const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
 
   const handleSave = async () => {
-    if (loading) return
+    if (loading)
+      return
     if (!name?.trim()) {
       notify({ type: 'error', message: t('datasetSettings.form.nameError') })
       return
@@ -57,14 +57,16 @@ const Form = ({
           name,
           description,
           permission,
-          indexing_technique: indexMethod
-        }
+          indexing_technique: indexMethod,
+        },
       })
       notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
       await mutateDatasets()
-    } catch (e) {
+    }
+    catch (e) {
       notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })
-    } finally {
+    }
+    finally {
       setLoading(false)
     }
   }
@@ -142,4 +144,4 @@ const Form = ({
   )
 }
 
-export default Form
+export default Form

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

@@ -1,9 +1,9 @@
 'use client'
 import React, { useEffect, useState } from 'react'
 import copy from 'copy-to-clipboard'
-import Tooltip from '@/app/components/base/tooltip'
 import { t } from 'i18next'
 import s from './style.module.css'
+import Tooltip from '@/app/components/base/tooltip'
 
 type IInputCopyProps = {
   value?: string

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

@@ -1,22 +1,22 @@
 'use client'
-import React, { FC, useEffect } from 'react'
+import type { FC } from 'react'
+import React, { useEffect } from 'react'
 import { useRouter } from 'next/navigation'
 import { useTranslation } from 'react-i18next'
 import { useContext } from 'use-context-selector'
+import Toast from '../../base/toast'
+import s from './style.module.css'
 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 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 CreateAppModal from '@/app/components/explore/create-app-modal'
 import Loading from '@/app/components/base/loading'
 import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
 
-import s from './style.module.css'
-import Toast from '../../base/toast'
-
-const Apps: FC = ({ }) => {
+const Apps: FC = () => {
   const { t } = useTranslation()
   const router = useRouter()
   const { setControlUpdateInstalledApps, hasEditPermission } = useContext(ExploreContext)
@@ -25,13 +25,14 @@ const Apps: FC = ({ }) => {
   const [isLoaded, setIsLoaded] = React.useState(false)
 
   const currList = (() => {
-    if(currCategory === '') return allList
+    if (currCategory === '')
+      return allList
     return allList.filter(item => item.category === currCategory)
   })()
   const [categories, setCategories] = React.useState([])
   useEffect(() => {
     (async () => {
-      const {categories, recommended_apps}:any = await fetchAppList()
+      const { categories, recommended_apps }: any = await fetchAppList()
       setCategories(categories)
       setAllList(recommended_apps)
       setIsLoaded(true)
@@ -49,9 +50,9 @@ const Apps: FC = ({ }) => {
 
   const [currApp, setCurrApp] = React.useState<App | null>(null)
   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)
-    
+
     try {
       const app = await createApp({
         name,
@@ -67,12 +68,13 @@ const Apps: FC = ({ }) => {
       })
       localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
       router.push(`/app/${app.id}/overview`)
-    } catch (e) {
+    }
+    catch (e) {
       Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
     }
   }
 
-  if(!isLoaded) {
+  if (!isLoaded) {
     return (
       <div className='flex h-full items-center'>
         <Loading type='area' />
@@ -92,16 +94,16 @@ const Apps: FC = ({ }) => {
         value={currCategory}
         onChange={setCurrCategory}
       />
-      <div 
+      <div
         className='flex mt-6 flex-col overflow-auto bg-gray-100 shrink-0 grow'
         style={{
-          maxHeight: 'calc(100vh - 243px)'
+          maxHeight: 'calc(100vh - 243px)',
         }}
       >
         <nav
           className={`${s.appList} grid content-start grid-cols-1 gap-4 px-12 pb-10grow shrink-0`}>
           {currList.map(app => (
-            <AppCard 
+            <AppCard
               key={app.app_id}
               app={app}
               canCreate={hasEditPermission}
@@ -116,13 +118,13 @@ const Apps: FC = ({ }) => {
       </div>
 
       {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>
   )
 }

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

@@ -1,12 +1,13 @@
 'use client'
-import React, { FC } from 'react'
+import type { FC } from 'react'
+import React from 'react'
 import { useTranslation } from 'react-i18next'
-import exploreI18n from '@/i18n/lang/explore.en'
 import cn from 'classnames'
+import exploreI18n from '@/i18n/lang/explore.en'
 
 const categoryI18n = exploreI18n.category
 
-export interface ICategoryProps {
+export type ICategoryProps = {
   className?: string
   list: string[]
   value: string
@@ -17,23 +18,23 @@ const Category: FC<ICategoryProps> = ({
   className,
   list,
   value,
-  onChange
+  onChange,
 }) => {
   const { t } = useTranslation()
 
-  const itemClassName = (isSelected: boolean) => cn(isSelected ? 'bg-white text-primary-600 border-gray-200 font-semibold' : 'border-transparent font-medium','flex items-center h-7 px-3 border cursor-pointer rounded-lg')
-  const itemStyle = (isSelected: boolean) => isSelected ? {boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'} : {}
+  const itemClassName = (isSelected: boolean) => cn(isSelected ? 'bg-white text-primary-600 border-gray-200 font-semibold' : 'border-transparent font-medium', 'flex items-center h-7 px-3 border cursor-pointer rounded-lg')
+  const itemStyle = (isSelected: boolean) => isSelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}
   return (
     <div className={cn(className, 'flex space-x-1 text-[13px]')}>
-      <div 
-          className={itemClassName('' === value)}
-          style={itemStyle('' === value)}
-          onClick={() => onChange('')}
-        >
-          {t('explore.apps.allCategories')}
-        </div>
+      <div
+        className={itemClassName(value === '')}
+        style={itemStyle(value === '')}
+        onClick={() => onChange('')}
+      >
+        {t('explore.apps.allCategories')}
+      </div>
       {list.map(name => (
-        <div 
+        <div
           key={name}
           className={itemClassName(name === value)}
           style={itemStyle(name === value)}

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

@@ -2,19 +2,18 @@
 import React, { useState } from 'react'
 import cn from 'classnames'
 import { useTranslation } from 'react-i18next'
+import s from './style.module.css'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
 import Toast from '@/app/components/base/toast'
 import AppIcon from '@/app/components/base/app-icon'
 import EmojiPicker from '@/app/components/base/emoji-picker'
 
-import s from './style.module.css'
-
 type IProps = {
-  appName: string,
-  show: boolean,
-  onConfirm: (info: any) => void,
-  onHide: () => void,
+  appName: string
+  show: boolean
+  onConfirm: (info: any) => void
+  onHide: () => void
 }
 
 const CreateAppModal = ({
@@ -31,7 +30,7 @@ const CreateAppModal = ({
   const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
 
   const submit = () => {
-    if(!name.trim()) {
+    if (!name.trim()) {
       Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') })
       return
     }
@@ -44,42 +43,42 @@ const CreateAppModal = ({
 
   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 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

@@ -1,18 +1,19 @@
 '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 Sidebar from '@/app/components/explore/sidebar'
 import { useAppContext } from '@/context/app-context'
 import { fetchMembers } from '@/service/common'
-import { InstalledApp } from '@/models/explore'
-import { useTranslation } from 'react-i18next'
+import type { InstalledApp } from '@/models/explore'
 
-export interface IExploreProps {
+export type IExploreProps = {
   children: React.ReactNode
 }
 
 const Explore: FC<IExploreProps> = ({
-  children
+  children,
 }) => {
   const { t } = useTranslation()
   const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
@@ -23,8 +24,9 @@ const Explore: FC<IExploreProps> = ({
   useEffect(() => {
     document.title = `${t('explore.title')} -  Dify`;
     (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)
       setHasEditPermission(currUser?.role !== 'normal')
     })()
@@ -39,7 +41,7 @@ const Explore: FC<IExploreProps> = ({
             setControlUpdateInstalledApps,
             hasEditPermission,
             installedApps,
-            setInstalledApps
+            setInstalledApps,
           }
         }
       >

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

@@ -1,12 +1,13 @@
 'use client'
-import React, { FC } from 'react'
+import type { FC } from 'react'
+import React from 'react'
 import { useContext } from 'use-context-selector'
 import ExploreContext from '@/context/explore-context'
 import ChatApp from '@/app/components/share/chat'
 import TextGenerationApp from '@/app/components/share/text-generation'
 import Loading from '@/app/components/base/loading'
 
-export interface IInstalledAppProps {
+export type IInstalledAppProps = {
   id: string
 }
 
@@ -14,23 +15,25 @@ const InstalledApp: FC<IInstalledAppProps> = ({
   id,
 }) => {
   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 (
       <div className='flex h-full items-center'>
         <Loading type='area' />
       </div>
     )
   }
-  
+
   return (
     <div className='h-full p-2'>
-      {installedApp?.app.mode === 'chat' ? (
-        <ChatApp isInstalledApp installedAppInfo={installedApp}/>
-      ): (
-        <TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
-      )}
+      {installedApp?.app.mode === 'chat'
+        ? (
+          <ChatApp isInstalledApp installedAppInfo={installedApp}/>
+        )
+        : (
+          <TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
+        )}
     </div>
   )
 }

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

@@ -1,11 +1,12 @@
 'use client'
-import React, { FC } from 'react'
+import type { FC } from 'react'
+import React from 'react'
 import cn from 'classnames'
 import { useTranslation } from 'react-i18next'
-import Popover from '@/app/components/base/popover'
 import { TrashIcon } from '@heroicons/react/24/outline'
 
 import s from './style.module.css'
+import Popover from '@/app/components/base/popover'
 
 const PinIcon = (
   <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -13,7 +14,7 @@ const PinIcon = (
   </svg>
 )
 
-export interface IItemOperationProps {
+export type IItemOperationProps = {
   className?: string
   isPinned: boolean
   isShowDelete: boolean
@@ -26,7 +27,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
   isPinned,
   isShowDelete,
   togglePin,
-  onDelete
+  onDelete,
 }) => {
   const { t } = useTranslation()
 
@@ -42,18 +43,18 @@ const ItemOperation: FC<IItemOperationProps> = ({
           </div>
           {isShowDelete && (
             <div className={cn(s.actionItem, s.deleteActionItem, 'hover:bg-gray-50 group')} onClick={onDelete} >
-            <TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
-            <span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('explore.sidebar.action.delete')}</span>
-          </div>
+              <TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
+              <span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('explore.sidebar.action.delete')}</span>
+            </div>
           )}
-          
+
         </div>
       }
       trigger='click'
       position='br'
       btnElement={<div />}
-      btnClassName={(open) => cn(className, s.btn, 'h-6 w-6 rounded-md border-none p-1', open && '!bg-gray-100 !shadow-none')}
-      className={`!w-[120px] h-fit !z-20`}
+      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

@@ -1,12 +1,11 @@
 'use client'
 import cn from 'classnames'
 import { useRouter } from 'next/navigation'
+import s from './style.module.css'
 import ItemOperation from '@/app/components/explore/item-operation'
 import AppIcon from '@/app/components/base/app-icon'
 
-import s from './style.module.css'
-
-export interface IAppNavItemProps {
+export type IAppNavItemProps = {
   name: string
   id: string
   icon: string
@@ -31,7 +30,7 @@ export default function AppNavItem({
 }: IAppNavItemProps) {
   const router = useRouter()
   const url = `/explore/installed/${id}`
-  
+
   return (
     <div
       key={id}
@@ -40,7 +39,7 @@ export default function AppNavItem({
         isSelected ? s.active : 'hover:bg-gray-200',
         '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().
       }}
     >
@@ -53,7 +52,7 @@ export default function AppNavItem({
             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>
       {

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

@@ -1,92 +1,90 @@
-import type { Provider, ProviderAzureToken } from '@/models/common'
-import { ProviderName } from '@/models/common'
 import { useTranslation } from 'react-i18next'
 import Link from 'next/link'
 import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
-import { useState, useEffect } from 'react'
+import { useEffect, useState } from 'react'
 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,
   ValidatingTip,
-  ValidatedErrorOnAzureOpenaiTip
 } from '../provider-input/Validate'
+import { ProviderName } from '@/models/common'
+import type { Provider, ProviderAzureToken } from '@/models/common'
 
-interface IAzureProviderProps {
+type IAzureProviderProps = {
   provider: Provider
   onValidatedStatus: (status?: ValidatedStatusState) => void
   onTokenChange: (token: ProviderAzureToken) => void
 }
 const AzureProvider = ({
-  provider, 
+  provider,
   onTokenChange,
-  onValidatedStatus
+  onValidatedStatus,
 }: IAzureProviderProps) => {
   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) => {
     if (token[type] === (provider?.token as ProviderAzureToken)[type]) {
       token[type] = ''
-      setToken({...token})
-      onTokenChange({...token})
+      setToken({ ...token })
+      onTokenChange({ ...token })
       setValidatedStatus({})
     }
   }
   const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => {
     token[type] = v
-    setToken({...token})
-    onTokenChange({...token})
-    validate({...token}, {
+    setToken({ ...token })
+    onTokenChange({ ...token })
+    validate({ ...token }, {
       beforeValidating: () => {
         if (!token.openai_api_base || !token.openai_api_key) {
           setValidatedStatus({})
           return false
         }
         return true
-      }
+      },
     })
   }
   const getValidatedIcon = () => {
-    if (validatedStatus.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) {
+    if (validatedStatus.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
       return <ValidatedErrorIcon />
-    }
-    if (validatedStatus.status === ValidatedStatus.Success) {
+
+    if (validatedStatus.status === ValidatedStatus.Success)
       return <ValidatedSuccessIcon />
-    }
   }
   const getValidatedTip = () => {
-    if (validating) {
+    if (validating)
       return <ValidatingTip />
-    }
-    if (validatedStatus.status === ValidatedStatus.Error) {
+
+    if (validatedStatus.status === ValidatedStatus.Error)
       return <ValidatedErrorOnAzureOpenaiTip errorMessage={validatedStatus.message ?? ''} />
-    }
   }
   useEffect(() => {
-    if (typeof onValidatedStatus === 'function') {
+    if (typeof onValidatedStatus === 'function')
       onValidatedStatus(validatedStatus)
-    }
   }, [validatedStatus])
 
   return (
     <div className='px-4 py-3'>
-      <ProviderInput 
+      <ProviderInput
         className='mb-4'
         name={t('common.provider.azure.apiBase')}
         placeholder={t('common.provider.azure.apiBasePlaceholder')}
         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')}
         validatedIcon={getValidatedIcon()}
       />
-      <ProviderInput 
+      <ProviderInput
         className='mb-4'
         name={t('common.provider.azure.apiKey')}
         placeholder={t('common.provider.azure.apiKeyPlaceholder')}
         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')}
         validatedIcon={getValidatedIcon()}
         validatedTip={getValidatedTip()}

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

@@ -1,15 +1,15 @@
 import { useState } from 'react'
 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 { useTranslation } from 'react-i18next'
 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'
 
-const providersMap: {[k: string]: any} = {
+const providersMap: { [k: string]: any } = {
   'openai-custom': {
     icon: 'openai',
     name: 'OpenAI',
@@ -17,7 +17,7 @@ const providersMap: {[k: string]: any} = {
   'azure_openai-custom': {
     icon: 'azure',
     name: 'Azure OpenAI Service',
-  }
+  },
 }
 
 // const providersList = [
@@ -56,7 +56,7 @@ const ProviderPage = () => {
   const { t } = useTranslation()
   const [activeProviderId, setActiveProviderId] = useState('')
   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}`
     return {
       provider,

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

@@ -1,19 +1,19 @@
-import type { Provider } from '@/models/common'
-import { useState, useEffect } from 'react'
+import { useEffect, useState } from 'react'
 import { useTranslation } from 'react-i18next'
-import ProviderInput from '../provider-input'
 import Link from 'next/link'
 import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
-import useValidateToken, { ValidatedStatus, 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,
   ValidatingTip,
-  ValidatedExceedOnOpenaiTip,
-  ValidatedErrorOnOpenaiTip
 } from '../provider-input/Validate'
+import type { Provider } from '@/models/common'
 
-interface IOpenaiProviderProps {
+type IOpenaiProviderProps = {
   provider: Provider
   onValidatedStatus: (status?: ValidatedStatusState) => void
   onTokenChange: (token: string) => void
@@ -22,11 +22,11 @@ interface IOpenaiProviderProps {
 const OpenaiProvider = ({
   provider,
   onValidatedStatus,
-  onTokenChange
+  onTokenChange,
 }: IOpenaiProviderProps) => {
   const { t } = useTranslation()
   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 = () => {
     if (token === provider.token) {
       setToken('')
@@ -44,35 +44,32 @@ const OpenaiProvider = ({
           return false
         }
         return true
-      }
+      },
     })
   }
   useEffect(() => {
-    if (typeof onValidatedStatus === 'function') {
+    if (typeof onValidatedStatus === 'function')
       onValidatedStatus(validatedStatus)
-    }
   }, [validatedStatus])
 
   const getValidatedIcon = () => {
-    if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) {
+    if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
       return <ValidatedErrorIcon />
-    }
-    if (validatedStatus.status === ValidatedStatus.Success) {
+
+    if (validatedStatus.status === ValidatedStatus.Success)
       return <ValidatedSuccessIcon />
-    }
   }
   const getValidatedTip = () => {
-    if (validating) {
+    if (validating)
       return <ValidatingTip />
-    }
-    if (validatedStatus?.status === ValidatedStatus.Error) {
+
+    if (validatedStatus?.status === ValidatedStatus.Error)
       return <ValidatedErrorOnOpenaiTip errorMessage={validatedStatus.message ?? ''} />
-    }
   }
 
   return (
     <div className='px-4 pt-3 pb-4'>
-      <ProviderInput 
+      <ProviderInput
         value={token}
         name={t('common.provider.apiKey')}
         placeholder={t('common.provider.enterYourKey')}
@@ -89,4 +86,4 @@ const OpenaiProvider = ({
   )
 }
 
-export default OpenaiProvider
+export default OpenaiProvider

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

@@ -15,7 +15,7 @@ export const ValidatedSuccessIcon = () => {
 export const ValidatingTip = () => {
   const { t } = useTranslation()
   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')}
     </div>
   )
@@ -26,11 +26,11 @@ export const ValidatedExceedOnOpenaiTip = () => {
   const { locale } = useContext(I18n)
 
   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;
-      <Link 
+      <Link
         className='underline'
-        href="https://platform.openai.com/account/api-keys" 
+        href="https://platform.openai.com/account/api-keys"
         target={'_blank'}>
         {locale === 'en' ? 'this link' : '这篇文档'}
       </Link>
@@ -42,7 +42,7 @@ export const ValidatedErrorOnOpenaiTip = ({ errorMessage }: { errorMessage: stri
   const { t } = useTranslation()
 
   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}
     </div>
   )
@@ -52,8 +52,8 @@ export const ValidatedErrorOnAzureOpenaiTip = ({ errorMessage }: { errorMessage:
   const { t } = useTranslation()
 
   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}
     </div>
   )
-}
+}

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

@@ -1,7 +1,7 @@
-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
   name: string
   placeholder: string
@@ -20,9 +20,8 @@ const ProviderInput = ({
   onChange,
   onFocus,
   validatedIcon,
-  validatedTip
+  validatedTip,
 }: IProviderInputProps) => {
-
   const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
     const inputValue = e.target.value
     onChange(inputValue)
@@ -35,12 +34,12 @@ const ProviderInput = ({
         flex items-center px-3 bg-white rounded-lg
         shadow-[0_1px_2px_rgba(16,24,40,0.05)]
       '>
-        <input 
+        <input
           className='
             w-full py-[9px]
             text-xs font-medium text-gray-700 leading-[18px]
-            appearance-none outline-none bg-transparent 
-          ' 
+            appearance-none outline-none bg-transparent
+          '
           value={value}
           placeholder={placeholder}
           onChange={handleChange}
@@ -53,4 +52,4 @@ const ProviderInput = ({
   )
 }
 
-export default ProviderInput
+export default ProviderInput

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

@@ -1,25 +1,26 @@
-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 { DebouncedFunc } from 'lodash-es'
+import type { DebouncedFunc } from 'lodash-es'
 import { validateProviderKey } from '@/service/common'
 
 export enum ValidatedStatus {
   Success = 'success',
   Error = 'error',
-  Exceed = 'exceed'
+  Exceed = 'exceed',
 }
 export type ValidatedStatusState = {
-  status?: ValidatedStatus,
+  status?: ValidatedStatus
   message?: string
 }
 // export type ValidatedStatusState = ValidatedStatus | undefined | ValidatedError
 export type SetValidatedStatus = Dispatch<SetStateAction<ValidatedStatusState>>
 export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void>
 type ValidateTokenReturn = [
-  boolean, 
-  ValidatedStatusState, 
+  boolean,
+  ValidatedStatusState,
   SetValidatedStatus,
-  ValidateFn
+  ValidateFn,
 ]
 export type ValidateFnConfig = {
   beforeValidating: (token: any) => boolean
@@ -29,19 +30,21 @@ const useValidateToken = (providerName: string): ValidateTokenReturn => {
   const [validating, setValidating] = useState(false)
   const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
   const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => {
-    if (!config.beforeValidating(token)) {
+    if (!config.beforeValidating(token))
       return false
-    }
+
     setValidating(true)
     try {
       const res = await validateProviderKey({ url: `/workspaces/current/providers/${providerName}/token-validate`, body: { token } })
       setValidatedStatus(
-        res.result === 'success' 
-          ? { status: ValidatedStatus.Success } 
+        res.result === 'success'
+          ? { status: ValidatedStatus.Success }
           : { status: ValidatedStatus.Error, message: res.error })
-    } catch (e: any) {
+    }
+    catch (e: any) {
       setValidatedStatus({ status: ValidatedStatus.Error, message: e.message })
-    } finally {
+    }
+    finally {
       setValidating(false)
     }
   }, 500), [])
@@ -50,8 +53,8 @@ const useValidateToken = (providerName: string): ValidateTokenReturn => {
     validating,
     validatedStatus,
     setValidatedStatus,
-    validate
+    validate,
   ]
 }
 
-export default useValidateToken
+export default useValidateToken

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

@@ -1,18 +1,19 @@
 import { useState } from 'react'
 import cn from 'classnames'
-import s from './index.module.css'
 import { useContext } from 'use-context-selector'
-import Indicator from '../../../indicator'
 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 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 { ToastContext } from '@/app/components/base/toast'
 
-interface IProviderItemProps {
+type IProviderItemProps = {
   icon: string
   name: string
   provider: Provider
@@ -26,17 +27,17 @@ const ProviderItem = ({
   name,
   provider,
   onActive,
-  onSave
+  onSave,
 }: IProviderItemProps) => {
   const { t } = useTranslation()
   const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>()
   const [loading, setLoading] = useState(false)
   const { notify } = useContext(ToastContext)
   const [token, setToken] = useState<ProviderAzureToken | string>(
-    provider.provider_name === 'azure_openai' 
+    provider.provider_name === 'azure_openai'
       ? { openai_api_base: '', openai_api_key: '' }
-      : ''
-    )
+      : '',
+  )
   const id = `${provider.provider_name}-${provider.provider_type}`
   const isOpen = id === activeId
   const comingSoon = false
@@ -44,26 +45,30 @@ const ProviderItem = ({
 
   const providerTokenHasSetted = () => {
     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
-    }
   }
   const handleUpdateToken = async () => {
-    if (loading) return
+    if (loading)
+      return
     if (validatedStatus?.status === ValidatedStatus.Success) {
       try {
         setLoading(true)
         await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } })
         notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
         onActive('')
-      } catch (e) {
+      }
+      catch (e) {
         notify({ type: 'error', message: t('common.provider.saveFailed') })
-      } finally {
+      }
+      finally {
         setLoading(false)
         onSave()
       }
@@ -126,18 +131,18 @@ const ProviderItem = ({
       </div>
       {
         provider.provider_name === ProviderName.OPENAI && isOpen && (
-          <OpenaiProvider 
-            provider={provider} 
-            onValidatedStatus={v => setValidatedStatus(v)} 
+          <OpenaiProvider
+            provider={provider}
+            onValidatedStatus={v => setValidatedStatus(v)}
             onTokenChange={v => setToken(v)}
           />
         )
       }
       {
         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)}
           />
         )

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 12 - 12
web/app/components/header/index.tsx


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

@@ -1,10 +1,11 @@
 '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 AppIcon from '@/app/components/base/app-icon'
 
-export interface IAppInfoProps {
+export type IAppInfoProps = {
   className?: string
   icon: string
   icon_background?: string
@@ -15,7 +16,7 @@ const AppInfo: FC<IAppInfoProps> = ({
   className,
   icon,
   icon_background,
-  name
+  name,
 }) => {
   return (
     <div className={cn(className, 'flex items-center space-x-3')}>

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

@@ -1,16 +1,16 @@
-import React, { useEffect, useRef } from 'react'
+import React, { useRef } from 'react'
 import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
 import {
   ChatBubbleOvalLeftEllipsisIcon,
-  PencilSquareIcon
+  PencilSquareIcon,
 } 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 AppInfo from '@/app/components/share/chat/sidebar/app-info'
 // import Card from './card'
 import type { ConversationItem, SiteInfo } from '@/models/share'
-import { useInfiniteScroll } from 'ahooks'
 import { fetchConversations } from '@/service/share'
 
 function classNames(...classes: any[]) {
@@ -25,7 +25,7 @@ export type ISidebarProps = {
   isInstalledApp: boolean
   installedAppId?: string
   siteInfo: SiteInfo
-  onMoreLoaded: (res: {data: ConversationItem[], has_more: boolean}) => void
+  onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void
   isNoMore: boolean
 }
 
@@ -45,19 +45,19 @@ const Sidebar: FC<ISidebarProps> = ({
 
   useInfiniteScroll(
     async () => {
-      if(!isNoMore) {
+      if (!isNoMore) {
         const lastId = list[list.length - 1].id
         const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId)
         onMoreLoaded({ data: conversations, has_more })
       }
-      return {list: []}
+      return { list: [] }
     },
     {
       target: listRef,
       isNoMore: () => {
         return isNoMore
       },
-      reloadDeps: [isNoMore]
+      reloadDeps: [isNoMore],
     },
   )
 
@@ -66,7 +66,7 @@ const Sidebar: FC<ISidebarProps> = ({
       className={
         classNames(
           isInstalledApp ? 'tablet:h-[calc(100vh_-_74px)]' : 'tablet:h-[calc(100vh_-_3rem)]',
-          "shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px]  border-r border-gray-200 mobile:h-screen"
+          '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

@@ -38,25 +38,27 @@ const ConfigSence: FC<IConfigSenceProps> = ({
             <div className='w-full mt-4' key={item.key}>
               <label className='text-gray-900 text-sm font-medium'>{item.name}</label>
               <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>
           ))}

+ 24 - 24
web/config/index.ts

@@ -1,35 +1,38 @@
-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
 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
   apiPrefix = globalThis.document.body.getAttribute('data-api-prefix') as string
   publicApiPrefix = globalThis.document.body.getAttribute('data-pubic-api-prefix') as string
-} else {
+}
+else {
   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('.');
     // 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];
-    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')
 export const IS_CE_EDITION = EDITION === 'SELF_HOSTED'
@@ -75,15 +78,15 @@ export const LOCALE_COOKIE_NAME = 'locale'
 
 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 emailRegex = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$/m
 const MAX_ZN_VAR_NAME_LENGHT = 8
 const MAX_EN_VAR_VALUE_LENGHT = 16
 export const getMaxVarNameLength = (value: string) => {
-  if (zhRegex.test(value)) {
+  if (zhRegex.test(value))
     return MAX_ZN_VAR_NAME_LENGHT
-  }
+
   return MAX_EN_VAR_VALUE_LENGHT
 }
 
@@ -94,12 +97,9 @@ export const VAR_ITEM_TEMPLATE = {
   name: '',
   type: 'string',
   max_length: DEFAULT_VALUE_MAX_LEN,
-  required: true
+  required: true,
 }
 
 export const appDefaultIconBackground = '#D5F5F6'
 
 export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList'
-
-
-

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

@@ -1,5 +1,6 @@
 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

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

@@ -1,5 +1,5 @@
 import { createContext } from 'use-context-selector'
-import { InstalledApp } from '@/models/explore'
+import type { InstalledApp } from '@/models/explore'
 
 type IExplore = {
   controlUpdateInstalledApps: number

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

@@ -39,7 +39,7 @@ const translation = {
   emoji: {
     ok: 'OK',
     cancel: 'Cancel',
-  }
+  },
 }
 
 export default translation

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

@@ -38,7 +38,7 @@ const translation = {
   emoji: {
     ok: '确认',
     cancel: '取消',
-  }
+  },
 }
 
 export default translation

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

@@ -76,6 +76,8 @@ const translation = {
     fileName: 'Preprocess document',
     lastStep: 'Last step',
     nextStep: 'Save & Process',
+    save: 'Save & Process',
+    cancel: 'Cancel',
     sideTipTitle: 'Why segment and preprocess?',
     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.',

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

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

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

@@ -11,7 +11,7 @@ const translation = {
     delete: {
       title: 'Delete app',
       content: 'Are you sure you want to delete this app?',
-    }
+    },
   },
   apps: {
     title: 'Explore Apps by Dify',
@@ -28,12 +28,12 @@ const translation = {
     nameRequired: 'App name is required',
   },
   category: {
-    'Assistant': 'Assistant',
-    'Writing': 'Writing',
-    'Translate': 'Translate',
-    'Programming': 'Programming',
-    'HR': 'HR',
-  }
+    Assistant: 'Assistant',
+    Writing: 'Writing',
+    Translate: 'Translate',
+    Programming: 'Programming',
+    HR: 'HR',
+  },
 }
 
 export default translation

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

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

+ 2 - 2
web/models/common.ts

@@ -56,7 +56,7 @@ export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_l
 
 export enum ProviderName {
   OPENAI = 'openai',
-  AZURE_OPENAI = 'azure_openai'
+  AZURE_OPENAI = 'azure_openai',
 }
 export type ProviderAzureToken = {
   openai_api_base?: string
@@ -91,7 +91,7 @@ export type AccountIntegrate = {
   link: string
 }
 
-export interface IWorkspace {
+export type IWorkspace = {
   id: string
   name: string
   plan: string

+ 20 - 20
web/models/explore.ts

@@ -1,30 +1,30 @@
-import { AppMode } from "./app";
+import type { AppMode } from './app'
 
 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 = {
-  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 = {
-  app: AppBasicInfo;
-  id: string;
+  app: AppBasicInfo
+  id: string
   uninstallable: boolean
   is_pinned: boolean
-}
+}

+ 6 - 6
web/service/explore.ts

@@ -1,10 +1,10 @@
-import { get, post, del, patch } from './base'
+import { del, get, patch, post } from './base'
 
 export const fetchAppList = () => {
   return get('/explore/apps')
 }
 
-export const fetchAppDetail = (id: string) : Promise<any> => {
+export const fetchAppDetail = (id: string): Promise<any> => {
   return get(`/explore/apps/${id}`)
 }
 
@@ -15,8 +15,8 @@ export const fetchInstalledAppList = () => {
 export const installApp = (id: string) => {
   return post('/installed-apps', {
     body: {
-      app_id: id
-    }
+      app_id: id,
+    },
   })
 }
 
@@ -27,7 +27,7 @@ export const uninstallApp = (id: string) => {
 export const updatePinStatus = (id: string, isPinned: boolean) => {
   return patch(`/installed-apps/${id}`, {
     body: {
-      is_pinned: isPinned
-    }
+      is_pinned: isPinned,
+    },
   })
 }

+ 9 - 9
web/service/share.ts

@@ -1,7 +1,7 @@
 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'
 import type { Feedbacktype } from '@/app/components/app/chat'
 
@@ -23,7 +23,7 @@ function getUrl(url: string, isInstalledApp: boolean, installedAppId: string) {
 export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, getAbortController }: {
   onData: IOnData
   onCompleted: IOnCompleted
-  onError: IOnError,
+  onError: IOnError
   getAbortController?: (abortController: AbortController) => void
 }, isInstalledApp: boolean, installedAppId = '') => {
   return ssePost(getUrl('chat-messages', isInstalledApp, installedAppId), {
@@ -51,11 +51,11 @@ export const fetchAppInfo = async () => {
   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: '' } })
 }
 
@@ -77,7 +77,7 @@ export const fetchMoreLikeThis = async (messageId: string, isInstalledApp: boole
   return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/more-like-this`, isInstalledApp, installedAppId), {
     params: {
       response_mode: 'blocking',
-    }
+    },
   })
 }
 
@@ -86,7 +86,7 @@ export const saveMessage = (messageId: string, isInstalledApp: boolean, installe
 }
 
 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 = '') => {

+ 6 - 7
web/types/app.ts

@@ -38,16 +38,16 @@ export type PromptVariable = {
 }
 
 export type TextTypeFormItem = {
-  label: string,
-  variable: string,
+  label: string
+  variable: string
   required: boolean
   max_length: number
 }
 
 export type SelectTypeFormItem = {
-  label: string,
-  variable: string,
-  required: boolean,
+  label: string
+  variable: string
+  required: boolean
   options: string[]
 }
 /**
@@ -59,7 +59,6 @@ export type UserInputFormItem = {
   'select': SelectTypeFormItem
 }
 
-
 export type ToolItem = {
   dataset: {
     enabled: boolean
@@ -195,7 +194,7 @@ export type App = {
   icon: string
   /** Icon Background */
   icon_background: string
-  
+
   /** Mode */
   mode: AppMode
   /** Enable web app */