- {isChosen &&
{t(`${I18N_PREFIX}.inUse`)}
}
+ {isChosen &&
{t(`${I18N_PREFIX}.inUse`)}
}
{!readOnly && (
{hasConfigured && (
-
-
+
+
{t(`${I18N_PREFIX}.view`)}
)}
-
+
{t(`${I18N_PREFIX}.config`)}
)}
-
-
+
{t(`${I18N_PREFIX}.${type}.description`)}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx
deleted file mode 100644
index 934eb681b9..0000000000
--- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-'use client'
-import { ChevronDoubleDownIcon } from '@heroicons/react/20/solid'
-import type { FC } from 'react'
-import { useTranslation } from 'react-i18next'
-import React, { useCallback } from 'react'
-import Tooltip from '@/app/components/base/tooltip'
-
-const I18N_PREFIX = 'app.tracing'
-
-type Props = {
- isFold: boolean
- onFoldChange: (isFold: boolean) => void
-}
-
-const ToggleFoldBtn: FC
= ({
- isFold,
- onFoldChange,
-}) => {
- const { t } = useTranslation()
-
- const handleFoldChange = useCallback((e: React.MouseEvent) => {
- e.stopPropagation()
- onFoldChange(!isFold)
- }, [isFold, onFoldChange])
- return (
- // text-[0px] to hide spacing between tooltip elements
-
-
- {isFold && (
-
-
-
- )}
- {!isFold && (
-
-
-
- )}
-
-
- )
-}
-export default React.memo(ToggleFoldBtn)
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts
index e07cf37c9d..982d01ffb3 100644
--- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts
+++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts
@@ -1,6 +1,7 @@
export enum TracingProvider {
langSmith = 'langsmith',
langfuse = 'langfuse',
+ opik = 'opik',
}
export type LangSmithConfig = {
@@ -14,3 +15,10 @@ export type LangFuseConfig = {
secret_key: string
host: string
}
+
+export type OpikConfig = {
+ api_key: string
+ project: string
+ workspace: string
+ url: string
+}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx
index 211b0b3677..dda198fc89 100644
--- a/web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx
+++ b/web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx
@@ -15,6 +15,7 @@ const AppDetail: FC = ({ children }) => {
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)
return router.replace('/datasets')
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCurrentWorkspaceDatasetOperator])
return (
diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx
index dabe75ee62..e7e1251897 100644
--- a/web/app/(commonLayout)/apps/AppCard.tsx
+++ b/web/app/(commonLayout)/apps/AppCard.tsx
@@ -71,6 +71,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
})
}
setShowConfirmDelete(false)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [app.id])
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
@@ -100,7 +101,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
onRefresh()
mutateApps()
}
- catch (e) {
+ catch {
notify({ type: 'error', message: t('app.editFailed') })
}
}, [app.id, mutateApps, notify, onRefresh, t])
@@ -127,7 +128,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
onPlanInfoChanged()
getRedirection(isCurrentWorkspaceEditor, newApp, push)
}
- catch (e) {
+ catch {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
@@ -144,7 +145,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
a.download = `${app.name}.yml`
a.click()
}
- catch (e) {
+ catch {
notify({ type: 'error', message: t('app.exportFailed') })
}
}
@@ -163,7 +164,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
}
setSecretEnvList(list)
}
- catch (e) {
+ catch {
notify({ type: 'error', message: t('app.exportFailed') })
}
}
diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx
index 5269571c21..463e9cf515 100644
--- a/web/app/(commonLayout)/apps/Apps.tsx
+++ b/web/app/(commonLayout)/apps/Apps.tsx
@@ -25,16 +25,18 @@ import Input from '@/app/components/base/input'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter'
+import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
activeTab: string,
+ isCreatedByMe: boolean,
tags: string[],
keywords: string,
) => {
if (!pageIndex || previousPageData.has_more) {
- const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords } }
+ const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords, is_created_by_me: isCreatedByMe } }
if (activeTab !== 'all')
params.params.mode = activeTab
@@ -57,7 +59,8 @@ const Apps = () => {
const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'all',
})
- const { query: { tagIDs = [], keywords = '' }, setQuery } = useAppsQueryState()
+ const { query: { tagIDs = [], keywords = '', isCreatedByMe: queryIsCreatedByMe = false }, setQuery } = useAppsQueryState()
+ const [isCreatedByMe, setIsCreatedByMe] = useState(queryIsCreatedByMe)
const [tagFilterValue, setTagFilterValue] = useState(tagIDs)
const [searchKeywords, setSearchKeywords] = useState(keywords)
const setKeywords = useCallback((keywords: string) => {
@@ -68,7 +71,7 @@ const Apps = () => {
}, [setQuery])
const { data, isLoading, setSize, mutate } = useSWRInfinite(
- (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, tagIDs, searchKeywords),
+ (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
fetchAppList,
{ revalidateFirstPage: true },
)
@@ -123,6 +126,12 @@ const Apps = () => {
handleTagsUpdate()
}
+ const handleCreatedByMeChange = useCallback(() => {
+ const newValue = !isCreatedByMe
+ setIsCreatedByMe(newValue)
+ setQuery(prev => ({ ...prev, isCreatedByMe: newValue }))
+ }, [isCreatedByMe, setQuery])
+
return (
<>
@@ -132,6 +141,12 @@ const Apps = () => {
options={options}
/>
)
diff --git a/web/app/(commonLayout)/datasets/Container.tsx b/web/app/(commonLayout)/datasets/Container.tsx
index a0edb1cd61..f484d30a3d 100644
--- a/web/app/(commonLayout)/datasets/Container.tsx
+++ b/web/app/(commonLayout)/datasets/Container.tsx
@@ -4,7 +4,8 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
-import { useDebounceFn } from 'ahooks'
+import { useBoolean, useDebounceFn } from 'ahooks'
+import { useQuery } from '@tanstack/react-query'
// Components
import ExternalAPIPanel from '../../components/datasets/external-api/external-api-panel'
@@ -16,7 +17,9 @@ import TabSliderNew from '@/app/components/base/tab-slider-new'
import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter'
import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
+import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
// Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
@@ -26,16 +29,14 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { useAppContext } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
-// eslint-disable-next-line import/order
-import { useQuery } from '@tanstack/react-query'
-import Input from '@/app/components/base/input'
const Container = () => {
const { t } = useTranslation()
const router = useRouter()
- const { currentWorkspace } = useAppContext()
+ const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
+ const [includeAll, { toggle: toggleIncludeAll }] = useBoolean(false)
const options = useMemo(() => {
return [
@@ -81,7 +82,7 @@ const Container = () => {
}, [currentWorkspace, router])
return (
-
+
{
/>
{activeTab === 'dataset' && (
+ {isCurrentWorkspaceOwner && }
{
{activeTab === 'dataset' && (
<>
-
+
{showTagManagementModal && (
diff --git a/web/app/(commonLayout)/datasets/Datasets.tsx b/web/app/(commonLayout)/datasets/Datasets.tsx
index db6cb4a518..ea918a2b17 100644
--- a/web/app/(commonLayout)/datasets/Datasets.tsx
+++ b/web/app/(commonLayout)/datasets/Datasets.tsx
@@ -6,7 +6,7 @@ import { debounce } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import NewDatasetCard from './NewDatasetCard'
import DatasetCard from './DatasetCard'
-import type { DataSetListResponse } from '@/models/datasets'
+import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets'
import { fetchDatasets } from '@/service/datasets'
import { useAppContext } from '@/context/app-context'
@@ -15,13 +15,15 @@ const getKey = (
previousPageData: DataSetListResponse,
tags: string[],
keyword: string,
+ includeAll: boolean,
) => {
if (!pageIndex || previousPageData.has_more) {
- const params: any = {
+ const params: FetchDatasetsParams = {
url: 'datasets',
params: {
page: pageIndex + 1,
limit: 30,
+ include_all: includeAll,
},
}
if (tags.length)
@@ -37,16 +39,18 @@ type Props = {
containerRef: React.RefObject
tags: string[]
keywords: string
+ includeAll: boolean
}
const Datasets = ({
containerRef,
tags,
keywords,
+ includeAll,
}: Props) => {
const { isCurrentWorkspaceEditor } = useAppContext()
const { data, isLoading, setSize, mutate } = useSWRInfinite(
- (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords),
+ (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll),
fetchDatasets,
{ revalidateFirstPage: false, revalidateAll: true },
)
diff --git a/web/app/(commonLayout)/datasets/Doc.tsx b/web/app/(commonLayout)/datasets/Doc.tsx
index 553dca5008..f56a5bc909 100644
--- a/web/app/(commonLayout)/datasets/Doc.tsx
+++ b/web/app/(commonLayout)/datasets/Doc.tsx
@@ -1,7 +1,9 @@
'use client'
-import { type FC, useEffect } from 'react'
+import { useEffect, useState } from 'react'
import { useContext } from 'use-context-selector'
+import { useTranslation } from 'react-i18next'
+import { RiListUnordered } from '@remixicon/react'
import TemplateEn from './template/template.en.mdx'
import TemplateZh from './template/template.zh.mdx'
import I18n from '@/context/i18n'
@@ -10,25 +12,106 @@ import { LanguagesSupported } from '@/i18n/language'
type DocProps = {
apiBaseUrl: string
}
-const Doc: FC = ({
- apiBaseUrl,
-}) => {
- const { locale } = useContext(I18n)
+const Doc = ({ apiBaseUrl }: DocProps) => {
+ const { locale } = useContext(I18n)
+ const { t } = useTranslation()
+ const [toc, setToc] = useState>([])
+ const [isTocExpanded, setIsTocExpanded] = useState(false)
+
+ // Set initial TOC expanded state based on screen width
useEffect(() => {
- const hash = location.hash
- if (hash)
- document.querySelector(hash)?.scrollIntoView()
+ const mediaQuery = window.matchMedia('(min-width: 1280px)')
+ setIsTocExpanded(mediaQuery.matches)
}, [])
+ // Extract TOC from article content
+ useEffect(() => {
+ const extractTOC = () => {
+ const article = document.querySelector('article')
+ if (article) {
+ const headings = article.querySelectorAll('h2')
+ const tocItems = Array.from(headings).map((heading) => {
+ const anchor = heading.querySelector('a')
+ if (anchor) {
+ return {
+ href: anchor.getAttribute('href') || '',
+ text: anchor.textContent || '',
+ }
+ }
+ return null
+ }).filter((item): item is { href: string; text: string } => item !== null)
+ setToc(tocItems)
+ }
+ }
+
+ setTimeout(extractTOC, 0)
+ }, [locale])
+
+ // Handle TOC item click
+ const handleTocClick = (e: React.MouseEvent, item: { href: string; text: string }) => {
+ e.preventDefault()
+ const targetId = item.href.replace('#', '')
+ const element = document.getElementById(targetId)
+ if (element) {
+ const scrollContainer = document.querySelector('.scroll-container')
+ if (scrollContainer) {
+ const headerOffset = -40
+ const elementTop = element.offsetTop - headerOffset
+ scrollContainer.scrollTo({
+ top: elementTop,
+ behavior: 'smooth',
+ })
+ }
+ }
+ }
+
return (
-
- {
- locale !== LanguagesSupported[1]
+
+
+ {isTocExpanded
+ ? (
+
+ )
+ : (
+
+ )}
+
+
+ {locale !== LanguagesSupported[1]
?
:
- }
-
+ }
+
+
)
}
diff --git a/web/app/(commonLayout)/datasets/template/template.en.mdx b/web/app/(commonLayout)/datasets/template/template.en.mdx
index d3dcfc4b24..ac57e3aef2 100644
--- a/web/app/(commonLayout)/datasets/template/template.en.mdx
+++ b/web/app/(commonLayout)/datasets/template/template.en.mdx
@@ -1,5 +1,5 @@
import { CodeGroup } from '@/app/components/develop/code.tsx'
-import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '@/app/components/develop/md.tsx'
+import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstruction, Paragraph } from '@/app/components/develop/md.tsx'
# Knowledge API
@@ -47,11 +47,58 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
Document content
+
+ Type of document (optional):
+ - book Book
+ - web_page Web page
+ - paper Academic paper/article
+ - social_media_post Social media post
+ - wikipedia_entry Wikipedia entry
+ - personal_document Personal document
+ - business_document Business document
+ - im_chat_log Chat log
+ - synced_from_notion Notion document
+ - synced_from_github GitHub document
+ - others Other document types
+
+
+ Document metadata (required if doc_type is provided). Fields vary by doc_type:
+ For book:
+ - title Book title
+ - language Book language
+ - author Book author
+ - publisher Publisher name
+ - publication_date Publication date
+ - isbn ISBN number
+ - category Book category
+
+ For web_page:
+ - title Page title
+ - url Page URL
+ - language Page language
+ - publish_date Publish date
+ - author/publisher Author or publisher
+ - topic/keywords Topic or keywords
+ - description 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
+
Index mode
- high_quality High quality: embedding using embedding model, built as vector database index
- economy Economy: Build using inverted index of keyword table index
+
+ Format of indexed content
+ - text_model Text documents are directly embedded; `economy` mode defaults to using this form
+ - hierarchical_model Parent-child mode
+ - qa_model Q&A Mode: Generates Q&A pairs for segmented documents and then embeds the questions
+
+
+ In Q&A mode, specify the language of the document, for example: English, Chinese
+
Processing rules
- mode (string) Cleaning, segmentation mode, automatic / custom
@@ -65,6 +112,32 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- segmentation (object) Segmentation rules
- separator Custom segment identifier, currently only allows one delimiter to be set. Default is \n
- max_tokens Maximum length (token) defaults to 1000
+ - parent_mode Retrieval mode of parent chunks: full-doc full text retrieval / paragraph paragraph retrieval
+ - subchunk_segmentation (object) Child chunk rules
+ - separator Segmentation identifier. Currently, only one delimiter is allowed. The default is ***
+ - max_tokens The maximum length (tokens) must be validated to be shorter than the length of the parent chunk
+ - chunk_overlap Define the overlap between adjacent chunks (optional)
+
+ When no parameters are set for the knowledge base, the first upload requires the following parameters to be provided; if not provided, the default parameters will be used.
+
+ Retrieval model
+ - search_method (string) Search method
+ - hybrid_search Hybrid search
+ - semantic_search Semantic search
+ - full_text_search Full-text search
+ - reranking_enable (bool) Whether to enable reranking
+ - reranking_mode (object) Rerank model configuration
+ - reranking_provider_name (string) Rerank model provider
+ - reranking_model_name (string) Rerank model name
+ - top_k (int) Number of results to return
+ - score_threshold_enabled (bool) Whether to enable score threshold
+ - score_threshold (float) Score threshold
+
+
+ Embedding model name
+
+
+ Embedding model provider
@@ -155,6 +228,75 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- high_quality High quality: embedding using embedding model, built as vector database index
- economy Economy: Build using inverted index of keyword table index
+ - doc_form Format of indexed content
+ - text_model Text documents are directly embedded; `economy` mode defaults to using this form
+ - hierarchical_model Parent-child mode
+ - qa_model Q&A Mode: Generates Q&A pairs for segmented documents and then embeds the questions
+
+ - doc_type Type of document (optional)
+ - book Book
+ Document records a book or publication
+ - web_page Web page
+ Document records web page content
+ - paper Academic paper/article
+ Document records academic paper or research article
+ - social_media_post Social media post
+ Content from social media posts
+ - wikipedia_entry Wikipedia entry
+ Content from Wikipedia entries
+ - personal_document Personal document
+ Documents related to personal content
+ - business_document Business document
+ Documents related to business content
+ - im_chat_log Chat log
+ Records of instant messaging chats
+ - synced_from_notion Notion document
+ Documents synchronized from Notion
+ - synced_from_github GitHub document
+ Documents synchronized from GitHub
+ - others Other document types
+ Other document types not listed above
+
+ - doc_metadata Document metadata (required if doc_type is provided)
+ Fields vary by doc_type:
+
+ For book:
+ - title Book title
+ Title of the book
+ - language Book language
+ Language of the book
+ - author Book author
+ Author of the book
+ - publisher Publisher name
+ Name of the publishing house
+ - publication_date Publication date
+ Date when the book was published
+ - isbn ISBN number
+ International Standard Book Number
+ - category Book category
+ Category or genre of the book
+
+ For web_page:
+ - title Page title
+ Title of the web page
+ - url Page URL
+ URL address of the web page
+ - language Page language
+ Language of the web page
+ - publish_date Publish date
+ Date when the web page was published
+ - author/publisher Author or publisher
+ Author or publisher of the web page
+ - topic/keywords Topic or keywords
+ Topics or keywords of the web page
+ - description 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
+
+ - doc_language In Q&A mode, specify the language of the document, for example: English, Chinese
+
- process_rule Processing rules
- mode (string) Cleaning, segmentation mode, automatic / custom
- rules (object) Custom rules (in automatic mode, this field is empty)
@@ -167,10 +309,36 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- segmentation (object) Segmentation rules
- separator Custom segment identifier, currently only allows one delimiter to be set. Default is \n
- max_tokens Maximum length (token) defaults to 1000
+ - parent_mode Retrieval mode of parent chunks: full-doc full text retrieval / paragraph paragraph retrieval
+ - subchunk_segmentation (object) Child chunk rules
+ - separator Segmentation identifier. Currently, only one delimiter is allowed. The default is ***
+ - max_tokens The maximum length (tokens) must be validated to be shorter than the length of the parent chunk
+ - chunk_overlap Define the overlap between adjacent chunks (optional)
Files that need to be uploaded.
+ When no parameters are set for the knowledge base, the first upload requires the following parameters to be provided; if not provided, the default parameters will be used.
+
+ Retrieval model
+ - search_method (string) Search method
+ - hybrid_search Hybrid search
+ - semantic_search Semantic search
+ - full_text_search Full-text search
+ - reranking_enable (bool) Whether to enable reranking
+ - reranking_mode (object) Rerank model configuration
+ - reranking_provider_name (string) Rerank model provider
+ - reranking_model_name (string) Rerank model name
+ - top_k (int) Number of results to return
+ - score_threshold_enabled (bool) Whether to enable score threshold
+ - score_threshold (float) Score threshold
+
+
+ Embedding model name
+
+
+ Embedding model provider
+
@@ -239,6 +407,44 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
Knowledge description (optional)
+
+ Type of document (optional):
+ - book Book
+ - web_page Web page
+ - paper Academic paper/article
+ - social_media_post Social media post
+ - wikipedia_entry Wikipedia entry
+ - personal_document Personal document
+ - business_document Business document
+ - im_chat_log Chat log
+ - synced_from_notion Notion document
+ - synced_from_github GitHub document
+ - others Other document types
+
+
+ Document metadata (required if doc_type is provided). Fields vary by doc_type:
+ For book:
+ - title Book title
+ - language Book language
+ - author Book author
+ - publisher Publisher name
+ - publication_date Publication date
+ - isbn ISBN number
+ - category Book category
+
+ For web_page:
+ - title Page title
+ - url Page URL
+ - language Page language
+ - publish_date Publish date
+ - author/publisher Author or publisher
+ - topic/keywords Topic or keywords
+ - description 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
+
Index technique (optional)
- high_quality High quality
@@ -449,6 +655,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- segmentation (object) Segmentation rules
- separator Custom segment identifier, currently only allows one delimiter to be set. Default is \n
- max_tokens Maximum length (token) defaults to 1000
+ - parent_mode Retrieval mode of parent chunks: full-doc full text retrieval / paragraph paragraph retrieval
+ - subchunk_segmentation (object) Child chunk rules
+ - separator Segmentation identifier. Currently, only one delimiter is allowed. The default is ***
+ - max_tokens The maximum length (tokens) must be validated to be shorter than the length of the parent chunk
+ - chunk_overlap Define the overlap between adjacent chunks (optional)
@@ -546,6 +757,72 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- segmentation (object) Segmentation rules
- separator Custom segment identifier, currently only allows one delimiter to be set. Default is \n
- max_tokens Maximum length (token) defaults to 1000
+ - parent_mode Retrieval mode of parent chunks: full-doc full text retrieval / paragraph paragraph retrieval
+ - subchunk_segmentation (object) Child chunk rules
+ - separator Segmentation identifier. Currently, only one delimiter is allowed. The default is ***
+ - max_tokens The maximum length (tokens) must be validated to be shorter than the length of the parent chunk
+ - chunk_overlap Define the overlap between adjacent chunks (optional)
+ - doc_type Type of document (optional)
+ - book Book
+ Document records a book or publication
+ - web_page Web page
+ Document records web page content
+ - paper Academic paper/article
+ Document records academic paper or research article
+ - social_media_post Social media post
+ Content from social media posts
+ - wikipedia_entry Wikipedia entry
+ Content from Wikipedia entries
+ - personal_document Personal document
+ Documents related to personal content
+ - business_document Business document
+ Documents related to business content
+ - im_chat_log Chat log
+ Records of instant messaging chats
+ - synced_from_notion Notion document
+ Documents synchronized from Notion
+ - synced_from_github GitHub document
+ Documents synchronized from GitHub
+ - others Other document types
+ Other document types not listed above
+
+ - doc_metadata Document metadata (required if doc_type is provided)
+ Fields vary by doc_type:
+
+ For book:
+ - title Book title
+ Title of the book
+ - language Book language
+ Language of the book
+ - author Book author
+ Author of the book
+ - publisher Publisher name
+ Name of the publishing house
+ - publication_date Publication date
+ Date when the book was published
+ - isbn ISBN number
+ International Standard Book Number
+ - category Book category
+ Category or genre of the book
+
+ For web_page:
+ - title Page title
+ Title of the web page
+ - url Page URL
+ URL address of the web page
+ - language Page language
+ Language of the web page
+ - publish_date Publish date
+ Date when the web page was published
+ - author/publisher Author or publisher
+ Author or publisher of the web page
+ - topic/keywords Topic or keywords
+ Topics or keywords of the web page
+ - description 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
@@ -984,7 +1261,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
@@ -1009,6 +1286,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- answer (text) Answer content, passed if the knowledge is in Q&A mode (optional)
- keywords (list) Keyword (optional)
- enabled (bool) False / true (optional)
+ - regenerate_child_chunks (bool) Whether to regenerate child chunks (optional)
@@ -1069,6 +1347,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
+
+
+
+ ### Path
+
+
+ Knowledge ID
+
+
+ Document ID
+
+
+
+
+
+ ```bash {{ title: 'cURL' }}
+ curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \
+ --header 'Authorization: Bearer {api_key}' \
+ --header 'Content-Type: application/json'
+ ```
+
+
+ ```json {{ title: 'Response' }}
+ {
+ "id": "file_id",
+ "name": "file_name",
+ "size": 1024,
+ "extension": "txt",
+ "url": "preview_url",
+ "download_url": "download_url",
+ "mime_type": "text/plain",
+ "created_by": "user_id",
+ "created_at": 1728734540,
+ }
+ ```
+
+
+
+
+
+
reranking_mode (object) Rerank model configuration, required if reranking is enabled
- reranking_provider_name (string) Rerank model provider
- reranking_model_name (string) Rerank model name
- - weights (double) Semantic search weight setting in hybrid search mode
+ - weights (float) Semantic search weight setting in hybrid search mode
- top_k (integer) Number of results to return (optional)
- score_threshold_enabled (bool) Whether to enable score threshold
- - score_threshold (double) Score threshold
+ - score_threshold (float) Score threshold
Unused field
diff --git a/web/app/(commonLayout)/datasets/template/template.zh.mdx b/web/app/(commonLayout)/datasets/template/template.zh.mdx
index db15ede9fc..0e5857c446 100644
--- a/web/app/(commonLayout)/datasets/template/template.zh.mdx
+++ b/web/app/(commonLayout)/datasets/template/template.zh.mdx
@@ -1,5 +1,5 @@
import { CodeGroup } from '@/app/components/develop/code.tsx'
-import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '@/app/components/develop/md.tsx'
+import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstruction, Paragraph } from '@/app/components/develop/md.tsx'
# 知识库 API
@@ -47,11 +47,60 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
文档内容
+
+ 文档类型(选填)
+ - book 图书 Book
+ - web_page 网页 Web page
+ - paper 学术论文/文章 Academic paper/article
+ - social_media_post 社交媒体帖子 Social media post
+ - wikipedia_entry 维基百科条目 Wikipedia entry
+ - personal_document 个人文档 Personal document
+ - business_document 商业文档 Business document
+ - im_chat_log 即时通讯记录 Chat log
+ - synced_from_notion Notion同步文档 Notion document
+ - synced_from_github GitHub同步文档 GitHub document
+ - others 其他文档类型 Other document types
+
+
+
+ 文档元数据(如提供文档类型则必填)。字段因文档类型而异:
+
+ 针对图书 For book:
+ - title 书名 Book title
+ - language 图书语言 Book language
+ - author 作者 Book author
+ - publisher 出版社 Publisher name
+ - publication_date 出版日期 Publication date
+ - isbn ISBN号码 ISBN number
+ - category 图书分类 Book category
+
+ 针对网页 For web_page:
+ - title 页面标题 Page title
+ - url 页面网址 Page URL
+ - language 页面语言 Page language
+ - publish_date 发布日期 Publish date
+ - author/publisher 作者/发布者 Author or publisher
+ - topic/keywords 主题/关键词 Topic or keywords
+ - description 页面描述 Page description
+
+ 请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
+
+ 针对"其他"类型文档,接受任何有效的JSON对象
+
索引方式
- high_quality 高质量:使用 embedding 模型进行嵌入,构建为向量数据库索引
- economy 经济:使用 keyword table index 的倒排索引进行构建
+
+ 索引内容的形式
+ - text_model text 文档直接 embedding,经济模式默认为该模式
+ - hierarchical_model parent-child 模式
+ - qa_model Q&A 模式:为分片文档生成 Q&A 对,然后对问题进行 embedding
+
+
+ 在 Q&A 模式下,指定文档的语言,例如:English、Chinese
+
处理规则
- mode (string) 清洗、分段模式 ,automatic 自动 / custom 自定义
@@ -63,8 +112,34 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- remove_urls_emails 删除 URL、电子邮件地址
- enabled (bool) 是否选中该规则,不传入文档 ID 时代表默认值
- segmentation (object) 分段规则
- - separator 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
+ - separator 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
- max_tokens 最大长度(token)默认为 1000
+ - parent_mode 父分段的召回模式 full-doc 全文召回 / paragraph 段落召回
+ - subchunk_segmentation (object) 子分段规则
+ - separator 分段标识符,目前仅允许设置一个分隔符。默认为 ***
+ - max_tokens 最大长度 (token) 需要校验小于父级的长度
+ - chunk_overlap 分段重叠指的是在对数据进行分段时,段与段之间存在一定的重叠部分(选填)
+
+ 当知识库未设置任何参数的时候,首次上传需要提供以下参数,未提供则使用默认选项:
+
+ 检索模式
+ - search_method (string) 检索方法
+ - hybrid_search 混合检索
+ - semantic_search 语义检索
+ - full_text_search 全文检索
+ - reranking_enable (bool) 是否开启rerank
+ - reranking_model (object) Rerank 模型配置
+ - reranking_provider_name (string) Rerank 模型的提供商
+ - reranking_model_name (string) Rerank 模型的名称
+ - top_k (int) 召回条数
+ - score_threshold_enabled (bool)是否开启召回分数限制
+ - score_threshold (float) 召回分数限制
+
+
+ Embedding 模型名称
+
+
+ Embedding 模型供应商
@@ -155,6 +230,75 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- high_quality 高质量:使用 embedding 模型进行嵌入,构建为向量数据库索引
- economy 经济:使用 keyword table index 的倒排索引进行构建
+ - doc_form 索引内容的形式
+ - text_model text 文档直接 embedding,经济模式默认为该模式
+ - hierarchical_model parent-child 模式
+ - qa_model Q&A 模式:为分片文档生成 Q&A 对,然后对问题进行 embedding
+ - doc_type 文档类型(选填)Type of document (optional)
+ - book 图书
+ 文档记录一本书籍或出版物
+ - web_page 网页
+ 网页内容的文档记录
+ - paper 学术论文/文章
+ 学术论文或研究文章的记录
+ - social_media_post 社交媒体帖子
+ 社交媒体上的帖子内容
+ - wikipedia_entry 维基百科条目
+ 维基百科的词条内容
+ - personal_document 个人文档
+ 个人相关的文档记录
+ - business_document 商业文档
+ 商业相关的文档记录
+ - im_chat_log 即时通讯记录
+ 即时通讯的聊天记录
+ - synced_from_notion Notion同步文档
+ 从Notion同步的文档内容
+ - synced_from_github GitHub同步文档
+ 从GitHub同步的文档内容
+ - others 其他文档类型
+ 其他未列出的文档类型
+
+ - doc_metadata 文档元数据(如提供文档类型则必填
+ 字段因文档类型而异
+
+ 针对图书类型 For book:
+ - title 书名
+ 书籍的标题
+ - language 图书语言
+ 书籍的语言
+ - author 作者
+ 书籍的作者
+ - publisher 出版社
+ 出版社的名称
+ - publication_date 出版日期
+ 书籍的出版日期
+ - isbn ISBN号码
+ 书籍的ISBN编号
+ - category 图书分类
+ 书籍的分类类别
+
+ 针对网页类型 For web_page:
+ - title 页面标题
+ 网页的标题
+ - url 页面网址
+ 网页的URL地址
+ - language 页面语言
+ 网页的语言
+ - publish_date 发布日期
+ 网页的发布日期
+ - author/publisher 作者/发布者
+ 网页的作者或发布者
+ - topic/keywords 主题/关键词
+ 网页的主题或关键词
+ - description 页面描述
+ 网页的描述信息
+
+ 请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
+
+ 针对"其他"类型文档,接受任何有效的JSON对象
+
+ - doc_language 在 Q&A 模式下,指定文档的语言,例如:English、Chinese
+
- process_rule 处理规则
- mode (string) 清洗、分段模式 ,automatic 自动 / custom 自定义
- rules (object) 自定义规则(自动模式下,该字段为空)
@@ -167,10 +311,36 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- segmentation (object) 分段规则
- separator 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
- max_tokens 最大长度(token)默认为 1000
+ - parent_mode 父分段的召回模式 full-doc 全文召回 / paragraph 段落召回
+ - subchunk_segmentation (object) 子分段规则
+ - separator 分段标识符,目前仅允许设置一个分隔符。默认为 ***
+ - max_tokens 最大长度 (token) 需要校验小于父级的长度
+ - chunk_overlap 分段重叠指的是在对数据进行分段时,段与段之间存在一定的重叠部分(选填)
需要上传的文件。
+ 当知识库未设置任何参数的时候,首次上传需要提供以下参数,未提供则使用默认选项:
+
+ 检索模式
+ - search_method (string) 检索方法
+ - hybrid_search 混合检索
+ - semantic_search 语义检索
+ - full_text_search 全文检索
+ - reranking_enable (bool) 是否开启rerank
+ - reranking_model (object) Rerank 模型配置
+ - reranking_provider_name (string) Rerank 模型的提供商
+ - reranking_model_name (string) Rerank 模型的名称
+ - top_k (int) 召回条数
+ - score_threshold_enabled (bool)是否开启召回分数限制
+ - score_threshold (float) 召回分数限制
+
+
+ Embedding 模型名称
+
+
+ Embedding 模型供应商
+
@@ -411,7 +581,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
@@ -436,6 +606,46 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
文档内容(选填)
+
+ 文档类型(选填)
+ - book 图书 Book
+ - web_page 网页 Web page
+ - paper 学术论文/文章 Academic paper/article
+ - social_media_post 社交媒体帖子 Social media post
+ - wikipedia_entry 维基百科条目 Wikipedia entry
+ - personal_document 个人文档 Personal document
+ - business_document 商业文档 Business document
+ - im_chat_log 即时通讯记录 Chat log
+ - synced_from_notion Notion同步文档 Notion document
+ - synced_from_github GitHub同步文档 GitHub document
+ - others 其他文档类型 Other document types
+
+
+
+ 文档元数据(如提供文档类型则必填)。字段因文档类型而异:
+
+ 针对图书 For book:
+ - title 书名 Book title
+ - language 图书语言 Book language
+ - author 作者 Book author
+ - publisher 出版社 Publisher name
+ - publication_date 出版日期 Publication date
+ - isbn ISBN号码 ISBN number
+ - category 图书分类 Book category
+
+ 针对网页 For web_page:
+ - title 页面标题 Page title
+ - url 页面网址 Page URL
+ - language 页面语言 Page language
+ - publish_date 发布日期 Publish date
+ - author/publisher 作者/发布者 Author or publisher
+ - topic/keywords 主题/关键词 Topic or keywords
+ - description 页面描述 Page description
+
+ 请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
+
+ 针对"其他"类型文档,接受任何有效的JSON对象
+
处理规则(选填)
- mode (string) 清洗、分段模式 ,automatic 自动 / custom 自定义
@@ -449,6 +659,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- segmentation (object) 分段规则
- separator 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
- max_tokens 最大长度(token)默认为 1000
+ - parent_mode 父分段的召回模式 full-doc 全文召回 / paragraph 段落召回
+ - subchunk_segmentation (object) 子分段规则
+ - separator 分段标识符,目前仅允许设置一个分隔符。默认为 ***
+ - max_tokens 最大长度 (token) 需要校验小于父级的长度
+ - chunk_overlap 分段重叠指的是在对数据进行分段时,段与段之间存在一定的重叠部分(选填)
@@ -508,7 +723,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
@@ -546,6 +761,73 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- segmentation (object) 分段规则
- separator 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
- max_tokens 最大长度(token)默认为 1000
+ - parent_mode 父分段的召回模式 full-doc 全文召回 / paragraph 段落召回
+ - subchunk_segmentation (object) 子分段规则
+ - separator 分段标识符,目前仅允许设置一个分隔符。默认为 ***
+ - max_tokens 最大长度 (token) 需要校验小于父级的长度
+ - chunk_overlap 分段重叠指的是在对数据进行分段时,段与段之间存在一定的重叠部分(选填)
+ - doc_type 文档类型(选填)Type of document (optional)
+ - book 图书
+ 文档记录一本书籍或出版物
+ - web_page 网页
+ 网页内容的文档记录
+ - paper 学术论文/文章
+ 学术论文或研究文章的记录
+ - social_media_post 社交媒体帖子
+ 社交媒体上的帖子内容
+ - wikipedia_entry 维基百科条目
+ 维基百科的词条内容
+ - personal_document 个人文档
+ 个人相关的文档记录
+ - business_document 商业文档
+ 商业相关的文档记录
+ - im_chat_log 即时通讯记录
+ 即时通讯的聊天记录
+ - synced_from_notion Notion同步文档
+ 从Notion同步的文档内容
+ - synced_from_github GitHub同步文档
+ 从GitHub同步的文档内容
+ - others 其他文档类型
+ 其他未列出的文档类型
+
+ - doc_metadata 文档元数据(如提供文档类型则必填
+ 字段因文档类型而异
+
+ 针对图书类型 For book:
+ - title 书名
+ 书籍的标题
+ - language 图书语言
+ 书籍的语言
+ - author 作者
+ 书籍的作者
+ - publisher 出版社
+ 出版社的名称
+ - publication_date 出版日期
+ 书籍的出版日期
+ - isbn ISBN号码
+ 书籍的ISBN编号
+ - category 图书分类
+ 书籍的分类类别
+
+ 针对网页类型 For web_page:
+ - title 页面标题
+ 网页的标题
+ - url 页面网址
+ 网页的URL地址
+ - language 页面语言
+ 网页的语言
+ - publish_date 发布日期
+ 网页的发布日期
+ - author/publisher 作者/发布者
+ 网页的作者或发布者
+ - topic/keywords 主题/关键词
+ 网页的主题或关键词
+ - description 页面描述
+ 网页的描述信息
+
+ 请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
+
+ 针对"其他"类型文档,接受任何有效的JSON对象
@@ -1009,6 +1291,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- answer (text) 答案内容,非必填,如果知识库的模式为 Q&A 模式则传值
- keywords (list) 关键字,非必填
- enabled (bool) false/true,非必填
+ - regenerate_child_chunks (bool) 是否重新生成子分段,非必填
@@ -1070,6 +1353,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
+
+
+
+ ### Path
+
+
+ 知识库 ID
+
+
+ 文档 ID
+
+
+
+
+
+ ```bash {{ title: 'cURL' }}
+ curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \
+ --header 'Authorization: Bearer {api_key}' \
+ --header 'Content-Type: application/json'
+ ```
+
+
+ ```json {{ title: 'Response' }}
+ {
+ "id": "file_id",
+ "name": "file_name",
+ "size": 1024,
+ "extension": "txt",
+ "url": "preview_url",
+ "download_url": "download_url",
+ "mime_type": "text/plain",
+ "created_by": "user_id",
+ "created_at": 1728734540,
+ }
+ ```
+
+
+
+
+
+
full_text_search 全文检索
- hybrid_search 混合检索
- reranking_enable (bool) 是否启用 Reranking,非必填,如果检索模式为 semantic_search 模式或者 hybrid_search 则传值
- - reranking_mode (object) Rerank模型配置,非必填,如果启用了 reranking 则传值
+ - reranking_mode (object) Rerank 模型配置,非必填,如果启用了 reranking 则传值
- reranking_provider_name (string) Rerank 模型提供商
- reranking_model_name (string) Rerank 模型名称
- - weights (double) 混合检索模式下语意检索的权重设置
+ - weights (float) 混合检索模式下语意检索的权重设置
- top_k (integer) 返回结果数量,非必填
- score_threshold_enabled (bool) 是否开启 score 阈值
- - score_threshold (double) Score 阈值
+ - score_threshold (float) Score 阈值
未启用字段
diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx
index f0f7e0321d..af36d4d961 100644
--- a/web/app/(commonLayout)/layout.tsx
+++ b/web/app/(commonLayout)/layout.tsx
@@ -8,27 +8,24 @@ import Header from '@/app/components/header'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ProviderContextProvider } from '@/context/provider-context'
import { ModalContextProvider } from '@/context/modal-context'
-import { TanstackQueryIniter } from '@/context/query-client'
const Layout = ({ children }: { children: ReactNode }) => {
return (
<>
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
+
+
+
+
+
+
+
+ {children}
+
+
+
+
>
)
diff --git a/web/app/(commonLayout)/plugins/page.tsx b/web/app/(commonLayout)/plugins/page.tsx
new file mode 100644
index 0000000000..a3066311b2
--- /dev/null
+++ b/web/app/(commonLayout)/plugins/page.tsx
@@ -0,0 +1,20 @@
+import PluginPage from '@/app/components/plugins/plugin-page'
+import PluginsPanel from '@/app/components/plugins/plugin-page/plugins-panel'
+import Marketplace from '@/app/components/plugins/marketplace'
+import { getLocaleOnServer } from '@/i18n/server'
+
+const PluginList = async () => {
+ const locale = await getLocaleOnServer()
+ return (
+ }
+ marketplace={}
+ />
+ )
+}
+
+export const metadata = {
+ title: 'Plugins - Dify',
+}
+
+export default PluginList
diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx
index 259af2bc2d..94ac1deb0b 100644
--- a/web/app/(shareLayout)/layout.tsx
+++ b/web/app/(shareLayout)/layout.tsx
@@ -1,7 +1,6 @@
import React from 'react'
import type { FC } from 'react'
import type { Metadata } from 'next'
-import GA, { GaType } from '@/app/components/base/ga'
export const metadata: Metadata = {
icons: 'data:,', // prevent browser from using default favicon
@@ -12,7 +11,6 @@ const Layout: FC<{
}> = ({ children }) => {
return (
-
{children}
)
diff --git a/web/app/account/account-page/AvatarWithEdit.tsx b/web/app/account/account-page/AvatarWithEdit.tsx
new file mode 100644
index 0000000000..97f6ba8da6
--- /dev/null
+++ b/web/app/account/account-page/AvatarWithEdit.tsx
@@ -0,0 +1,122 @@
+'use client'
+
+import type { Area } from 'react-easy-crop'
+import React, { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import { RiPencilLine } from '@remixicon/react'
+import { updateUserProfile } from '@/service/common'
+import { ToastContext } from '@/app/components/base/toast'
+import ImageInput, { type OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
+import Modal from '@/app/components/base/modal'
+import Divider from '@/app/components/base/divider'
+import Button from '@/app/components/base/button'
+import Avatar, { type AvatarProps } from '@/app/components/base/avatar'
+import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
+import type { ImageFile } from '@/types/app'
+import getCroppedImg from '@/app/components/base/app-icon-picker/utils'
+import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
+
+type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
+type AvatarWithEditProps = AvatarProps & { onSave?: () => void }
+
+const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
+ const { t } = useTranslation()
+ const { notify } = useContext(ToastContext)
+
+ const [inputImageInfo, setInputImageInfo] = useState()
+ const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false)
+ const [uploading, setUploading] = useState(false)
+
+ const handleImageInput: OnImageInput = useCallback(async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
+ setInputImageInfo(
+ isCropped
+ ? { tempUrl: fileOrTempUrl as string, croppedAreaPixels: croppedAreaPixels!, fileName: fileName! }
+ : { file: fileOrTempUrl as File },
+ )
+ }, [setInputImageInfo])
+
+ const handleSaveAvatar = useCallback(async (uploadedFileId: string) => {
+ try {
+ await updateUserProfile({ url: 'account/avatar', body: { avatar: uploadedFileId } })
+ notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
+ setIsShowAvatarPicker(false)
+ onSave?.()
+ }
+ catch (e) {
+ notify({ type: 'error', message: (e as Error).message })
+ }
+ }, [notify, onSave, t])
+
+ const { handleLocalFileUpload } = useLocalFileUploader({
+ limit: 3,
+ disabled: false,
+ onUpload: (imageFile: ImageFile) => {
+ if (imageFile.progress === 100) {
+ setUploading(false)
+ setInputImageInfo(undefined)
+ handleSaveAvatar(imageFile.fileId)
+ }
+
+ // Error
+ if (imageFile.progress === -1)
+ setUploading(false)
+ },
+ })
+
+ const handleSelect = useCallback(async () => {
+ if (!inputImageInfo)
+ return
+ setUploading(true)
+ if ('file' in inputImageInfo) {
+ handleLocalFileUpload(inputImageInfo.file)
+ return
+ }
+ const blob = await getCroppedImg(inputImageInfo.tempUrl, inputImageInfo.croppedAreaPixels, inputImageInfo.fileName)
+ const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
+ handleLocalFileUpload(file)
+ }, [handleLocalFileUpload, inputImageInfo])
+
+ if (DISABLE_UPLOAD_IMAGE_AS_ICON)
+ return
+
+ return (
+ <>
+
+
+
+
{ setIsShowAvatarPicker(true) }}
+ className="absolute inset-0 bg-black bg-opacity-50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer flex items-center justify-center"
+ >
+
+
+
+
+
+
+
+ setIsShowAvatarPicker(false)}
+ >
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default AvatarWithEdit
diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx
index c7af05793f..16d826a7c2 100644
--- a/web/app/account/account-page/index.tsx
+++ b/web/app/account/account-page/index.tsx
@@ -3,17 +3,17 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
+import DeleteAccount from '../delete-account'
import s from './index.module.css'
+import AvatarWithEdit from './AvatarWithEdit'
import Collapse from '@/app/components/header/account-setting/collapse'
import type { IItem } from '@/app/components/header/account-setting/collapse'
import Modal from '@/app/components/base/modal'
-import Confirm from '@/app/components/base/confirm'
import Button from '@/app/components/base/button'
import { updateUserProfile } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { ToastContext } from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
-import Avatar from '@/app/components/base/avatar'
import { IS_CE_EDITION } from '@/config'
import Input from '@/app/components/base/input'
@@ -133,7 +133,7 @@ export default function AccountPage() {
{t('common.account.myAccount')}
-
+
{userProfile.name}
{userProfile.email}
@@ -296,37 +296,9 @@ export default function AccountPage() {
}
{
showDeleteAccountModal && (
-
setShowDeleteAccountModal(false)}
onConfirm={() => setShowDeleteAccountModal(false)}
- showCancel={false}
- type='warning'
- title={t('common.account.delete')}
- content={
- <>
-
- {t('common.account.deleteTip')}
-
-
- {`${t('common.account.delete')}: ${userProfile.email}`}
- >
- }
- confirmText={t('common.operation.ok') as string}
/>
)
}
diff --git a/web/app/account/avatar.tsx b/web/app/account/avatar.tsx
index 8fdecc07bf..bd9cbec4c4 100644
--- a/web/app/account/avatar.tsx
+++ b/web/app/account/avatar.tsx
@@ -8,7 +8,7 @@ import { logout } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
-export type IAppSelector = {
+export interface IAppSelector {
isMobile: boolean
}
@@ -45,7 +45,7 @@ export default function AppSelector() {
${open && 'bg-components-panel-bg-blur'}
`}
>
-
+
{userProfile.name}
{userProfile.email}
-
+
diff --git a/web/app/account/delete-account/components/check-email.tsx b/web/app/account/delete-account/components/check-email.tsx
new file mode 100644
index 0000000000..84ea8a4c24
--- /dev/null
+++ b/web/app/account/delete-account/components/check-email.tsx
@@ -0,0 +1,48 @@
+'use client'
+import { useTranslation } from 'react-i18next'
+import { useCallback, useState } from 'react'
+import Link from 'next/link'
+import { useSendDeleteAccountEmail } from '../state'
+import { useAppContext } from '@/context/app-context'
+import Input from '@/app/components/base/input'
+import Button from '@/app/components/base/button'
+
+type DeleteAccountProps = {
+ onCancel: () => void
+ onConfirm: () => void
+}
+
+export default function CheckEmail(props: DeleteAccountProps) {
+ const { t } = useTranslation()
+ const { userProfile } = useAppContext()
+ const [userInputEmail, setUserInputEmail] = useState('')
+
+ const { isPending: isSendingEmail, mutateAsync: getDeleteEmailVerifyCode } = useSendDeleteAccountEmail()
+
+ const handleConfirm = useCallback(async () => {
+ try {
+ const ret = await getDeleteEmailVerifyCode()
+ if (ret.result === 'success')
+ props.onConfirm()
+ }
+ catch (error) { console.error(error) }
+ }, [getDeleteEmailVerifyCode, props])
+
+ return <>
+
+ {t('common.account.deleteTip')}
+
+
+ {t('common.account.deletePrivacyLinkTip')}
+ {t('common.account.deletePrivacyLink')}
+
+
+
{
+ setUserInputEmail(e.target.value)
+ }} />
+
+
+
+
+ >
+}
diff --git a/web/app/account/delete-account/components/feed-back.tsx b/web/app/account/delete-account/components/feed-back.tsx
new file mode 100644
index 0000000000..1d01c69d94
--- /dev/null
+++ b/web/app/account/delete-account/components/feed-back.tsx
@@ -0,0 +1,68 @@
+'use client'
+import { useTranslation } from 'react-i18next'
+import { useCallback, useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { useDeleteAccountFeedback } from '../state'
+import { useAppContext } from '@/context/app-context'
+import Button from '@/app/components/base/button'
+import CustomDialog from '@/app/components/base/dialog'
+import Textarea from '@/app/components/base/textarea'
+import Toast from '@/app/components/base/toast'
+import { logout } from '@/service/common'
+
+type DeleteAccountProps = {
+ onCancel: () => void
+ onConfirm: () => void
+}
+
+export default function FeedBack(props: DeleteAccountProps) {
+ const { t } = useTranslation()
+ const { userProfile } = useAppContext()
+ const router = useRouter()
+ const [userFeedback, setUserFeedback] = useState('')
+ const { isPending, mutateAsync: sendFeedback } = useDeleteAccountFeedback()
+
+ const handleSuccess = useCallback(async () => {
+ try {
+ await logout({
+ url: '/logout',
+ params: {},
+ })
+ localStorage.removeItem('refresh_token')
+ localStorage.removeItem('console_token')
+ router.push('/signin')
+ Toast.notify({ type: 'info', message: t('common.account.deleteSuccessTip') })
+ }
+ catch (error) { console.error(error) }
+ }, [router, t])
+
+ const handleSubmit = useCallback(async () => {
+ try {
+ await sendFeedback({ feedback: userFeedback, email: userProfile.email })
+ props.onConfirm()
+ await handleSuccess()
+ }
+ catch (error) { console.error(error) }
+ }, [handleSuccess, userFeedback, sendFeedback, userProfile, props])
+
+ const handleSkip = useCallback(() => {
+ props.onCancel()
+ handleSuccess()
+ }, [handleSuccess, props])
+ return
+
+
+}
diff --git a/web/app/account/delete-account/components/verify-email.tsx b/web/app/account/delete-account/components/verify-email.tsx
new file mode 100644
index 0000000000..a986c79154
--- /dev/null
+++ b/web/app/account/delete-account/components/verify-email.tsx
@@ -0,0 +1,55 @@
+'use client'
+import { useTranslation } from 'react-i18next'
+import { useCallback, useEffect, useState } from 'react'
+import Link from 'next/link'
+import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state'
+import Input from '@/app/components/base/input'
+import Button from '@/app/components/base/button'
+import Countdown from '@/app/components/signin/countdown'
+
+const CODE_EXP = /[A-Za-z\d]{6}/gi
+
+type DeleteAccountProps = {
+ onCancel: () => void
+ onConfirm: () => void
+}
+
+export default function VerifyEmail(props: DeleteAccountProps) {
+ const { t } = useTranslation()
+ const emailToken = useAccountDeleteStore(state => state.sendEmailToken)
+ const [verificationCode, setVerificationCode] = useState
()
+ const [shouldButtonDisabled, setShouldButtonDisabled] = useState(true)
+ const { mutate: sendEmail } = useSendDeleteAccountEmail()
+ const { isPending: isDeleting, mutateAsync: confirmDeleteAccount } = useConfirmDeleteAccount()
+
+ useEffect(() => {
+ setShouldButtonDisabled(!(verificationCode && CODE_EXP.test(verificationCode)) || isDeleting)
+ }, [verificationCode, isDeleting])
+
+ const handleConfirm = useCallback(async () => {
+ try {
+ const ret = await confirmDeleteAccount({ code: verificationCode!, token: emailToken })
+ if (ret.result === 'success')
+ props.onConfirm()
+ }
+ catch (error) { console.error(error) }
+ }, [emailToken, verificationCode, confirmDeleteAccount, props])
+ return <>
+
+ {t('common.account.deleteTip')}
+
+
+ {t('common.account.deletePrivacyLinkTip')}
+ {t('common.account.deletePrivacyLink')}
+
+
+ {
+ setVerificationCode(e.target.value)
+ }} />
+
+
+
+
+
+ >
+}
diff --git a/web/app/account/delete-account/index.tsx b/web/app/account/delete-account/index.tsx
new file mode 100644
index 0000000000..545889041a
--- /dev/null
+++ b/web/app/account/delete-account/index.tsx
@@ -0,0 +1,44 @@
+'use client'
+import { useTranslation } from 'react-i18next'
+import { useCallback, useState } from 'react'
+import CheckEmail from './components/check-email'
+import VerifyEmail from './components/verify-email'
+import FeedBack from './components/feed-back'
+import CustomDialog from '@/app/components/base/dialog'
+import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
+
+type DeleteAccountProps = {
+ onCancel: () => void
+ onConfirm: () => void
+}
+
+export default function DeleteAccount(props: DeleteAccountProps) {
+ const { t } = useTranslation()
+
+ const [showVerifyEmail, setShowVerifyEmail] = useState(false)
+ const [showFeedbackDialog, setShowFeedbackDialog] = useState(false)
+
+ const handleEmailCheckSuccess = useCallback(async () => {
+ try {
+ setShowVerifyEmail(true)
+ localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
+ }
+ catch (error) { console.error(error) }
+ }, [])
+
+ if (showFeedbackDialog)
+ return
+
+ return
+ {!showVerifyEmail && }
+ {showVerifyEmail && {
+ setShowFeedbackDialog(true)
+ }} />}
+
+}
diff --git a/web/app/account/delete-account/state.tsx b/web/app/account/delete-account/state.tsx
new file mode 100644
index 0000000000..4c43fba12d
--- /dev/null
+++ b/web/app/account/delete-account/state.tsx
@@ -0,0 +1,39 @@
+import { useMutation } from '@tanstack/react-query'
+import { create } from 'zustand'
+import { sendDeleteAccountCode, submitDeleteAccountFeedback, verifyDeleteAccountCode } from '@/service/common'
+
+type State = {
+ sendEmailToken: string
+ setSendEmailToken: (token: string) => void
+}
+
+export const useAccountDeleteStore = create(set => ({
+ sendEmailToken: '',
+ setSendEmailToken: (token: string) => set({ sendEmailToken: token }),
+}))
+
+export function useSendDeleteAccountEmail() {
+ const updateEmailToken = useAccountDeleteStore(state => state.setSendEmailToken)
+ return useMutation({
+ mutationKey: ['delete-account'],
+ mutationFn: sendDeleteAccountCode,
+ onSuccess: (ret) => {
+ if (ret.result === 'success')
+ updateEmailToken(ret.data)
+ },
+ })
+}
+
+export function useConfirmDeleteAccount() {
+ return useMutation({
+ mutationKey: ['confirm-delete-account'],
+ mutationFn: verifyDeleteAccountCode,
+ })
+}
+
+export function useDeleteAccountFeedback() {
+ return useMutation({
+ mutationKey: ['delete-account-feedback'],
+ mutationFn: submitDeleteAccountFeedback,
+ })
+}
diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx
index 12f9c59cd1..57eb013be7 100644
--- a/web/app/components/app-sidebar/app-info.tsx
+++ b/web/app/components/app-sidebar/app-info.tsx
@@ -1,18 +1,18 @@
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
-import { RiArrowDownSLine } from '@remixicon/react'
import React, { useCallback, useState } from 'react'
+import {
+ RiDeleteBinLine,
+ RiEditLine,
+ RiEqualizer2Line,
+ RiFileCopy2Line,
+ RiFileDownloadLine,
+ RiFileUploadLine,
+} from '@remixicon/react'
import AppIcon from '../base/app-icon'
import SwitchAppModal from '../app/switch-app-modal'
-import s from './style.module.css'
import cn from '@/utils/classnames'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
-import Divider from '@/app/components/base/divider'
import Confirm from '@/app/components/base/confirm'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
@@ -22,8 +22,6 @@ import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/ap
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import CreateAppModal from '@/app/components/explore/create-app-modal'
-import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
-import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
@@ -31,6 +29,9 @@ import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
import { fetchWorkflowDraft } from '@/service/workflow'
+import ContentDialog from '@/app/components/base/content-dialog'
+import Button from '@/app/components/base/button'
+import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView'
export type IAppInfoProps = {
expand: boolean
@@ -47,7 +48,6 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
const [showEditModal, setShowEditModal] = useState(false)
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
- const [showSwitchTip, setShowSwitchTip] = useState('')
const [showSwitchModal, setShowSwitchModal] = useState(false)
const [showImportDSLModal, setShowImportDSLModal] = useState(false)
const [secretEnvList, setSecretEnvList] = useState([])
@@ -183,291 +183,199 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
return null
return (
-
-
-
{
- if (isCurrentWorkspaceEditor)
- setOpen(v => !v)
- }}
- className='block'
- >
-
-
-
-
- {appDetail.mode === 'advanced-chat' && (
-
- )}
- {appDetail.mode === 'agent-chat' && (
-
- )}
- {appDetail.mode === 'chat' && (
-
- )}
- {appDetail.mode === 'completion' && (
-
- )}
- {appDetail.mode === 'workflow' && (
-
- )}
-
-
- {expand && (
-
-
-
{appDetail.name}
- {isCurrentWorkspaceEditor &&
}
-
-
- {appDetail.mode === 'advanced-chat' && (
- <>
-
{t('app.types.chatbot').toUpperCase()}
-
{t('app.types.advanced').toUpperCase()}
- >
- )}
- {appDetail.mode === 'agent-chat' && (
-
{t('app.types.agent').toUpperCase()}
- )}
- {appDetail.mode === 'chat' && (
- <>
-
{t('app.types.chatbot').toUpperCase()}
-
{(t('app.types.basic').toUpperCase())}
- >
- )}
- {appDetail.mode === 'completion' && (
- <>
-
{t('app.types.completion').toUpperCase()}
-
{(t('app.types.basic').toUpperCase())}
- >
- )}
- {appDetail.mode === 'workflow' && (
-
{t('app.types.workflow').toUpperCase()}
- )}
-
+
+