Merge branch 'main' into feat/knowledge-dark-mode

This commit is contained in:
twwu
2025-02-07 14:30:14 +08:00
212 changed files with 4395 additions and 1365 deletions

View File

@ -47,6 +47,44 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='text' type='string' key='text'>
Document content
</Property>
<Property name='doc_type' type='string' key='doc_type'>
Type of document (optional):
- <code>book</code> Book
- <code>web_page</code> Web page
- <code>paper</code> Academic paper/article
- <code>social_media_post</code> Social media post
- <code>wikipedia_entry</code> Wikipedia entry
- <code>personal_document</code> Personal document
- <code>business_document</code> Business document
- <code>im_chat_log</code> Chat log
- <code>synced_from_notion</code> Notion document
- <code>synced_from_github</code> GitHub document
- <code>others</code> Other document types
</Property>
<Property name='doc_metadata' type='object' key='doc_metadata'>
Document metadata (required if doc_type is provided). Fields vary by doc_type:
For <code>book</code>:
- <code>title</code> Book title
- <code>language</code> Book language
- <code>author</code> Book author
- <code>publisher</code> Publisher name
- <code>publication_date</code> Publication date
- <code>isbn</code> ISBN number
- <code>category</code> Book category
For <code>web_page</code>:
- <code>title</code> Page title
- <code>url</code> Page URL
- <code>language</code> Page language
- <code>publish_date</code> Publish date
- <code>author/publisher</code> Author or publisher
- <code>topic/keywords</code> Topic or keywords
- <code>description</code> Page description
Please check [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) for more details on the fields required for each doc_type.
For doc_type "others", any valid JSON object is accepted
</Property>
<Property name='indexing_technique' type='string' key='indexing_technique'>
Index mode
- <code>high_quality</code> High quality: embedding using embedding model, built as vector database index
@ -195,6 +233,68 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>hierarchical_model</code> Parent-child mode
- <code>qa_model</code> Q&A Mode: Generates Q&A pairs for segmented documents and then embeds the questions
- <code>doc_type</code> Type of document (optional)
- <code>book</code> Book
Document records a book or publication
- <code>web_page</code> Web page
Document records web page content
- <code>paper</code> Academic paper/article
Document records academic paper or research article
- <code>social_media_post</code> Social media post
Content from social media posts
- <code>wikipedia_entry</code> Wikipedia entry
Content from Wikipedia entries
- <code>personal_document</code> Personal document
Documents related to personal content
- <code>business_document</code> Business document
Documents related to business content
- <code>im_chat_log</code> Chat log
Records of instant messaging chats
- <code>synced_from_notion</code> Notion document
Documents synchronized from Notion
- <code>synced_from_github</code> GitHub document
Documents synchronized from GitHub
- <code>others</code> Other document types
Other document types not listed above
- <code>doc_metadata</code> Document metadata (required if doc_type is provided)
Fields vary by doc_type:
For <code>book</code>:
- <code>title</code> Book title
Title of the book
- <code>language</code> Book language
Language of the book
- <code>author</code> Book author
Author of the book
- <code>publisher</code> Publisher name
Name of the publishing house
- <code>publication_date</code> Publication date
Date when the book was published
- <code>isbn</code> ISBN number
International Standard Book Number
- <code>category</code> Book category
Category or genre of the book
For <code>web_page</code>:
- <code>title</code> Page title
Title of the web page
- <code>url</code> Page URL
URL address of the web page
- <code>language</code> Page language
Language of the web page
- <code>publish_date</code> Publish date
Date when the web page was published
- <code>author/publisher</code> Author or publisher
Author or publisher of the web page
- <code>topic/keywords</code> Topic or keywords
Topics or keywords of the web page
- <code>description</code> Page description
Description of the web page content
Please check [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) for more details on the fields required for each doc_type.
For doc_type "others", any valid JSON object is accepted
- <code>doc_language</code> In Q&A mode, specify the language of the document, for example: <code>English</code>, <code>Chinese</code>
- <code>process_rule</code> Processing rules
@ -307,6 +407,44 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='description' type='string' key='description'>
Knowledge description (optional)
</Property>
<Property name='doc_type' type='string' key='doc_type'>
Type of document (optional):
- <code>book</code> Book
- <code>web_page</code> Web page
- <code>paper</code> Academic paper/article
- <code>social_media_post</code> Social media post
- <code>wikipedia_entry</code> Wikipedia entry
- <code>personal_document</code> Personal document
- <code>business_document</code> Business document
- <code>im_chat_log</code> Chat log
- <code>synced_from_notion</code> Notion document
- <code>synced_from_github</code> GitHub document
- <code>others</code> Other document types
</Property>
<Property name='doc_metadata' type='object' key='doc_metadata'>
Document metadata (required if doc_type is provided). Fields vary by doc_type:
For <code>book</code>:
- <code>title</code> Book title
- <code>language</code> Book language
- <code>author</code> Book author
- <code>publisher</code> Publisher name
- <code>publication_date</code> Publication date
- <code>isbn</code> ISBN number
- <code>category</code> Book category
For <code>web_page</code>:
- <code>title</code> Page title
- <code>url</code> Page URL
- <code>language</code> Page language
- <code>publish_date</code> Publish date
- <code>author/publisher</code> Author or publisher
- <code>topic/keywords</code> Topic or keywords
- <code>description</code> Page description
Please check [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) for more details on the fields required for each doc_type.
For doc_type "others", any valid JSON object is accepted
</Property>
<Property name='indexing_technique' type='string' key='indexing_technique'>
Index technique (optional)
- <code>high_quality</code> High quality
@ -624,6 +762,67 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>separator</code> Segmentation identifier. Currently, only one delimiter is allowed. The default is <code>***</code>
- <code>max_tokens</code> The maximum length (tokens) must be validated to be shorter than the length of the parent chunk
- <code>chunk_overlap</code> Define the overlap between adjacent chunks (optional)
- <code>doc_type</code> Type of document (optional)
- <code>book</code> Book
Document records a book or publication
- <code>web_page</code> Web page
Document records web page content
- <code>paper</code> Academic paper/article
Document records academic paper or research article
- <code>social_media_post</code> Social media post
Content from social media posts
- <code>wikipedia_entry</code> Wikipedia entry
Content from Wikipedia entries
- <code>personal_document</code> Personal document
Documents related to personal content
- <code>business_document</code> Business document
Documents related to business content
- <code>im_chat_log</code> Chat log
Records of instant messaging chats
- <code>synced_from_notion</code> Notion document
Documents synchronized from Notion
- <code>synced_from_github</code> GitHub document
Documents synchronized from GitHub
- <code>others</code> Other document types
Other document types not listed above
- <code>doc_metadata</code> Document metadata (required if doc_type is provided)
Fields vary by doc_type:
For <code>book</code>:
- <code>title</code> Book title
Title of the book
- <code>language</code> Book language
Language of the book
- <code>author</code> Book author
Author of the book
- <code>publisher</code> Publisher name
Name of the publishing house
- <code>publication_date</code> Publication date
Date when the book was published
- <code>isbn</code> ISBN number
International Standard Book Number
- <code>category</code> Book category
Category or genre of the book
For <code>web_page</code>:
- <code>title</code> Page title
Title of the web page
- <code>url</code> Page URL
URL address of the web page
- <code>language</code> Page language
Language of the web page
- <code>publish_date</code> Publish date
Date when the web page was published
- <code>author/publisher</code> Author or publisher
Author or publisher of the web page
- <code>topic/keywords</code> Topic or keywords
Topics or keywords of the web page
- <code>description</code> Page description
Description of the web page content
Please check [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) for more details on the fields required for each doc_type.
For doc_type "others", any valid JSON object is accepted
</Property>
</Properties>
</Col>

View File

@ -47,6 +47,46 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='text' type='string' key='text'>
文档内容
</Property>
<Property name='doc_type' type='string' key='doc_type'>
文档类型(选填)
- <code>book</code> 图书 Book
- <code>web_page</code> 网页 Web page
- <code>paper</code> 学术论文/文章 Academic paper/article
- <code>social_media_post</code> 社交媒体帖子 Social media post
- <code>wikipedia_entry</code> 维基百科条目 Wikipedia entry
- <code>personal_document</code> 个人文档 Personal document
- <code>business_document</code> 商业文档 Business document
- <code>im_chat_log</code> 即时通讯记录 Chat log
- <code>synced_from_notion</code> Notion同步文档 Notion document
- <code>synced_from_github</code> GitHub同步文档 GitHub document
- <code>others</code> 其他文档类型 Other document types
</Property>
<Property name='doc_metadata' type='object' key='doc_metadata'>
文档元数据(如提供文档类型则必填)。字段因文档类型而异:
针对图书 For <code>book</code>:
- <code>title</code> 书名 Book title
- <code>language</code> 图书语言 Book language
- <code>author</code> 作者 Book author
- <code>publisher</code> 出版社 Publisher name
- <code>publication_date</code> 出版日期 Publication date
- <code>isbn</code> ISBN号码 ISBN number
- <code>category</code> 图书分类 Book category
针对网页 For <code>web_page</code>:
- <code>title</code> 页面标题 Page title
- <code>url</code> 页面网址 Page URL
- <code>language</code> 页面语言 Page language
- <code>publish_date</code> 发布日期 Publish date
- <code>author/publisher</code> 作者/发布者 Author or publisher
- <code>topic/keywords</code> 主题/关键词 Topic or keywords
- <code>description</code> 页面描述 Page description
请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
针对"其他"类型文档接受任何有效的JSON对象
</Property>
<Property name='indexing_technique' type='string' key='indexing_technique'>
索引方式
- <code>high_quality</code> 高质量:使用 embedding 模型进行嵌入,构建为向量数据库索引
@ -194,6 +234,68 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>text_model</code> text 文档直接 embedding经济模式默认为该模式
- <code>hierarchical_model</code> parent-child 模式
- <code>qa_model</code> Q&A 模式:为分片文档生成 Q&A 对,然后对问题进行 embedding
- <code>doc_type</code> 文档类型选填Type of document (optional)
- <code>book</code> 图书
文档记录一本书籍或出版物
- <code>web_page</code> 网页
网页内容的文档记录
- <code>paper</code> 学术论文/文章
学术论文或研究文章的记录
- <code>social_media_post</code> 社交媒体帖子
社交媒体上的帖子内容
- <code>wikipedia_entry</code> 维基百科条目
维基百科的词条内容
- <code>personal_document</code> 个人文档
个人相关的文档记录
- <code>business_document</code> 商业文档
商业相关的文档记录
- <code>im_chat_log</code> 即时通讯记录
即时通讯的聊天记录
- <code>synced_from_notion</code> Notion同步文档
从Notion同步的文档内容
- <code>synced_from_github</code> GitHub同步文档
从GitHub同步的文档内容
- <code>others</code> 其他文档类型
其他未列出的文档类型
- <code>doc_metadata</code> 文档元数据(如提供文档类型则必填
字段因文档类型而异
针对图书类型 For <code>book</code>:
- <code>title</code> 书名
书籍的标题
- <code>language</code> 图书语言
书籍的语言
- <code>author</code> 作者
书籍的作者
- <code>publisher</code> 出版社
出版社的名称
- <code>publication_date</code> 出版日期
书籍的出版日期
- <code>isbn</code> ISBN号码
书籍的ISBN编号
- <code>category</code> 图书分类
书籍的分类类别
针对网页类型 For <code>web_page</code>:
- <code>title</code> 页面标题
网页的标题
- <code>url</code> 页面网址
网页的URL地址
- <code>language</code> 页面语言
网页的语言
- <code>publish_date</code> 发布日期
网页的发布日期
- <code>author/publisher</code> 作者/发布者
网页的作者或发布者
- <code>topic/keywords</code> 主题/关键词
网页的主题或关键词
- <code>description</code> 页面描述
网页的描述信息
请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
针对"其他"类型文档接受任何有效的JSON对象
- <code>doc_language</code> 在 Q&A 模式下,指定文档的语言,例如:<code>English</code>、<code>Chinese</code>
@ -504,6 +606,46 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='text' type='string' key='text'>
文档内容(选填)
</Property>
<Property name='doc_type' type='string' key='doc_type'>
文档类型(选填)
- <code>book</code> 图书 Book
- <code>web_page</code> 网页 Web page
- <code>paper</code> 学术论文/文章 Academic paper/article
- <code>social_media_post</code> 社交媒体帖子 Social media post
- <code>wikipedia_entry</code> 维基百科条目 Wikipedia entry
- <code>personal_document</code> 个人文档 Personal document
- <code>business_document</code> 商业文档 Business document
- <code>im_chat_log</code> 即时通讯记录 Chat log
- <code>synced_from_notion</code> Notion同步文档 Notion document
- <code>synced_from_github</code> GitHub同步文档 GitHub document
- <code>others</code> 其他文档类型 Other document types
</Property>
<Property name='doc_metadata' type='object' key='doc_metadata'>
文档元数据(如提供文档类型则必填)。字段因文档类型而异:
针对图书 For <code>book</code>:
- <code>title</code> 书名 Book title
- <code>language</code> 图书语言 Book language
- <code>author</code> 作者 Book author
- <code>publisher</code> 出版社 Publisher name
- <code>publication_date</code> 出版日期 Publication date
- <code>isbn</code> ISBN号码 ISBN number
- <code>category</code> 图书分类 Book category
针对网页 For <code>web_page</code>:
- <code>title</code> 页面标题 Page title
- <code>url</code> 页面网址 Page URL
- <code>language</code> 页面语言 Page language
- <code>publish_date</code> 发布日期 Publish date
- <code>author/publisher</code> 作者/发布者 Author or publisher
- <code>topic/keywords</code> 主题/关键词 Topic or keywords
- <code>description</code> 页面描述 Page description
请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
针对"其他"类型文档接受任何有效的JSON对象
</Property>
<Property name='process_rule' type='object' key='process_rule'>
处理规则(选填)
- <code>mode</code> (string) 清洗、分段模式 automatic 自动 / custom 自定义
@ -624,6 +766,68 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>separator</code> 分段标识符,目前仅允许设置一个分隔符。默认为 <code>***</code>
- <code>max_tokens</code> 最大长度 (token) 需要校验小于父级的长度
- <code>chunk_overlap</code> 分段重叠指的是在对数据进行分段时,段与段之间存在一定的重叠部分(选填)
- <code>doc_type</code> 文档类型选填Type of document (optional)
- <code>book</code> 图书
文档记录一本书籍或出版物
- <code>web_page</code> 网页
网页内容的文档记录
- <code>paper</code> 学术论文/文章
学术论文或研究文章的记录
- <code>social_media_post</code> 社交媒体帖子
社交媒体上的帖子内容
- <code>wikipedia_entry</code> 维基百科条目
维基百科的词条内容
- <code>personal_document</code> 个人文档
个人相关的文档记录
- <code>business_document</code> 商业文档
商业相关的文档记录
- <code>im_chat_log</code> 即时通讯记录
即时通讯的聊天记录
- <code>synced_from_notion</code> Notion同步文档
从Notion同步的文档内容
- <code>synced_from_github</code> GitHub同步文档
从GitHub同步的文档内容
- <code>others</code> 其他文档类型
其他未列出的文档类型
- <code>doc_metadata</code> 文档元数据(如提供文档类型则必填
字段因文档类型而异
针对图书类型 For <code>book</code>:
- <code>title</code> 书名
书籍的标题
- <code>language</code> 图书语言
书籍的语言
- <code>author</code> 作者
书籍的作者
- <code>publisher</code> 出版社
出版社的名称
- <code>publication_date</code> 出版日期
书籍的出版日期
- <code>isbn</code> ISBN号码
书籍的ISBN编号
- <code>category</code> 图书分类
书籍的分类类别
针对网页类型 For <code>web_page</code>:
- <code>title</code> 页面标题
网页的标题
- <code>url</code> 页面网址
网页的URL地址
- <code>language</code> 页面语言
网页的语言
- <code>publish_date</code> 发布日期
网页的发布日期
- <code>author/publisher</code> 作者/发布者
网页的作者或发布者
- <code>topic/keywords</code> 主题/关键词
网页的主题或关键词
- <code>description</code> 页面描述
网页的描述信息
请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
针对"其他"类型文档接受任何有效的JSON对象
</Property>
</Properties>
</Col>

View File

@ -23,12 +23,14 @@ type ItemProps = {
onRemove: (id: string) => void
readonly?: boolean
onSave: (newDataset: DataSet) => void
editable?: boolean
}
const Item: FC<ItemProps> = ({
config,
onSave,
onRemove,
editable = true,
}) => {
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
@ -68,19 +70,21 @@ const Item: FC<ItemProps> = ({
<div className='flex items-center h-[18px]'>
<div className='grow text-[13px] font-medium text-gray-800 truncate' title={config.name}>{config.name}</div>
{config.provider === 'external'
? <Badge text={t('dataset.externalTag')}></Badge>
? <Badge text={t('dataset.externalTag') as string} />
: <Badge
text={formatIndexingTechniqueAndMethod(config.indexing_technique, config.retrieval_model_dict?.search_method)}
/>}
</div>
</div>
<div className='hidden rounded-lg group-hover:flex items-center justify-end absolute right-0 top-0 bottom-0 pr-2 w-[124px] bg-gradient-to-r from-white/50 to-white to-50%'>
<div
className='flex items-center justify-center mr-1 w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
onClick={() => setShowSettingsModal(true)}
>
<RiEditLine className='w-4 h-4 text-gray-500' />
</div>
{
editable && <div
className='flex items-center justify-center mr-1 w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
onClick={() => setShowSettingsModal(true)}
>
<RiEditLine className='w-4 h-4 text-gray-500' />
</div>
}
<div
className='group/action flex items-center justify-center w-6 h-6 hover:bg-[#FEE4E2] rounded-md cursor-pointer'
onClick={() => onRemove(config.id)}

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import produce from 'immer'
@ -19,6 +19,8 @@ import {
} from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { hasEditPermissionForDataset } from '@/utils/permission'
const Icon = (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -29,6 +31,7 @@ const Icon = (
const DatasetConfig: FC = () => {
const { t } = useTranslation()
const userProfile = useAppContextSelector(s => s.userProfile)
const {
mode,
dataSets: dataSet,
@ -105,6 +108,20 @@ const DatasetConfig: FC = () => {
setModelConfig(newModelConfig)
}
const formattedDataset = useMemo(() => {
return dataSet.map((item) => {
const datasetConfig = {
createdBy: item.created_by,
partialMemberList: item.partial_member_list || [],
permission: item.permission,
}
return {
...item,
editable: hasEditPermissionForDataset(userProfile?.id || '', datasetConfig),
}
})
}, [dataSet, userProfile?.id])
return (
<FeaturePanel
className='mt-2'
@ -122,12 +139,13 @@ const DatasetConfig: FC = () => {
{hasData
? (
<div className='flex flex-wrap mt-1 px-3 pb-3 justify-between'>
{dataSet.map(item => (
{formattedDataset.map(item => (
<CardItem
key={item.id}
config={item}
onRemove={onRemove}
onSave={handleSave}
editable={item.editable}
/>
))}
</div>

View File

@ -12,7 +12,7 @@ import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { type DataSet } from '@/models/datasets'
import { type DataSet, DatasetPermission } from '@/models/datasets'
import { useToastContext } from '@/app/components/base/toast'
import { updateDatasetSetting } from '@/service/datasets'
import { useAppContext } from '@/context/app-context'
@ -134,7 +134,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
}),
},
} as any
if (permission === 'partial_members') {
if (permission === DatasetPermission.partialMembers) {
requestParams.body.partial_member_list = selectedMemberIDs.map((id) => {
return {
user_id: id,

View File

@ -67,7 +67,6 @@ const ChatItem: FC<ChatItemProps> = ({
}, [modelConfig.configs.prompt_variables])
const {
chatList,
chatListRef,
isResponding,
handleSend,
suggestedQuestions,
@ -102,7 +101,7 @@ const ChatItem: FC<ChatItemProps> = ({
query: message,
inputs,
model_config: configData,
parent_message_id: getLastAnswer(chatListRef.current)?.id || null,
parent_message_id: getLastAnswer(chatList)?.id || null,
}
if ((config.file_upload as any).enabled && files?.length && supportVision)
@ -116,7 +115,7 @@ const ChatItem: FC<ChatItemProps> = ({
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
},
)
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, chatListRef])
}, [appId, chatList, config, handleSend, inputs, modelAndParameter.model, modelAndParameter.parameters, modelAndParameter.provider, textGenerationModelList])
const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => {

View File

@ -12,7 +12,7 @@ import {
import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type { ChatConfig, ChatItem, OnSend } from '@/app/components/base/chat/types'
import type { ChatConfig, ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
import { useProviderContext } from '@/context/provider-context'
import {
fetchConversationMessages,
@ -24,7 +24,7 @@ import { useAppContext } from '@/context/app-context'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures } from '@/app/components/base/features/hooks'
import { getLastAnswer } from '@/app/components/base/chat/utils'
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
import type { InputForm } from '@/app/components/base/chat/chat/type'
type DebugWithSingleModelProps = {
@ -68,12 +68,11 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
}, [modelConfig.configs.prompt_variables])
const {
chatList,
chatListRef,
setTargetMessageId,
isResponding,
handleSend,
suggestedQuestions,
handleStop,
handleUpdateChatList,
handleRestart,
handleAnnotationAdded,
handleAnnotationEdited,
@ -89,7 +88,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
)
useFormattingChangedSubscription(chatList)
const doSend: OnSend = useCallback((message, files, last_answer) => {
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
if (checkCanSend && !checkCanSend())
return
const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider)
@ -110,7 +109,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
query: message,
inputs,
model_config: configData,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
}
if ((config.file_upload as any)?.enabled && files?.length && supportVision)
@ -124,23 +123,13 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
},
)
}, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList])
}, [appId, chatList, checkCanSend, completionParams, config, handleSend, inputs, modelConfig.mode, modelConfig.model_id, modelConfig.provider, textGenerationModelList])
const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id)
if (index === -1)
return
const prevMessages = chatList.slice(0, index)
const question = prevMessages.pop()
const lastAnswer = getLastAnswer(prevMessages)
if (!question)
return
handleUpdateChatList(prevMessages)
doSend(question.content, question.message_files, lastAnswer)
}, [chatList, handleUpdateChatList, doSend])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend])
const allToolIcons = useMemo(() => {
const icons: Record<string, any> = {}
@ -173,6 +162,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
inputs={inputs}
inputsForm={inputsForm}
onRegenerate={doRegenerate}
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
onStopResponding={handleStop}
showPromptLog
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={40} />}

View File

@ -491,7 +491,7 @@ const Configuration: FC = () => {
}, [formattingChangedDispatcher, setShowAppConfigureFeaturesModal])
const handleAddPromptVariable = useCallback((variable: PromptVariable[]) => {
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.configs.prompt_variables = variable
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...variable]
})
setModelConfig(newModelConfig)
}, [modelConfig])

View File

@ -3,10 +3,11 @@ import Chat from '../chat'
import type {
ChatConfig,
ChatItem,
ChatItemInTree,
OnSend,
} from '../types'
import { useChat } from '../chat/hooks'
import { getLastAnswer } from '../utils'
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
import { useChatWithHistoryContext } from './context'
import Header from './header'
import ConfigPanel from './config-panel'
@ -20,7 +21,7 @@ import AnswerIcon from '@/app/components/base/answer-icon'
const ChatWrapper = () => {
const {
appParams,
appPrevChatList,
appPrevChatTree,
currentConversationId,
currentConversationItem,
inputsForms,
@ -50,8 +51,7 @@ const ChatWrapper = () => {
}, [appParams, currentConversationItem?.introduction, currentConversationId])
const {
chatList,
chatListRef,
handleUpdateChatList,
setTargetMessageId,
handleSend,
handleStop,
isResponding,
@ -62,7 +62,7 @@ const ChatWrapper = () => {
inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any,
inputsForm: inputsForms,
},
appPrevChatList,
appPrevChatTree,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
)
@ -72,13 +72,13 @@ const ChatWrapper = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const doSend: OnSend = useCallback((message, files, last_answer) => {
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
const data: any = {
query: message,
files,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
}
handleSend(
@ -91,31 +91,21 @@ const ChatWrapper = () => {
},
)
}, [
chatListRef,
chatList,
handleNewConversationCompleted,
handleSend,
currentConversationId,
currentConversationItem,
handleSend,
newConversationInputs,
handleNewConversationCompleted,
isInstalledApp,
appId,
])
const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id)
if (index === -1)
return
const prevMessages = chatList.slice(0, index)
const question = prevMessages.pop()
const lastAnswer = getLastAnswer(prevMessages)
if (!question)
return
handleUpdateChatList(prevMessages)
doSend(question.content, question.message_files, lastAnswer)
}, [chatList, handleUpdateChatList, doSend])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend])
const chatNode = useMemo(() => {
if (inputsForms.length) {
@ -187,6 +177,7 @@ const ChatWrapper = () => {
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
/>
</div>
)

View File

@ -5,7 +5,7 @@ import { createContext, useContext } from 'use-context-selector'
import type {
Callback,
ChatConfig,
ChatItem,
ChatItemInTree,
Feedback,
} from '../types'
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
@ -25,7 +25,7 @@ export type ChatWithHistoryContextValue = {
appChatListDataLoading?: boolean
currentConversationId: string
currentConversationItem?: ConversationItem
appPrevChatList: ChatItem[]
appPrevChatTree: ChatItemInTree[]
pinnedConversationList: AppConversationData['data']
conversationList: AppConversationData['data']
showConfigPanelBeforeChat: boolean
@ -53,7 +53,7 @@ export type ChatWithHistoryContextValue = {
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
currentConversationId: '',
appPrevChatList: [],
appPrevChatTree: [],
pinnedConversationList: [],
conversationList: [],
showConfigPanelBeforeChat: false,

View File

@ -12,10 +12,13 @@ import produce from 'immer'
import type {
Callback,
ChatConfig,
ChatItem,
Feedback,
} from '../types'
import { CONVERSATION_ID_INFO } from '../constants'
import { getPrevChatList } from '../utils'
import { buildChatItemTree } from '../utils'
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import {
delConversation,
fetchAppInfo,
@ -40,6 +43,32 @@ import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
messages.forEach((item) => {
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
newChatList.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
parentMessageId: item.parent_message_id || undefined,
})
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
newChatList.push({
id: item.id,
content: item.answer,
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
feedback: item.feedback,
isAnswer: true,
citation: item.retriever_resources,
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
parentMessageId: `question-${item.id}`,
})
})
return newChatList
}
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
@ -109,9 +138,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
const appPrevChatList = useMemo(
const appPrevChatTree = useMemo(
() => (currentConversationId && appChatListData?.data.length)
? getPrevChatList(appChatListData.data)
? buildChatItemTree(getFormattedChatList(appChatListData.data))
: [],
[appChatListData, currentConversationId],
)
@ -403,7 +432,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
appConversationDataLoading,
appChatListData,
appChatListDataLoading,
appPrevChatList,
appPrevChatTree,
pinnedConversationList,
conversationList,
showConfigPanelBeforeChat,

View File

@ -30,7 +30,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
appInfoError,
appData,
appInfoLoading,
appPrevChatList,
appPrevChatTree,
showConfigPanelBeforeChat,
appChatListDataLoading,
chatShouldReloadKey,
@ -38,7 +38,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
themeBuilder,
} = useChatWithHistoryContext()
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length)
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatTree.length)
const customConfig = appData?.custom_config
const site = appData?.site
@ -76,9 +76,9 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
<HeaderInMobile />
)
}
<div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatList.length && 'flex items-center justify-center'}`}>
<div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatTree.length && 'flex items-center justify-center'}`}>
{
showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatList.length && (
showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatTree.length && (
<div className={`flex w-full items-center justify-center h-full ${isMobile && 'px-4'}`}>
<ConfigPanel />
</div>
@ -120,7 +120,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appChatListDataLoading,
currentConversationId,
currentConversationItem,
appPrevChatList,
appPrevChatTree,
pinnedConversationList,
conversationList,
showConfigPanelBeforeChat,
@ -154,7 +154,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appChatListDataLoading,
currentConversationId,
currentConversationItem,
appPrevChatList,
appPrevChatTree,
pinnedConversationList,
conversationList,
showConfigPanelBeforeChat,

View File

@ -209,19 +209,19 @@ const Answer: FC<AnswerProps> = ({
}
{item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && <div className="pt-3.5 flex justify-center items-center text-sm">
<button
className={`${item.prevSibling ? 'opacity-100' : 'opacity-65'}`}
className={`${item.prevSibling ? 'opacity-100' : 'opacity-30'}`}
disabled={!item.prevSibling}
onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)}
>
<ChevronRight className="w-[14px] h-[14px] rotate-180 text-text-tertiary" />
<ChevronRight className="w-[14px] h-[14px] rotate-180 text-text-primary" />
</button>
<span className="px-2 text-xs text-text-quaternary">{item.siblingIndex + 1} / {item.siblingCount}</span>
<span className="px-2 text-xs text-text-primary">{item.siblingIndex + 1} / {item.siblingCount}</span>
<button
className={`${item.nextSibling ? 'opacity-100' : 'opacity-65'}`}
className={`${item.nextSibling ? 'opacity-100' : 'opacity-30'}`}
disabled={!item.nextSibling}
onClick={() => item.nextSibling && switchSibling?.(item.nextSibling)}
>
<ChevronRight className="w-[14px] h-[14px] text-text-tertiary" />
<ChevronRight className="w-[14px] h-[14px] text-text-primary" />
</button>
</div>}
</div>

View File

@ -1,6 +1,7 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
@ -12,8 +13,10 @@ import { v4 as uuidV4 } from 'uuid'
import type {
ChatConfig,
ChatItem,
ChatItemInTree,
Inputs,
} from '../types'
import { getThreadMessages } from '../utils'
import type { InputForm } from './type'
import {
getProcessedInputs,
@ -46,7 +49,7 @@ export const useChat = (
inputs: Inputs
inputsForm: InputForm[]
},
prevChatList?: ChatItem[],
prevChatTree?: ChatItemInTree[],
stopChat?: (taskId: string) => void,
) => {
const { t } = useTranslation()
@ -56,14 +59,48 @@ export const useChat = (
const hasStopResponded = useRef(false)
const [isResponding, setIsResponding] = useState(false)
const isRespondingRef = useRef(false)
const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
const chatListRef = useRef<ChatItem[]>(prevChatList || [])
const taskIdRef = useRef('')
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
const conversationMessagesAbortControllerRef = useRef<AbortController | null>(null)
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
const params = useParams()
const pathname = usePathname()
const [chatTree, setChatTree] = useState<ChatItemInTree[]>(prevChatTree || [])
const chatTreeRef = useRef<ChatItemInTree[]>(chatTree)
const [targetMessageId, setTargetMessageId] = useState<string>()
const threadMessages = useMemo(() => getThreadMessages(chatTree, targetMessageId), [chatTree, targetMessageId])
const getIntroduction = useCallback((str: string) => {
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
}, [formSettings?.inputs, formSettings?.inputsForm])
/** Final chat list that will be rendered */
const chatList = useMemo(() => {
const ret = [...threadMessages]
if (config?.opening_statement) {
const index = threadMessages.findIndex(item => item.isOpeningStatement)
if (index > -1) {
ret[index] = {
...ret[index],
content: getIntroduction(config.opening_statement),
suggestedQuestions: config.suggested_questions,
}
}
else {
ret.unshift({
id: `${Date.now()}`,
content: getIntroduction(config.opening_statement),
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
})
}
}
return ret
}, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
useEffect(() => {
setAutoFreeze(false)
return () => {
@ -71,43 +108,50 @@ export const useChat = (
}
}, [])
const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
setChatList(newChatList)
chatListRef.current = newChatList
/** Find the target node by bfs and then operate on it */
const produceChatTreeNode = useCallback((targetId: string, operation: (node: ChatItemInTree) => void) => {
return produce(chatTreeRef.current, (draft) => {
const queue: ChatItemInTree[] = [...draft]
while (queue.length > 0) {
const current = queue.shift()!
if (current.id === targetId) {
operation(current)
break
}
if (current.children)
queue.push(...current.children)
}
})
}, [])
type UpdateChatTreeNode = {
(id: string, fields: Partial<ChatItemInTree>): void
(id: string, update: (node: ChatItemInTree) => void): void
}
const updateChatTreeNode: UpdateChatTreeNode = useCallback((
id: string,
fieldsOrUpdate: Partial<ChatItemInTree> | ((node: ChatItemInTree) => void),
) => {
const nextState = produceChatTreeNode(id, (node) => {
if (typeof fieldsOrUpdate === 'function') {
fieldsOrUpdate(node)
}
else {
Object.keys(fieldsOrUpdate).forEach((key) => {
(node as any)[key] = (fieldsOrUpdate as any)[key]
})
}
})
setChatTree(nextState)
chatTreeRef.current = nextState
}, [produceChatTreeNode])
const handleResponding = useCallback((isResponding: boolean) => {
setIsResponding(isResponding)
isRespondingRef.current = isResponding
}, [])
const getIntroduction = useCallback((str: string) => {
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
}, [formSettings?.inputs, formSettings?.inputsForm])
useEffect(() => {
if (config?.opening_statement) {
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const index = draft.findIndex(item => item.isOpeningStatement)
if (index > -1) {
draft[index] = {
...draft[index],
content: getIntroduction(config.opening_statement),
suggestedQuestions: config.suggested_questions,
}
}
else {
draft.unshift({
id: `${Date.now()}`,
content: getIntroduction(config.opening_statement),
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
})
}
}))
}
}, [config?.opening_statement, getIntroduction, config?.suggested_questions, handleUpdateChatList])
const handleStop = useCallback(() => {
hasStopResponded.current = true
handleResponding(false)
@ -123,50 +167,50 @@ export const useChat = (
conversationId.current = ''
taskIdRef.current = ''
handleStop()
const newChatList = config?.opening_statement
? [{
id: `${Date.now()}`,
content: config.opening_statement,
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
}]
: []
handleUpdateChatList(newChatList)
setChatTree([])
setSuggestQuestions([])
}, [
config,
handleStop,
handleUpdateChatList,
])
}, [handleStop])
const updateCurrentQA = useCallback(({
const updateCurrentQAOnTree = useCallback(({
parentId,
responseItem,
questionId,
placeholderAnswerId,
placeholderQuestionId,
questionItem,
}: {
parentId?: string
responseItem: ChatItem
questionId: string
placeholderAnswerId: string
placeholderQuestionId: string
questionItem: ChatItem
}) => {
const newListWithAnswer = produce(
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
let nextState: ChatItemInTree[]
const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] }
if (!parentId && !chatTree.some(item => [placeholderQuestionId, questionItem.id].includes(item.id))) {
// QA whose parent is not provided is considered as a first message of the conversation,
// and it should be a root node of the chat tree
nextState = produce(chatTree, (draft) => {
draft.push(currentQA)
})
handleUpdateChatList(newListWithAnswer)
}, [handleUpdateChatList])
}
else {
// find the target QA in the tree and update it; if not found, insert it to its parent node
nextState = produceChatTreeNode(parentId!, (parentNode) => {
const questionNodeIndex = parentNode.children!.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
if (questionNodeIndex === -1)
parentNode.children!.push(currentQA)
else
parentNode.children![questionNodeIndex] = currentQA
})
}
setChatTree(nextState)
chatTreeRef.current = nextState
}, [chatTree, produceChatTreeNode])
const handleSend = useCallback(async (
url: string,
data: {
query: string
files?: FileEntity[]
parent_message_id?: string
[key: string]: any
},
{
@ -183,12 +227,15 @@ export const useChat = (
return false
}
const questionId = `question-${Date.now()}`
const parentMessage = threadMessages.find(item => item.id === data.parent_message_id)
const placeholderQuestionId = `question-${Date.now()}`
const questionItem = {
id: questionId,
id: placeholderQuestionId,
content: data.query,
isAnswer: false,
message_files: data.files,
parentMessageId: data.parent_message_id,
}
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
@ -196,18 +243,27 @@ export const useChat = (
id: placeholderAnswerId,
content: '',
isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
}
const newList = [...chatListRef.current, questionItem, placeholderAnswerItem]
handleUpdateChatList(newList)
setTargetMessageId(parentMessage?.id)
updateCurrentQAOnTree({
parentId: data.parent_message_id,
responseItem: placeholderAnswerItem,
placeholderQuestionId,
questionItem,
})
// answer
const responseItem: ChatItem = {
const responseItem: ChatItemInTree = {
id: placeholderAnswerId,
content: '',
agent_thoughts: [],
message_files: [],
isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
}
handleResponding(true)
@ -268,7 +324,9 @@ export const useChat = (
}
if (messageId && !hasSetResponseId) {
questionItem.id = `question-${messageId}`
responseItem.id = messageId
responseItem.parentMessageId = questionItem.id
hasSetResponseId = true
}
@ -279,11 +337,11 @@ export const useChat = (
if (messageId)
responseItem.id = messageId
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
async onCompleted(hasError?: boolean) {
@ -304,43 +362,32 @@ export const useChat = (
if (!newResponseItem)
return
const newChatList = produce(chatListRef.current, (draft) => {
const index = draft.findIndex(item => item.id === responseItem.id)
if (index !== -1) {
const question = draft[index - 1]
draft[index - 1] = {
...question,
}
draft[index] = {
...draft[index],
content: newResponseItem.answer,
log: [
...newResponseItem.message,
...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
? [
{
role: 'assistant',
text: newResponseItem.answer,
files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
more: {
time: formatTime(newResponseItem.created_at, 'hh:mm A'),
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
latency: newResponseItem.provider_response_latency.toFixed(2),
},
// for agent log
conversationId: conversationId.current,
input: {
inputs: newResponseItem.inputs,
query: newResponseItem.query,
},
}
}
updateChatTreeNode(responseItem.id, {
content: newResponseItem.answer,
log: [
...newResponseItem.message,
...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
? [
{
role: 'assistant',
text: newResponseItem.answer,
files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
more: {
time: formatTime(newResponseItem.created_at, 'hh:mm A'),
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
latency: newResponseItem.provider_response_latency.toFixed(2),
},
// for agent log
conversationId: conversationId.current,
input: {
inputs: newResponseItem.inputs,
query: newResponseItem.query,
},
})
handleUpdateChatList(newChatList)
}
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
try {
@ -360,11 +407,11 @@ export const useChat = (
if (lastThought)
responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file]
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
onThought(thought) {
@ -372,6 +419,7 @@ export const useChat = (
const response = responseItem as any
if (thought.message_id && !hasSetResponseId)
response.id = thought.message_id
if (response.agent_thoughts.length === 0) {
response.agent_thoughts.push(thought)
}
@ -387,11 +435,11 @@ export const useChat = (
responseItem.agent_thoughts!.push(thought)
}
}
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
onMessageEnd: (messageEnd) => {
@ -401,43 +449,36 @@ export const useChat = (
id: messageEnd.metadata.annotation_reply.id,
authorName: messageEnd.metadata.annotation_reply.account.name,
})
const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId)
const newListWithAnswer = produce(
baseState,
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({
...responseItem,
})
})
handleUpdateChatList(newListWithAnswer)
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
return
}
responseItem.citation = messageEnd.metadata?.retriever_resources || []
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
const newListWithAnswer = produce(
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
})
handleUpdateChatList(newListWithAnswer)
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
onMessageReplace: (messageReplace) => {
responseItem.content = messageReplace.answer
},
onError() {
handleResponding(false)
const newChatList = produce(chatListRef.current, (draft) => {
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
handleUpdateChatList(newChatList)
},
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
taskIdRef.current = task_id
@ -446,89 +487,84 @@ export const useChat = (
status: WorkflowRunningStatus.Running,
tracing: [],
}
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
onWorkflowFinished: ({ data }) => {
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
onWorkflowFinished: ({ data: workflowFinishedData }) => {
responseItem.workflowProcess!.status = workflowFinishedData.status as WorkflowRunningStatus
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
onIterationStart: ({ data }) => {
onIterationStart: ({ data: iterationStartedData }) => {
responseItem.workflowProcess!.tracing!.push({
...data,
...iterationStartedData,
status: WorkflowRunningStatus.Running,
} as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
onIterationFinish: ({ data }) => {
onIterationFinish: ({ data: iterationFinishedData }) => {
const tracing = responseItem.workflowProcess!.tracing!
const iterationIndex = tracing.findIndex(item => item.node_id === data.node_id
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
const iterationIndex = tracing.findIndex(item => item.node_id === iterationFinishedData.node_id
&& (item.execution_metadata?.parallel_id === iterationFinishedData.execution_metadata?.parallel_id || item.parallel_id === iterationFinishedData.execution_metadata?.parallel_id))!
tracing[iterationIndex] = {
...tracing[iterationIndex],
...data,
...iterationFinishedData,
status: WorkflowRunningStatus.Succeeded,
} as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
onNodeStarted: ({ data }) => {
if (data.iteration_id)
onNodeStarted: ({ data: nodeStartedData }) => {
if (nodeStartedData.iteration_id)
return
responseItem.workflowProcess!.tracing!.push({
...data,
...nodeStartedData,
status: WorkflowRunningStatus.Running,
} as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
onNodeFinished: ({ data }) => {
if (data.iteration_id)
onNodeFinished: ({ data: nodeFinishedData }) => {
if (nodeFinishedData.iteration_id)
return
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => {
if (!item.execution_metadata?.parallel_id)
return item.node_id === data.node_id
return item.node_id === nodeFinishedData.node_id
return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata.parallel_id)
return item.node_id === nodeFinishedData.node_id && (item.execution_metadata?.parallel_id === nodeFinishedData.execution_metadata.parallel_id)
})
responseItem.workflowProcess!.tracing[currentIndex] = nodeFinishedData as any
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
responseItem.workflowProcess!.tracing[currentIndex] = data as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
onTTSChunk: (messageId: string, audio: string) => {
if (!audio || audio === '')
@ -542,11 +578,13 @@ export const useChat = (
})
return true
}, [
config?.suggested_questions_after_answer,
updateCurrentQA,
t,
chatTree.length,
threadMessages,
config?.suggested_questions_after_answer,
updateCurrentQAOnTree,
updateChatTreeNode,
notify,
handleUpdateChatList,
handleResponding,
formatTime,
params.token,
@ -556,76 +594,61 @@ export const useChat = (
])
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
handleUpdateChatList(chatListRef.current.map((item, i) => {
if (i === index - 1) {
return {
...item,
content: query,
}
}
if (i === index) {
return {
...item,
content: answer,
annotation: {
...item.annotation,
logAnnotation: undefined,
} as any,
}
}
return item
}))
}, [handleUpdateChatList])
const targetQuestionId = chatList[index - 1].id
const targetAnswerId = chatList[index].id
updateChatTreeNode(targetQuestionId, {
content: query,
})
updateChatTreeNode(targetAnswerId, {
content: answer,
annotation: {
...chatList[index].annotation,
logAnnotation: undefined,
} as any,
})
}, [chatList, updateChatTreeNode])
const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
handleUpdateChatList(chatListRef.current.map((item, i) => {
if (i === index - 1) {
return {
...item,
content: query,
}
}
if (i === index) {
const answerItem = {
...item,
content: item.content,
annotation: {
id: annotationId,
authorName,
logAnnotation: {
content: answer,
account: {
id: '',
name: authorName,
email: '',
},
},
} as Annotation,
}
return answerItem
}
return item
}))
}, [handleUpdateChatList])
const handleAnnotationRemoved = useCallback((index: number) => {
handleUpdateChatList(chatListRef.current.map((item, i) => {
if (i === index) {
return {
...item,
content: item.content,
annotation: {
...(item.annotation || {}),
const targetQuestionId = chatList[index - 1].id
const targetAnswerId = chatList[index].id
updateChatTreeNode(targetQuestionId, {
content: query,
})
updateChatTreeNode(targetAnswerId, {
content: chatList[index].content,
annotation: {
id: annotationId,
authorName,
logAnnotation: {
content: answer,
account: {
id: '',
} as Annotation,
}
}
return item
}))
}, [handleUpdateChatList])
name: authorName,
email: '',
},
},
} as Annotation,
})
}, [chatList, updateChatTreeNode])
const handleAnnotationRemoved = useCallback((index: number) => {
const targetAnswerId = chatList[index].id
updateChatTreeNode(targetAnswerId, {
content: chatList[index].content,
annotation: {
...(chatList[index].annotation || {}),
id: '',
} as Annotation,
})
}, [chatList, updateChatTreeNode])
return {
chatList,
chatListRef,
handleUpdateChatList,
setTargetMessageId,
conversationId: conversationId.current,
isResponding,
setIsResponding,

View File

@ -3,10 +3,11 @@ import Chat from '../chat'
import type {
ChatConfig,
ChatItem,
ChatItemInTree,
OnSend,
} from '../types'
import { useChat } from '../chat/hooks'
import { getLastAnswer } from '../utils'
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
import { useEmbeddedChatbotContext } from './context'
import ConfigPanel from './config-panel'
import { isDify } from './utils'
@ -51,13 +52,12 @@ const ChatWrapper = () => {
} as ChatConfig
}, [appParams, currentConversationItem?.introduction, currentConversationId])
const {
chatListRef,
chatList,
setTargetMessageId,
handleSend,
handleStop,
isResponding,
suggestedQuestions,
handleUpdateChatList,
} = useChat(
appConfig,
{
@ -71,15 +71,15 @@ const ChatWrapper = () => {
useEffect(() => {
if (currentChatInstanceRef.current)
currentChatInstanceRef.current.handleStop = handleStop
}, [])
}, [currentChatInstanceRef, handleStop])
const doSend: OnSend = useCallback((message, files, last_answer) => {
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
const data: any = {
query: message,
files,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
}
handleSend(
@ -92,32 +92,21 @@ const ChatWrapper = () => {
},
)
}, [
chatListRef,
appConfig,
chatList,
handleNewConversationCompleted,
handleSend,
currentConversationId,
currentConversationItem,
handleSend,
newConversationInputs,
handleNewConversationCompleted,
isInstalledApp,
appId,
])
const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id)
if (index === -1)
return
const prevMessages = chatList.slice(0, index)
const question = prevMessages.pop()
const lastAnswer = getLastAnswer(prevMessages)
if (!question)
return
handleUpdateChatList(prevMessages)
doSend(question.content, question.message_files, lastAnswer)
}, [chatList, handleUpdateChatList, doSend])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend])
const chatNode = useMemo(() => {
if (inputsForms.length) {
@ -172,6 +161,7 @@ const ChatWrapper = () => {
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
/>
)
}

View File

@ -67,9 +67,12 @@ export type ChatItem = IChatItem & {
export type ChatItemInTree = {
children?: ChatItemInTree[]
} & IChatItem
} & ChatItem
export type OnSend = (message: string, files?: FileEntity[], last_answer?: ChatItem | null) => void
export type OnSend = {
(message: string, files?: FileEntity[]): void
(message: string, files: FileEntity[] | undefined, isRegenerate: boolean, lastAnswer?: ChatItem | null): void
}
export type OnRegenerate = (chatItem: ChatItem) => void

View File

@ -1,8 +1,6 @@
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
import { UUID_NIL } from './constants'
import type { IChatItem } from './chat/type'
import type { ChatItem, ChatItemInTree } from './types'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
async function decodeBase64AndDecompress(base64String: string) {
const binaryString = atob(base64String)
@ -21,67 +19,24 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
return inputs
}
function getLastAnswer(chatList: ChatItem[]) {
function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
}
function getLastAnswer<T extends ChatItem | ChatItemInTree>(chatList: T[]): T | null {
for (let i = chatList.length - 1; i >= 0; i--) {
const item = chatList[i]
if (item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement)
if (isValidGeneratedAnswer(item))
return item
}
return null
}
function appendQAToChatList(chatList: ChatItem[], item: any) {
// we append answer first and then question since will reverse the whole chatList later
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
chatList.push({
id: item.id,
content: item.answer,
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
feedback: item.feedback,
isAnswer: true,
citation: item.retriever_resources,
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
})
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
chatList.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
})
}
/**
* Computes the latest thread messages from all messages of the conversation.
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
*
* @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
* @returns An array of ChatItems representing the latest thread.
* Build a chat item tree from a chat list
* @param allMessages - The chat list, sorted from oldest to newest
* @returns The chat item tree
*/
function getPrevChatList(fetchedMessages: any[]) {
const ret: ChatItem[] = []
let nextMessageId = null
for (const item of fetchedMessages) {
if (!item.parent_message_id) {
appendQAToChatList(ret, item)
break
}
if (!nextMessageId) {
appendQAToChatList(ret, item)
nextMessageId = item.parent_message_id
}
else {
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
appendQAToChatList(ret, item)
nextMessageId = item.parent_message_id
}
}
}
return ret.reverse()
}
function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
const map: Record<string, ChatItemInTree> = {}
const rootNodes: ChatItemInTree[] = []
@ -208,7 +163,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
export {
getProcessedInputsFromUrlParams,
getPrevChatList,
isValidGeneratedAnswer,
getLastAnswer,
buildChatItemTree,
getThreadMessages,

View File

@ -211,7 +211,7 @@ export const useFile = (fileConfig: FileUpload) => {
type: '',
size: 0,
progress: 0,
transferMethod: TransferMethod.local_file,
transferMethod: TransferMethod.remote_url,
supportFileType: '',
url,
isRemote: true,

View File

@ -229,7 +229,11 @@ export function Markdown(props: { content: string; className?: string }) {
return (
<div className={cn(props.className, 'markdown-body')}>
<ReactMarkdown
remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
remarkPlugins={[
RemarkGfm,
[RemarkMath, { singleDollarTextMath: false }],
RemarkBreaks,
]}
rehypePlugins={[
RehypeKatex,
RehypeRaw as any,

View File

@ -149,7 +149,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
<div className='relative min-h-5'>
<RichTextPlugin
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`} style={style || {}} />}
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-text-secondary`} style={style || {}} />}
placeholder={<Placeholder value={placeholder} className={cn('truncate', placeholderClassName)} compact={compact} />}
ErrorBoundary={LexicalErrorBoundary}
/>

View File

@ -133,7 +133,7 @@ export const useVariableOptions = (
return (
<VariableMenuItem
title={item.value}
icon={<BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />}
icon={<BracketsX className='w-[14px] h-[14px] text-text-accent' />}
queryString={queryString}
isSelected={isSelected}
onClick={onSelect}
@ -162,7 +162,7 @@ export const useVariableOptions = (
return (
<VariableMenuItem
title={t('common.promptEditor.variable.modal.add')}
icon={<BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />}
icon={<BracketsX className='w-[14px] h-[14px] text-text-accent' />}
queryString={queryString}
isSelected={isSelected}
onClick={onSelect}
@ -211,7 +211,7 @@ export const useExternalToolOptions = (
background={item.icon_background}
/>
}
extraElement={<div className='text-xs text-gray-400'>{item.variableName}</div>}
extraElement={<div className='text-xs text-text-tertiary'>{item.variableName}</div>}
queryString={queryString}
isSelected={isSelected}
onClick={onSelect}
@ -240,8 +240,8 @@ export const useExternalToolOptions = (
return (
<VariableMenuItem
title={t('common.promptEditor.variable.modal.addTool')}
icon={<Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />}
extraElement={< ArrowUpRight className='w-3 h-3 text-gray-400' />}
icon={<Tool03 className='w-[14px] h-[14px] text-text-accent' />}
extraElement={< ArrowUpRight className='w-3 h-3 text-text-tertiary' />}
queryString={queryString}
isSelected={isSelected}
onClick={onSelect}

View File

@ -135,7 +135,7 @@ const ComponentPicker = ({
// See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
<div className='w-0 h-0'>
<div
className='p-1 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'
className='p-1 w-[260px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'
style={{
...floatingStyles,
visibility: isPositioned ? 'visible' : 'hidden',
@ -148,7 +148,7 @@ const ComponentPicker = ({
{
// Divider
index !== 0 && options.at(index - 1)?.group !== option.group && (
<div className='h-px bg-gray-100 my-1 w-full -translate-x-1'></div>
<div className='h-px bg-divider-subtle my-1 w-full -translate-x-1'></div>
)
}
{option.renderMenuOption({
@ -169,7 +169,7 @@ const ComponentPicker = ({
<>
{
(!!options.length) && (
<div className='h-px bg-gray-100 my-1 w-full -translate-x-1'></div>
<div className='h-px bg-divider-subtle my-1 w-full -translate-x-1'></div>
)
}
<div className='p-1'>

View File

@ -21,9 +21,9 @@ export const PromptMenuItem = memo(({
return (
<div
className={`
flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50 rounded-md
${isSelected && !disabled && '!bg-gray-50'}
${disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
flex items-center px-3 h-6 cursor-pointer hover:bg-state-base-hover rounded-md
${isSelected && !disabled && '!bg-state-base-hover'}
${disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-state-base-hover cursor-pointer'}
`}
tabIndex={-1}
ref={setRefElement}
@ -38,7 +38,7 @@ export const PromptMenuItem = memo(({
onClick()
}}>
{icon}
<div className='ml-1 text-[13px] text-gray-900'>{title}</div>
<div className='ml-1 text-[13px] text-text-secondary'>{title}</div>
</div>
)
})

View File

@ -38,8 +38,8 @@ export const VariableMenuItem = memo(({
return (
<div
className={`
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
${isSelected && 'bg-primary-50'}
flex items-center px-3 h-6 rounded-md hover:bg-state-base-hover cursor-pointer
${isSelected && 'bg-state-base-hover'}
`}
tabIndex={-1}
ref={setRefElement}
@ -48,9 +48,9 @@ export const VariableMenuItem = memo(({
<div className='mr-2'>
{icon}
</div>
<div className='grow text-[13px] text-gray-900 truncate' title={title}>
<div className='grow text-[13px] text-text-secondary truncate' title={title}>
{before}
<span className='text-[#2970FF]'>{middle}</span>
<span className='text-text-accent'>{middle}</span>
{after}
</div>
{extraElement}

View File

@ -16,7 +16,7 @@ const Placeholder = ({
return (
<div className={cn(
className,
'absolute top-0 left-0 h-full w-full text-sm text-gray-300 select-none pointer-events-none',
'absolute top-0 left-0 h-full w-full text-sm text-components-input-text-placeholder select-none pointer-events-none',
compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm',
)}>
{value || t('common.promptEditor.placeholder')}

View File

@ -24,7 +24,7 @@ export class VariableValueBlockNode extends TextNode {
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config)
element.classList.add('inline-flex', 'items-center', 'px-0.5', 'h-[22px]', 'text-[#155EEF]', 'rounded-[5px]', 'align-middle')
element.classList.add('inline-flex', 'items-center', 'px-0.5', 'h-[22px]', 'text-text-accent', 'rounded-[5px]', 'align-middle')
return element
}

View File

@ -77,8 +77,8 @@ const WorkflowVariableBlockComponent = ({
<div
className={cn(
'mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none',
isSelected ? ' border-[#84ADFF] bg-[#F5F8FF]' : ' border-black/5 bg-white',
!node && !isEnv && !isChatVar && '!border-[#F04438] !bg-[#FEF3F2]',
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
!node && !isEnv && !isChatVar && '!border-state-destructive-solid !bg-state-destructive-hover',
)}
ref={ref}
>
@ -88,25 +88,30 @@ const WorkflowVariableBlockComponent = ({
node?.type && (
<div className='p-[1px]'>
<VarBlockIcon
className='!text-gray-500'
className='!text-text-secondary'
type={node?.type}
/>
</div>
)
}
<div className='shrink-0 mx-0.5 max-w-[60px] text-xs font-medium text-gray-500 truncate' title={node?.title} style={{
<div className='shrink-0 mx-0.5 max-w-[60px] text-xs font-medium text-text-secondary truncate' title={node?.title} style={{
}}>{node?.title}</div>
<Line3 className='mr-0.5 text-gray-300'></Line3>
<Line3 className='mr-0.5 text-divider-deep'></Line3>
</div>
)}
<div className='flex items-center text-primary-600'>
<div className='flex items-center text-text-accent'>
{!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5', isException && 'text-text-warning')} />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900', isException && 'text-text-warning')} title={varName}>{varName}</div>
<div className={cn(
'shrink-0 ml-0.5 text-xs font-medium truncate',
isEnv && 'text-util-colors-violet-violet-600',
isChatVar && 'text-util-colors-teal-teal-700',
isException && 'text-text-warning',
)} title={varName}>{varName}</div>
{
!node && !isEnv && !isChatVar && (
<RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />
<RiErrorWarningFill className='ml-0.5 w-3 h-3 text-text-destructive' />
)
}
</div>

View File

@ -75,7 +75,7 @@ const TagManagementModal = ({ show, type }: TagManagementModalProps) => {
autoFocus
value={name}
onChange={e => setName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && createNewTag()}
onKeyDown={e => e.key === 'Enter' && !e.nativeEvent.isComposing && createNewTag()}
onBlur={createNewTag}
/>
{tagList.map(tag => (

View File

@ -17,7 +17,7 @@ import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import { updateDatasetSetting } from '@/service/datasets'
import { type DataSetListResponse } from '@/models/datasets'
import { type DataSetListResponse, DatasetPermission } from '@/models/datasets'
import DatasetDetailContext from '@/context/dataset-detail'
import { type RetrievalConfig } from '@/types/app'
import { useAppContext } from '@/context/app-context'
@ -145,7 +145,7 @@ const Form = () => {
}),
},
} as any
if (permission === 'partial_members') {
if (permission === DatasetPermission.partialMembers) {
requestParams.body.partial_member_list = selectedMemberIDs.map((id) => {
return {
user_id: id,

View File

@ -12,7 +12,7 @@ import Avatar from '@/app/components/base/avatar'
import Input from '@/app/components/base/input'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
import { Users01, UsersPlus } from '@/app/components/base/icons/src/vender/solid/users'
import type { DatasetPermission } from '@/models/datasets'
import { DatasetPermission } from '@/models/datasets'
import { useAppContext } from '@/context/app-context'
import type { Member } from '@/models/common'
export type RoleSelectorProps = {
@ -60,6 +60,10 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
return memberList.filter(member => (member.name.includes(searchKeywords) || member.email.includes(searchKeywords)) && member.id !== userProfile.id && ['owner', 'admin', 'editor', 'dataset_operator'].includes(member.role))
}, [memberList, searchKeywords, userProfile])
const isOnlyMe = permission === DatasetPermission.onlyMe
const isAllTeamMembers = permission === DatasetPermission.allTeamMembers
const isPartialMembers = permission === DatasetPermission.partialMembers
return (
<PortalToFollowElem
open={open}
@ -72,14 +76,14 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
onClick={() => !disabled && setOpen(v => !v)}
className='block'
>
{permission === 'only_me' && (
{isOnlyMe && (
<div className={cn('flex items-center px-3 py-[6px] rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200', disabled && 'hover:!bg-gray-100 !cursor-default')}>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0 mr-2' size={24} />
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
{!disabled && <RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />}
</div>
)}
{permission === 'all_team_members' && (
{isAllTeamMembers && (
<div className={cn('flex items-center px-3 py-[6px] rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<div className='mr-2 flex items-center justify-center w-6 h-6 rounded-lg bg-[#EEF4FF]'>
<Users01 className='w-3.5 h-3.5 text-[#444CE7]' />
@ -88,7 +92,7 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
{!disabled && <RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />}
</div>
)}
{permission === 'partial_members' && (
{isPartialMembers && (
<div className={cn('flex items-center px-3 py-[6px] rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<div className='mr-2 flex items-center justify-center w-6 h-6 rounded-lg bg-[#EEF4FF]'>
<Users01 className='w-3.5 h-3.5 text-[#444CE7]' />
@ -102,17 +106,17 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
<div className='relative w-[480px] rounded-lg border-[0.5px] bg-white shadow-lg'>
<div className='p-1'>
<div className='pl-3 pr-2 py-1 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
onChange('only_me')
onChange(DatasetPermission.onlyMe)
setOpen(false)
}}>
<div className='flex items-center gap-2'>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0 mr-2' size={24} />
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
{permission === 'only_me' && <Check className='w-4 h-4 text-primary-600' />}
{isOnlyMe && <Check className='w-4 h-4 text-primary-600' />}
</div>
</div>
<div className='pl-3 pr-2 py-1 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
onChange('all_team_members')
onChange(DatasetPermission.allTeamMembers)
setOpen(false)
}}>
<div className='flex items-center gap-2'>
@ -120,23 +124,23 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
<Users01 className='w-3.5 h-3.5 text-[#444CE7]' />
</div>
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsAllMember')}</div>
{permission === 'all_team_members' && <Check className='w-4 h-4 text-primary-600' />}
{isAllTeamMembers && <Check className='w-4 h-4 text-primary-600' />}
</div>
</div>
<div className='pl-3 pr-2 py-1 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
onChange('partial_members')
onChange(DatasetPermission.partialMembers)
onMemberSelect([userProfile.id])
}}>
<div className='flex items-center gap-2'>
<div className={cn('mr-2 flex items-center justify-center w-6 h-6 rounded-lg bg-[#FFF6ED]', permission === 'partial_members' && '!bg-[#EEF4FF]')}>
<UsersPlus className={cn('w-3.5 h-3.5 text-[#FB6514]', permission === 'partial_members' && '!text-[#444CE7]')} />
<div className={cn('mr-2 flex items-center justify-center w-6 h-6 rounded-lg bg-[#FFF6ED]', isPartialMembers && '!bg-[#EEF4FF]')}>
<UsersPlus className={cn('w-3.5 h-3.5 text-[#FB6514]', isPartialMembers && '!text-[#444CE7]')} />
</div>
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsInvitedMembers')}</div>
{permission === 'partial_members' && <Check className='w-4 h-4 text-primary-600' />}
{isPartialMembers && <Check className='w-4 h-4 text-primary-600' />}
</div>
</div>
</div>
{permission === 'partial_members' && (
{isPartialMembers && (
<div className='max-h-[360px] border-t-[1px] border-gray-100 p-1 overflow-y-auto'>
<div className='sticky left-0 top-0 p-2 pb-1 bg-white'>
<Input

View File

@ -1,66 +0,0 @@
'use client'
import { useTranslation } from 'react-i18next'
import s from './index.module.css'
import classNames from '@/utils/classnames'
import type { DataSet } from '@/models/datasets'
const itemClass = `
flex items-center w-full sm:w-[234px] h-12 px-3 rounded-xl bg-gray-25 border border-gray-100 cursor-pointer
`
const radioClass = `
w-4 h-4 border-[2px] border-gray-200 rounded-full
`
type IPermissionsRadioProps = {
value?: DataSet['permission']
onChange: (v?: DataSet['permission']) => void
itemClassName?: string
disable?: boolean
}
const PermissionsRadio = ({
value,
onChange,
itemClassName,
disable,
}: IPermissionsRadioProps) => {
const { t } = useTranslation()
const options = [
{
key: 'only_me',
text: t('datasetSettings.form.permissionsOnlyMe'),
},
{
key: 'all_team_members',
text: t('datasetSettings.form.permissionsAllMember'),
},
]
return (
<div className={classNames(s.wrapper, 'flex justify-between w-full flex-wrap gap-y-2')}>
{
options.map(option => (
<div
key={option.key}
className={classNames(
itemClass,
itemClassName,
s.item,
option.key === value && s['item-active'],
disable && s.disable,
)}
onClick={() => {
if (!disable)
onChange(option.key as DataSet['permission'])
}}
>
<div className={classNames(s['user-icon'], 'mr-3')} />
<div className='grow text-sm text-gray-900'>{option.text}</div>
<div className={classNames(radioClass, s.radio)} />
</div>
))
}
</div>
)
}
export default PermissionsRadio

View File

@ -192,8 +192,8 @@ The text generation application offers non-session support and is ideal for tran
data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": "'m", "created_at": 1679586595}
data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " glad", "created_at": 1679586595}
data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " to", "created_at": 1679586595}
data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " meet", "created_at": 1679586595}
data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " you", "created_at": 1679586595}
data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " meet", "created_at": 1679586595}
data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " you", "created_at": 1679586595}
data: {"event": "message_end", "id": "5e52ce04-874b-4d27-9045-b3bc80def685", "metadata": {"usage": {"prompt_tokens": 1033, "prompt_unit_price": "0.001", "prompt_price_unit": "0.001", "prompt_price": "0.0010330", "completion_tokens": 135, "completion_unit_price": "0.002", "completion_price_unit": "0.001", "completion_price": "0.0002700", "total_tokens": 1168, "total_price": "0.0013030", "currency": "USD", "latency": 1.381760165997548}}}
data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"}
data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""}
@ -400,7 +400,7 @@ The text generation application offers non-session support and is ideal for tran
For text messages generated by Dify, simply pass the generated message-id directly. The backend will use the message-id to look up the corresponding content and synthesize the voice information directly. If both message_id and text are provided simultaneously, the message_id is given priority.
</Property>
<Property name='text' type='str' key='text'>
Speech generated content
Speech generated content.
</Property>
<Property name='user' type='string' key='user'>
The user identifier, defined by the developer, must ensure uniqueness within the app.

View File

@ -0,0 +1,5 @@
import { BlockEnum } from './types'
export const ALL_AVAILABLE_BLOCKS = Object.values(BlockEnum)
export const ALL_CHAT_AVAILABLE_BLOCKS = ALL_AVAILABLE_BLOCKS.filter(key => key !== BlockEnum.End && key !== BlockEnum.Start) as BlockEnum[]
export const ALL_COMPLETION_AVAILABLE_BLOCKS = ALL_AVAILABLE_BLOCKS.filter(key => key !== BlockEnum.Answer && key !== BlockEnum.Start) as BlockEnum[]

View File

@ -203,9 +203,6 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
}
export const ALL_CHAT_AVAILABLE_BLOCKS = Object.keys(NODES_EXTRA_DATA).filter(key => key !== BlockEnum.End && key !== BlockEnum.Start) as BlockEnum[]
export const ALL_COMPLETION_AVAILABLE_BLOCKS = Object.keys(NODES_EXTRA_DATA).filter(key => key !== BlockEnum.Answer && key !== BlockEnum.Start) as BlockEnum[]
export const NODES_INITIAL_DATA = {
[BlockEnum.Start]: {
type: BlockEnum.Start,

View File

@ -45,13 +45,13 @@ const NodeControl: FC<NodeControlProps> = ({
`}
>
<div
className='flex items-center px-0.5 h-6 bg-white rounded-lg border-[0.5px] border-gray-100 shadow-xs text-gray-500'
className='flex items-center px-0.5 h-6 bg-components-actionbar-bg rounded-lg border-[0.5px] border-components-actionbar-border backdrop-blur-[5px] shadow-md text-text-tertiary'
onClick={e => e.stopPropagation()}
>
{
canRunBySingle(data.type) && (
<div
className='flex items-center justify-center w-5 h-5 rounded-md cursor-pointer hover:bg-black/5'
className='flex items-center justify-center w-5 h-5 rounded-md cursor-pointer hover:bg-state-base-hover'
onClick={() => {
handleNodeDataUpdate({
id,

View File

@ -54,12 +54,12 @@ const PanelOperator = ({
<div
className={`
flex items-center justify-center w-6 h-6 rounded-md cursor-pointer
hover:bg-black/5
${open && 'bg-black/5'}
hover:bg-state-base-hover
${open && 'bg-state-base-hover'}
${triggerClassName}
`}
>
<RiMoreFill className={`w-4 h-4 ${inNode ? 'text-gray-500' : 'text-gray-700'}`} />
<RiMoreFill className={'w-4 h-4 text-text-tertiary'} />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>

View File

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

View File

@ -270,7 +270,7 @@ const VarReferencePicker: FC<Props> = ({
<AddButton onClick={() => { }}></AddButton>
</div>
)
: (<div ref={!isSupportConstantValue ? triggerRef : null} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8', !isSupportConstantValue && 'p-1 rounded-lg bg-gray-100 border', isInTable && 'bg-transparent border-none', readonly && 'bg-components-input-bg-disabled')}>
: (<div ref={!isSupportConstantValue ? triggerRef : null} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8', !isSupportConstantValue && 'p-1 rounded-lg bg-components-input-bg-normal', isInTable && 'bg-transparent border-none', readonly && 'bg-components-input-bg-disabled')}>
{isSupportConstantValue
? <div onClick={(e) => {
e.stopPropagation()

View File

@ -107,7 +107,7 @@ const BaseNode: FC<BaseNodeProps> = ({
'group relative pb-1 shadow-xs',
'border border-transparent rounded-[15px]',
data.type !== BlockEnum.Iteration && 'w-[240px] bg-workflow-block-bg',
data.type === BlockEnum.Iteration && 'flex flex-col w-full h-full bg-[#fcfdff]/80',
data.type === BlockEnum.Iteration && 'flex flex-col w-full h-full bg-workflow-block-bg-transparent border-workflow-block-border',
!data._runningStatus && 'hover:shadow-lg',
showRunningBorder && '!border-state-accent-solid',
showSuccessBorder && '!border-state-success-solid',
@ -169,7 +169,7 @@ const BaseNode: FC<BaseNodeProps> = ({
}
<div className={cn(
'flex items-center px-3 pt-3 pb-2 rounded-t-2xl',
data.type === BlockEnum.Iteration && 'bg-[rgba(250,252,255,0.9)]',
data.type === BlockEnum.Iteration && 'bg-transparent',
)}>
<BlockIcon
className='shrink-0 mr-2'

View File

@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { AnswerNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault<AnswerNodeType> = {
defaultValue: {

View File

@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type AssignerNodeType, WriteMode } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault<AssignerNodeType> = {

View File

@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { CodeLanguage, type CodeNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'

View File

@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type DocExtractorNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault<DocExtractorNodeType> = {

View File

@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type EndNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault<EndNodeType> = {
defaultValue: {

View File

@ -5,7 +5,7 @@ import type { BodyPayload, HttpNodeType } from './types'
import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
} from '@/app/components/workflow/constants'
} from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault<HttpNodeType> = {
defaultValue: {

View File

@ -2,7 +2,7 @@ import { BlockEnum, type NodeDefault } from '../../types'
import { type IfElseNodeType, LogicalOperator } from './types'
import { isEmptyRelatedOperator } from './utils'
import { TransferMethod } from '@/types/app'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault<IfElseNodeType> = {

View File

@ -1,6 +1,6 @@
import type { NodeDefault } from '../../types'
import type { IterationStartNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault<IterationStartNodeType> = {
defaultValue: {},

View File

@ -9,7 +9,7 @@ const IterationStartNode = ({ id, data }: NodeProps) => {
const { t } = useTranslation()
return (
<div className='group flex nodrag items-center justify-center w-11 h-11 mt-1 rounded-2xl border border-workflow-block-border bg-white'>
<div className='group flex nodrag items-center justify-center w-11 h-11 mt-1 rounded-2xl border border-workflow-block-border bg-workflow-block-bg shadow-xs'>
<Tooltip popupContent={t('workflow.blocks.iteration-start')} asChild={false}>
<div className='flex items-center justify-center w-6 h-6 rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500'>
<RiHome5Fill className='w-3 h-3 text-text-primary-on-surface' />

View File

@ -49,9 +49,9 @@ const AddBlock = ({
const renderTriggerElement = useCallback((open: boolean) => {
return (
<div className={cn(
'relative inline-flex items-center px-3 h-8 rounded-lg border-[0.5px] border-gray-50 bg-white shadow-xs cursor-pointer hover:bg-gray-200 text-[13px] font-medium text-gray-700',
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
open && '!bg-gray-50',
'relative inline-flex items-center px-3 h-8 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs cursor-pointer hover:bg-components-button-secondary-bg-hover system-sm-medium text-components-button-secondary-text backdrop-blur-[5px]',
`${nodesReadOnly && '!cursor-not-allowed bg-components-button-secondary-bg-disabled'}`,
open && 'bg-components-button-secondary-bg-hover',
)}>
<RiAddLine className='mr-1 w-4 h-4' />
{t('workflow.common.addBlock')}

View File

@ -4,7 +4,7 @@ import type { IterationNodeType } from './types'
import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
} from '@/app/components/workflow/constants'
} from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow'
const nodeDefault: NodeDefault<IterationNodeType> = {

View File

@ -43,14 +43,14 @@ const Node: FC<NodeProps<IterationNodeType>> = ({
return (
<div className={cn(
'relative min-w-[240px] min-h-[90px] w-full h-full rounded-2xl bg-[#F0F2F7]/90',
'relative min-w-[240px] min-h-[90px] w-full h-full rounded-2xl',
)}>
<Background
id={`iteration-background-${id}`}
className='rounded-2xl !z-0'
gap={[14 / zoom, 14 / zoom]}
size={2 / zoom}
color='#E4E5E7'
color='var(--color-workflow-canvas-workflow-dot-color)'
/>
{
data._isCandidate && (

View File

@ -76,7 +76,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<Field
title={t(`${i18nPrefix}.input`)}
operations={(
<div className='flex items-center h-[18px] px-1 border border-black/8 rounded-[5px] text-xs font-medium text-gray-500 capitalize'>Array</div>
<div className='flex items-center h-[18px] px-1 border border-divider-deep rounded-[5px] system-2xs-medium-uppercase text-text-tertiary capitalize'>Array</div>
)}
>
<VarReferencePicker
@ -94,7 +94,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<Field
title={t(`${i18nPrefix}.output`)}
operations={(
<div className='flex items-center h-[18px] px-1 border border-black/8 rounded-[5px] text-xs font-medium text-gray-500 capitalize'>Array</div>
<div className='flex items-center h-[18px] px-1 border border-divider-deep rounded-[5px] system-2xs-medium-uppercase text-text-tertiary capitalize'>Array</div>
)}
>
<VarReferencePicker
@ -134,8 +134,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<div className='px-4 py-2'>
<Field title={t(`${i18nPrefix}.errorResponseMethod`)} >
<Select items={responseMethod} defaultValue={inputs.error_handle_mode} onSelect={changeErrorResponseMode} allowSearch={false}>
</Select>
<Select items={responseMethod} defaultValue={inputs.error_handle_mode} onSelect={changeErrorResponseMode} allowSearch={false} />
</Field>
</div>

View File

@ -23,6 +23,7 @@ type Props = {
onRemove: () => void
onChange: (dataSet: DataSet) => void
readonly?: boolean
editable?: boolean
}
const DatasetItem: FC<Props> = ({
@ -30,6 +31,7 @@ const DatasetItem: FC<Props> = ({
onRemove,
onChange,
readonly,
editable = true,
}) => {
const media = useBreakpoints()
const { t } = useTranslation()
@ -75,14 +77,16 @@ const DatasetItem: FC<Props> = ({
</div>
{!readonly && (
<div className='hidden group-hover/dataset-item:flex shrink-0 ml-2 items-center space-x-1'>
<ActionButton
onClick={(e) => {
e.stopPropagation()
showSettingsModal()
}}
>
<RiEditLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
</ActionButton>
{
editable && <ActionButton
onClick={(e) => {
e.stopPropagation()
showSettingsModal()
}}
>
<RiEditLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
</ActionButton>
}
<ActionButton
onClick={handleRemove}
state={ActionButtonState.Destructive}
@ -102,7 +106,7 @@ const DatasetItem: FC<Props> = ({
{
payload.provider === 'external' && <Badge
className='group-hover/dataset-item:hidden shrink-0'
text={t('dataset.externalTag')}
text={t('dataset.externalTag') as string}
/>
}

View File

@ -1,10 +1,13 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import React, { useCallback, useMemo } from 'react'
import produce from 'immer'
import { useTranslation } from 'react-i18next'
import Item from './dataset-item'
import type { DataSet } from '@/models/datasets'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { hasEditPermissionForDataset } from '@/utils/permission'
type Props = {
list: DataSet[]
onChange: (list: DataSet[]) => void
@ -17,6 +20,7 @@ const DatasetList: FC<Props> = ({
readonly,
}) => {
const { t } = useTranslation()
const userProfile = useAppContextSelector(s => s.userProfile)
const handleRemove = useCallback((index: number) => {
return () => {
@ -35,10 +39,25 @@ const DatasetList: FC<Props> = ({
onChange(newList)
}
}, [list, onChange])
const formattedList = useMemo(() => {
return list.map((item) => {
const datasetConfig = {
createdBy: item.created_by,
partialMemberList: item.partial_member_list || [],
permission: item.permission,
}
return {
...item,
editable: hasEditPermissionForDataset(userProfile?.id || '', datasetConfig),
}
})
}, [list, userProfile?.id])
return (
<div className='space-y-1'>
{list.length
? list.map((item, index) => {
{formattedList.length
? formattedList.map((item, index) => {
return (
<Item
key={index}
@ -46,6 +65,7 @@ const DatasetList: FC<Props> = ({
onRemove={handleRemove(index)}
onChange={handleChange(index)}
readonly={readonly}
editable={item.editable}
/>
)
})

View File

@ -2,7 +2,7 @@ import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { KnowledgeRetrievalNodeType } from './types'
import { checkoutRerankModelConfigedInRetrievalSettings } from './utils'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import { DATASET_DEFAULT } from '@/config'
import { RETRIEVE_TYPE } from '@/types/app'
const i18nPrefix = 'workflow'

View File

@ -2,7 +2,7 @@ import { BlockEnum, VarType } from '../../types'
import type { NodeDefault } from '../../types'
import { comparisonOperatorNotRequireValue } from '../if-else/utils'
import { type ListFilterNodeType, OrderBy } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault<ListFilterNodeType> = {

View File

@ -1,7 +1,7 @@
import { BlockEnum, EditionType } from '../../types'
import { type NodeDefault, type PromptItem, PromptRole } from '../../types'
import type { LLMNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'

View File

@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type ParameterExtractorNodeType, ReasoningModeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow'
const nodeDefault: NodeDefault<ParameterExtractorNodeType> = {

View File

@ -1,7 +1,7 @@
import type { NodeDefault } from '../../types'
import { BlockEnum } from '../../types'
import type { QuestionClassifierNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow'

View File

@ -1,6 +1,6 @@
import type { NodeDefault } from '../../types'
import type { StartNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault<StartNodeType> = {
defaultValue: {

View File

@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { TemplateTransformNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault<TemplateTransformNodeType> = {

View File

@ -2,7 +2,7 @@ import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { ToolNodeType } from './types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'

View File

@ -1,7 +1,7 @@
import { type NodeDefault, VarType } from '../../types'
import { BlockEnum } from '../../types'
import type { VariableAssignerNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow'

View File

@ -19,14 +19,14 @@ import ConversationVariableModal from './conversation-variable-modal'
import { useChat } from './hooks'
import type { ChatWrapperRefType } from './index'
import Chat from '@/app/components/base/chat/chat'
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
import type { ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
import { useFeatures } from '@/app/components/base/features/hooks'
import {
fetchSuggestedQuestions,
stopChatMessageResponding,
} from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
import { getLastAnswer } from '@/app/components/base/chat/utils'
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
type ChatWrapperProps = {
showConversationVariableModal: boolean
@ -65,13 +65,12 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
const {
conversationId,
chatList,
chatListRef,
handleUpdateChatList,
handleStop,
isResponding,
suggestedQuestions,
handleSend,
handleRestart,
setTargetMessageId,
} = useChat(
config,
{
@ -82,36 +81,26 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
taskId => stopChatMessageResponding(appDetail!.id, taskId),
)
const doSend = useCallback<OnSend>((query, files, last_answer) => {
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
handleSend(
{
query,
query: message,
files,
inputs: workflowStore.getState().inputs,
conversation_id: conversationId,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || undefined,
},
{
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
},
)
}, [chatListRef, conversationId, handleSend, workflowStore, appDetail])
}, [handleSend, workflowStore, conversationId, chatList, appDetail])
const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id)
if (index === -1)
return
const prevMessages = chatList.slice(0, index)
const question = prevMessages.pop()
const lastAnswer = getLastAnswer(prevMessages)
if (!question)
return
handleUpdateChatList(prevMessages)
doSend(question.content, question.message_files, lastAnswer)
}, [chatList, handleUpdateChatList, doSend])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend])
useImperativeHandle(ref, () => {
return {
@ -159,6 +148,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
suggestedQuestions={suggestedQuestions}
showPromptLog
chatAnswerContainerInner='!pr-2'
switchSibling={setTargetMessageId}
/>
{showConversationVariableModal && (
<ConversationVariableModal

View File

@ -1,6 +1,7 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
@ -13,6 +14,7 @@ import { useWorkflowStore } from '../../store'
import { DEFAULT_ITER_TIMES } from '../../constants'
import type {
ChatItem,
ChatItemInTree,
Inputs,
} from '@/app/components/base/chat/types'
import type { InputForm } from '@/app/components/base/chat/chat/type'
@ -27,6 +29,7 @@ import {
getProcessedFilesFromResponse,
} from '@/app/components/base/file-uploader/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import { getThreadMessages } from '@/app/components/base/chat/utils'
import type { NodeTracing } from '@/types/workflow'
type GetAbortController = (abortController: AbortController) => void
@ -39,7 +42,7 @@ export const useChat = (
inputs: Inputs
inputsForm: InputForm[]
},
prevChatList?: ChatItem[],
prevChatTree?: ChatItemInTree[],
stopChat?: (taskId: string) => void,
) => {
const { t } = useTranslation()
@ -49,16 +52,54 @@ export const useChat = (
const workflowStore = useWorkflowStore()
const conversationId = useRef('')
const taskIdRef = useRef('')
const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
const chatListRef = useRef<ChatItem[]>(prevChatList || [])
const [isResponding, setIsResponding] = useState(false)
const isRespondingRef = useRef(false)
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
const {
setIterTimes,
} = workflowStore.getState()
const handleResponding = useCallback((isResponding: boolean) => {
setIsResponding(isResponding)
isRespondingRef.current = isResponding
}, [])
const [chatTree, setChatTree] = useState<ChatItemInTree[]>(prevChatTree || [])
const chatTreeRef = useRef<ChatItemInTree[]>(chatTree)
const [targetMessageId, setTargetMessageId] = useState<string>()
const threadMessages = useMemo(() => getThreadMessages(chatTree, targetMessageId), [chatTree, targetMessageId])
const getIntroduction = useCallback((str: string) => {
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
}, [formSettings?.inputs, formSettings?.inputsForm])
/** Final chat list that will be rendered */
const chatList = useMemo(() => {
const ret = [...threadMessages]
if (config?.opening_statement) {
const index = threadMessages.findIndex(item => item.isOpeningStatement)
if (index > -1) {
ret[index] = {
...ret[index],
content: getIntroduction(config.opening_statement),
suggestedQuestions: config.suggested_questions,
}
}
else {
ret.unshift({
id: `${Date.now()}`,
content: getIntroduction(config.opening_statement),
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
})
}
}
return ret
}, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
useEffect(() => {
setAutoFreeze(false)
return () => {
@ -66,43 +107,21 @@ export const useChat = (
}
}, [])
const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
setChatList(newChatList)
chatListRef.current = newChatList
}, [])
const handleResponding = useCallback((isResponding: boolean) => {
setIsResponding(isResponding)
isRespondingRef.current = isResponding
}, [])
const getIntroduction = useCallback((str: string) => {
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
}, [formSettings?.inputs, formSettings?.inputsForm])
useEffect(() => {
if (config?.opening_statement) {
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const index = draft.findIndex(item => item.isOpeningStatement)
if (index > -1) {
draft[index] = {
...draft[index],
content: getIntroduction(config.opening_statement),
suggestedQuestions: config.suggested_questions,
}
/** Find the target node by bfs and then operate on it */
const produceChatTreeNode = useCallback((targetId: string, operation: (node: ChatItemInTree) => void) => {
return produce(chatTreeRef.current, (draft) => {
const queue: ChatItemInTree[] = [...draft]
while (queue.length > 0) {
const current = queue.shift()!
if (current.id === targetId) {
operation(current)
break
}
else {
draft.unshift({
id: `${Date.now()}`,
content: getIntroduction(config.opening_statement),
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
})
}
}))
}
}, [config?.opening_statement, getIntroduction, config?.suggested_questions, handleUpdateChatList])
if (current.children)
queue.push(...current.children)
}
})
}, [])
const handleStop = useCallback(() => {
hasStopResponded.current = true
@ -119,50 +138,52 @@ export const useChat = (
taskIdRef.current = ''
handleStop()
setIterTimes(DEFAULT_ITER_TIMES)
const newChatList = config?.opening_statement
? [{
id: `${Date.now()}`,
content: config.opening_statement,
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
}]
: []
handleUpdateChatList(newChatList)
setChatTree([])
setSuggestQuestions([])
}, [
config,
handleStop,
handleUpdateChatList,
setIterTimes,
])
const updateCurrentQA = useCallback(({
const updateCurrentQAOnTree = useCallback(({
parentId,
responseItem,
questionId,
placeholderAnswerId,
placeholderQuestionId,
questionItem,
}: {
parentId?: string
responseItem: ChatItem
questionId: string
placeholderAnswerId: string
placeholderQuestionId: string
questionItem: ChatItem
}) => {
const newListWithAnswer = produce(
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
let nextState: ChatItemInTree[]
const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] }
if (!parentId && !chatTree.some(item => [placeholderQuestionId, questionItem.id].includes(item.id))) {
// QA whose parent is not provided is considered as a first message of the conversation,
// and it should be a root node of the chat tree
nextState = produce(chatTree, (draft) => {
draft.push(currentQA)
})
handleUpdateChatList(newListWithAnswer)
}, [handleUpdateChatList])
}
else {
// find the target QA in the tree and update it; if not found, insert it to its parent node
nextState = produceChatTreeNode(parentId!, (parentNode) => {
const questionNodeIndex = parentNode.children!.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
if (questionNodeIndex === -1)
parentNode.children!.push(currentQA)
else
parentNode.children![questionNodeIndex] = currentQA
})
}
setChatTree(nextState)
chatTreeRef.current = nextState
}, [chatTree, produceChatTreeNode])
const handleSend = useCallback((
params: {
query: string
files?: FileEntity[]
parent_message_id?: string
[key: string]: any
},
{
@ -174,12 +195,15 @@ export const useChat = (
return false
}
const questionId = `question-${Date.now()}`
const parentMessage = threadMessages.find(item => item.id === params.parent_message_id)
const placeholderQuestionId = `question-${Date.now()}`
const questionItem = {
id: questionId,
id: placeholderQuestionId,
content: params.query,
isAnswer: false,
message_files: params.files,
parentMessageId: params.parent_message_id,
}
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
@ -187,10 +211,17 @@ export const useChat = (
id: placeholderAnswerId,
content: '',
isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
}
const newList = [...chatListRef.current, questionItem, placeholderAnswerItem]
handleUpdateChatList(newList)
setTargetMessageId(parentMessage?.id)
updateCurrentQAOnTree({
parentId: params.parent_message_id,
responseItem: placeholderAnswerItem,
placeholderQuestionId,
questionItem,
})
// answer
const responseItem: ChatItem = {
@ -199,6 +230,8 @@ export const useChat = (
agent_thoughts: [],
message_files: [],
isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
}
handleResponding(true)
@ -230,7 +263,9 @@ export const useChat = (
responseItem.content = responseItem.content + message
if (messageId && !hasSetResponseId) {
questionItem.id = `question-${messageId}`
responseItem.id = messageId
responseItem.parentMessageId = questionItem.id
hasSetResponseId = true
}
@ -241,11 +276,11 @@ export const useChat = (
if (messageId)
responseItem.id = messageId
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
async onCompleted(hasError?: boolean, errorMessage?: string) {
@ -255,15 +290,12 @@ export const useChat = (
if (errorMessage) {
responseItem.content = errorMessage
responseItem.isError = true
const newListWithAnswer = produce(
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
})
handleUpdateChatList(newListWithAnswer)
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
}
return
}
@ -286,15 +318,12 @@ export const useChat = (
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
const newListWithAnswer = produce(
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
})
handleUpdateChatList(newListWithAnswer)
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onMessageReplace: (messageReplace) => {
responseItem.content = messageReplace.answer
@ -309,23 +338,21 @@ export const useChat = (
status: WorkflowRunningStatus.Running,
tracing: [],
}
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onWorkflowFinished: ({ data }) => {
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onIterationStart: ({ data }) => {
responseItem.workflowProcess!.tracing!.push({
@ -333,13 +360,12 @@ export const useChat = (
status: NodeRunningStatus.Running,
details: [],
} as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onIterationNext: ({ data }) => {
const tracing = responseItem.workflowProcess!.tracing!
@ -347,10 +373,12 @@ export const useChat = (
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
iterations.details!.push([])
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.length - 1
draft[currentIndex] = responseItem
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onIterationFinish: ({ data }) => {
const tracing = responseItem.workflowProcess!.tracing!
@ -361,10 +389,12 @@ export const useChat = (
...data,
status: NodeRunningStatus.Succeeded,
} as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.length - 1
draft[currentIndex] = responseItem
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onNodeStarted: ({ data }) => {
if (data.iteration_id)
@ -374,13 +404,12 @@ export const useChat = (
...data,
status: NodeRunningStatus.Running,
} as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onNodeRetry: ({ data }) => {
if (data.iteration_id)
@ -422,23 +451,21 @@ export const useChat = (
: {}),
...data,
} as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
},
)
}, [handleRun, handleResponding, handleUpdateChatList, notify, t, updateCurrentQA, config.suggested_questions_after_answer?.enabled, formSettings])
}, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, notify, t, config?.suggested_questions_after_answer?.enabled])
return {
conversationId: conversationId.current,
chatList,
chatListRef,
handleUpdateChatList,
setTargetMessageId,
handleSend,
handleStop,
handleRestart,

View File

@ -23,7 +23,14 @@ const OutputPanel: FC<OutputPanelProps> = ({
height,
}) => {
const isTextOutput = useMemo(() => {
return outputs && Object.keys(outputs).length === 1 && typeof outputs[Object.keys(outputs)[0]] === 'string'
if (!outputs || typeof outputs !== 'object')
return false
const keys = Object.keys(outputs)
const value = outputs[keys[0]]
return keys.length === 1 && (
typeof value === 'string'
|| (Array.isArray(value) && value.every(item => typeof item === 'string'))
)
}, [outputs])
const fileList = useMemo(() => {
@ -65,7 +72,13 @@ const OutputPanel: FC<OutputPanelProps> = ({
)}
{isTextOutput && (
<div className='px-4 py-2'>
<Markdown content={outputs[Object.keys(outputs)[0]] || ''} />
<Markdown
content={
Array.isArray(outputs[Object.keys(outputs)[0]])
? outputs[Object.keys(outputs)[0]].join('\n')
: (outputs[Object.keys(outputs)[0]] || '')
}
/>
</div>
)}
{fileList.length > 0 && (
@ -78,14 +91,14 @@ const OutputPanel: FC<OutputPanelProps> = ({
/>
</div>
)}
{outputs && Object.keys(outputs).length > 1 && height! > 0 && (
{!isTextOutput && outputs && Object.keys(outputs).length > 0 && height! > 0 && (
<div className='flex flex-col gap-2'>
<CodeEditor
showFileList
readOnly
title={<div></div>}
title={<div tabIndex={0}>Output</div>}
language={CodeLanguage.json}
value={outputs}
value={JSON.stringify(outputs, null, 2)}
isJSONStringifyBeauty
height={height ? (height - 16) / 2 : undefined}
/>

View File

@ -47,7 +47,7 @@
.markdown-body {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
margin: 4px 0 0 0;
color: #101828;
background-color: var(--color-canvas-default);
font-size: 14px;