mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
Merge branch 'main' into feat/knowledge-dark-mode
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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} />}
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 => (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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.
|
||||
|
||||
5
web/app/components/workflow/blocks.tsx
Normal file
5
web/app/components/workflow/blocks.tsx
Normal 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[]
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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]'>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -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: {},
|
||||
|
||||
@ -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' />
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user