diff --git a/api/README.md b/api/README.md index 97f09fc700..3bdb299479 100644 --- a/api/README.md +++ b/api/README.md @@ -33,3 +33,4 @@ flask run --host 0.0.0.0 --port=5001 --debug ``` 7. Setup your application by visiting http://localhost:5001/console/api/setup or other apis... +8. If you need to debug local async processing, you can run `celery -A app.celery worker`, celery can do dataset importing and other async tasks. \ No newline at end of file diff --git a/api/core/completion.py b/api/core/completion.py index f215bd0ee5..d05d8417da 100644 --- a/api/core/completion.py +++ b/api/core/completion.py @@ -125,13 +125,17 @@ class Completion: pre_prompt = PromptBuilder.process_template(pre_prompt) if pre_prompt else pre_prompt if mode == 'completion': prompt_template = OutLinePromptTemplate.from_template( - template=("Use the following pieces of [CONTEXT] to answer the question at the end. " - "If you don't know the answer, " - "just say that you don't know, don't try to make up an answer. \n" - "```\n" - "[CONTEXT]\n" - "{context}\n" - "```\n" if chain_output else "") + template=("""Use the following CONTEXT as your learned knowledge: +[CONTEXT] +{context} +[END CONTEXT] + +When answer to user: +- If you don't know, just say that you don't know. +- If you don't know when you are not sure, ask for clarification. +Avoid mentioning that you obtained the information from the context. +And answer according to the language of the user's question. +""" if chain_output else "") + (pre_prompt + "\n" if pre_prompt else "") + "{query}\n" ) @@ -153,38 +157,38 @@ class Completion: else: messages: List[BaseMessage] = [] - system_message = None - if pre_prompt: - # append pre prompt as system message - system_message = PromptBuilder.to_system_message(pre_prompt, inputs) - - if chain_output: - # append context as system message, currently only use simple stuff prompt - context_message = PromptBuilder.to_system_message( - """Use the following pieces of [CONTEXT] to answer the users question. -If you don't know the answer, just say that you don't know, don't try to make up an answer. -``` -[CONTEXT] -{context} -```""", - {'context': chain_output} - ) - - if not system_message: - system_message = context_message - else: - system_message.content = context_message.content + "\n\n" + system_message.content - - if system_message: - messages.append(system_message) - human_inputs = { "query": query } + human_message_prompt = "{query}" + + if chain_output: + human_inputs['context'] = chain_output + human_message_instruction = """Use the following CONTEXT as your learned knowledge. +[CONTEXT] +{context} +[END CONTEXT] + +When answer to user: +- If you don't know, just say that you don't know. +- If you don't know when you are not sure, ask for clarification. +Avoid mentioning that you obtained the information from the context. +And answer according to the language of the user's question. +""" + if pre_prompt: + human_inputs.update(inputs) + human_message_instruction += pre_prompt + "\n" + + human_message_prompt = human_message_instruction + "Q:{query}\nA:" + else: + if pre_prompt: + human_inputs.update(inputs) + human_message_prompt = pre_prompt + "\n" + human_message_prompt + # construct main prompt human_message = PromptBuilder.to_human_message( - prompt_content="{query}", + prompt_content=human_message_prompt, inputs=human_inputs ) diff --git a/api/core/conversation_message_task.py b/api/core/conversation_message_task.py index 81477533e7..0c2e9983ce 100644 --- a/api/core/conversation_message_task.py +++ b/api/core/conversation_message_task.py @@ -281,6 +281,9 @@ class PubHandler: @classmethod def generate_channel_name(cls, user: Union[Account | EndUser], task_id: str): + if not user: + raise ValueError("user is required") + user_str = 'account-' + user.id if isinstance(user, Account) else 'end-user-' + user.id return "generate_result:{}-{}".format(user_str, task_id) diff --git a/api/core/vector_store/weaviate_vector_store_client.py b/api/core/vector_store/weaviate_vector_store_client.py index 2310278cf9..ea020f951e 100644 --- a/api/core/vector_store/weaviate_vector_store_client.py +++ b/api/core/vector_store/weaviate_vector_store_client.py @@ -29,7 +29,7 @@ class WeaviateVectorStoreClient(BaseVectorStoreClient): return weaviate.Client( url=endpoint, auth_client_secret=auth_config, - timeout_config=(5, 15), + timeout_config=(5, 60), startup_period=None ) diff --git a/web/app/(commonLayout)/_layout-client.tsx b/web/app/(commonLayout)/_layout-client.tsx index 8624091de2..799f8985d3 100644 --- a/web/app/(commonLayout)/_layout-client.tsx +++ b/web/app/(commonLayout)/_layout-client.tsx @@ -1,5 +1,5 @@ 'use client' -import type { FC } from 'react' +import { FC, useRef } from 'react' import React, { useEffect, useState } from 'react' import { usePathname, useRouter, useSelectedLayoutSegments } from 'next/navigation' import useSWR, { SWRConfig } from 'swr' @@ -8,7 +8,7 @@ import { fetchAppList } from '@/service/apps' import { fetchDatasets } from '@/service/datasets' import { fetchLanggeniusVersion, fetchUserProfile, logout } from '@/service/common' import Loading from '@/app/components/base/loading' -import AppContext from '@/context/app-context' +import { AppContextProvider } from '@/context/app-context' import DatasetsContext from '@/context/datasets-context' import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' @@ -23,6 +23,7 @@ const CommonLayout: FC = ({ children }) => { const pattern = pathname.replace(/.*\/app\//, '') const [idOrMethod] = pattern.split('/') const isNotDetailPage = idOrMethod === 'list' + const pageContainerRef = useRef(null) const appId = isNotDetailPage ? '' : idOrMethod @@ -71,14 +72,14 @@ const CommonLayout: FC = ({ children }) => { - - -
+ + +
{children}
- +
) } diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index fd989edde4..aa3ac28458 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -1,23 +1,50 @@ 'use client' -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' +import useSWRInfinite from 'swr/infinite' +import { debounce } from 'lodash-es' import AppCard from './AppCard' import NewAppCard from './NewAppCard' -import { useAppContext } from '@/context/app-context' +import { AppListResponse } from '@/models/app' +import { fetchAppList } from '@/service/apps' +import { useSelector } from '@/context/app-context' + +const getKey = (pageIndex: number, previousPageData: AppListResponse) => { + if (!pageIndex || previousPageData.has_more) + return { url: 'apps', params: { page: pageIndex + 1, limit: 30 } } + return null +} const Apps = () => { - const { apps, mutateApps } = useAppContext() + const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false }) + const loadingStateRef = useRef(false) + const pageContainerRef = useSelector(state => state.pageContainerRef) + const anchorRef = useRef(null) useEffect(() => { - mutateApps() + loadingStateRef.current = isLoading + }, [isLoading]) + + useEffect(() => { + const onScroll = debounce(() => { + if (!loadingStateRef.current) { + const { scrollTop, clientHeight } = pageContainerRef.current! + const anchorOffset = anchorRef.current!.offsetTop + if (anchorOffset - scrollTop - clientHeight < 100) { + setSize(size => size + 1) + } + } + }, 50) + + pageContainerRef.current?.addEventListener('scroll', onScroll) + return () => pageContainerRef.current?.removeEventListener('scroll', onScroll) }, []) return ( - ) } diff --git a/web/app/(commonLayout)/apps/NewAppCard.tsx b/web/app/(commonLayout)/apps/NewAppCard.tsx index ddbb0f03b9..f8cfb4062c 100644 --- a/web/app/(commonLayout)/apps/NewAppCard.tsx +++ b/web/app/(commonLayout)/apps/NewAppCard.tsx @@ -1,16 +1,20 @@ 'use client' -import { useState } from 'react' +import { forwardRef, useState } from 'react' import classNames from 'classnames' import { useTranslation } from 'react-i18next' import style from '../list.module.css' import NewAppDialog from './NewAppDialog' -const CreateAppCard = () => { +export type CreateAppCardProps = { + onSuccess?: () => void +} + +const CreateAppCard = forwardRef(({ onSuccess }, ref) => { const { t } = useTranslation() const [showNewAppDialog, setShowNewAppDialog] = useState(false) return ( - setShowNewAppDialog(true)}> + setShowNewAppDialog(true)}>
@@ -20,9 +24,9 @@ const CreateAppCard = () => {
{/*
{t('app.createFromConfigFile')}
*/} - setShowNewAppDialog(false)} /> + setShowNewAppDialog(false)} />
) -} +}) export default CreateAppCard diff --git a/web/app/(commonLayout)/apps/NewAppDialog.tsx b/web/app/(commonLayout)/apps/NewAppDialog.tsx index 10966ba4a4..e378560dd4 100644 --- a/web/app/(commonLayout)/apps/NewAppDialog.tsx +++ b/web/app/(commonLayout)/apps/NewAppDialog.tsx @@ -21,10 +21,11 @@ import EmojiPicker from '@/app/components/base/emoji-picker' type NewAppDialogProps = { show: boolean + onSuccess?: () => void onClose?: () => void } -const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => { +const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => { const router = useRouter() const { notify } = useContext(ToastContext) const { t } = useTranslation() @@ -79,6 +80,8 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => { mode: isWithTemplate ? templates.data[selectedTemplateIndex].mode : newAppMode!, config: isWithTemplate ? templates.data[selectedTemplateIndex].model_config : undefined, }) + if (onSuccess) + onSuccess() if (onClose) onClose() notify({ type: 'success', message: t('app.newApp.appCreated') }) diff --git a/web/app/(commonLayout)/datasets/Datasets.tsx b/web/app/(commonLayout)/datasets/Datasets.tsx index b044547748..31e38d7fc0 100644 --- a/web/app/(commonLayout)/datasets/Datasets.tsx +++ b/web/app/(commonLayout)/datasets/Datasets.tsx @@ -1,24 +1,49 @@ 'use client' -import { useEffect } from 'react' -import useSWR from 'swr' -import { DataSet } from '@/models/datasets'; +import { useEffect, useRef } from 'react' +import useSWRInfinite from 'swr/infinite' +import { debounce } from 'lodash-es'; +import { DataSetListResponse } from '@/models/datasets'; import NewDatasetCard from './NewDatasetCard' import DatasetCard from './DatasetCard'; import { fetchDatasets } from '@/service/datasets'; +import { useSelector } from '@/context/app-context'; + +const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { + if (!pageIndex || previousPageData.has_more) + return { url: 'datasets', params: { page: pageIndex + 1, limit: 30 } } + return null +} const Datasets = () => { - // const { datasets, mutateDatasets } = useAppContext() - const { data: datasetList, mutate: mutateDatasets } = useSWR({ url: '/datasets', params: { page: 1 } }, fetchDatasets) + const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchDatasets, { revalidateFirstPage: false }) + const loadingStateRef = useRef(false) + const pageContainerRef = useSelector(state => state.pageContainerRef) + const anchorRef = useRef(null) useEffect(() => { - mutateDatasets() + loadingStateRef.current = isLoading + }, [isLoading]) + + useEffect(() => { + const onScroll = debounce(() => { + if (!loadingStateRef.current) { + const { scrollTop, clientHeight } = pageContainerRef.current! + const anchorOffset = anchorRef.current!.offsetTop + if (anchorOffset - scrollTop - clientHeight < 100) { + setSize(size => size + 1) + } + } + }, 50) + + pageContainerRef.current?.addEventListener('scroll', onScroll) + return () => pageContainerRef.current?.removeEventListener('scroll', onScroll) }, []) return ( ) } diff --git a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx b/web/app/(commonLayout)/datasets/NewDatasetCard.tsx index a3f6282c97..72f6b18dcc 100644 --- a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx +++ b/web/app/(commonLayout)/datasets/NewDatasetCard.tsx @@ -1,16 +1,16 @@ 'use client' -import { useState } from 'react' +import { forwardRef, useState } from 'react' import classNames from 'classnames' import { useTranslation } from 'react-i18next' import style from '../list.module.css' -const CreateAppCard = () => { +const CreateAppCard = forwardRef((_, ref) => { const { t } = useTranslation() const [showNewAppDialog, setShowNewAppDialog] = useState(false) return ( - +
@@ -23,6 +23,6 @@ const CreateAppCard = () => { {/*
{t('app.createFromConfigFile')}
*/}
) -} +}) export default CreateAppCard diff --git a/web/app/components/app/chat/index.tsx b/web/app/components/app/chat/index.tsx index 38d85edc4c..3cfcc42773 100644 --- a/web/app/components/app/chat/index.tsx +++ b/web/app/components/app/chat/index.tsx @@ -291,74 +291,76 @@ const Answer: FC = ({ item, feedbackDisabled = false, isHideFeedba
} -
-
-
- {item.isOpeningStatement && ( -
- -
{t('appDebug.openingStatement.title')}
-
- )} - {(isResponsing && !content) ? ( -
- -
- ) : ( - - )} - {!showEdit - ? (annotation?.content - && <> - - {annotation.content} - ) - : <> - - setInputValue(e.target.value)} - minHeight={58} - className={`${cn(s.textArea)} !py-2 resize-none block w-full !px-3 bg-gray-50 border border-gray-200 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm text-gray-700 tracking-[0.2px]`} - /> -
- - +
+
+
+
+ {item.isOpeningStatement && ( +
+ +
{t('appDebug.openingStatement.title')}
- - } -
-
- - {!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')} - {/* Admin feedback is displayed only in the background. */} - {!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)} - {/* User feedback must be displayed */} - {!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')} + )} + {(isResponsing && !content) ? ( +
+ +
+ ) : ( + + )} + {!showEdit + ? (annotation?.content + && <> + + {annotation.content} + ) + : <> + + setInputValue(e.target.value)} + minHeight={58} + className={`${cn(s.textArea)} !py-2 resize-none block w-full !px-3 bg-gray-50 border border-gray-200 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm text-gray-700 tracking-[0.2px]`} + /> +
+ + +
+ + } +
+
+ + {!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')} + {/* Admin feedback is displayed only in the background. */} + {!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)} + {/* User feedback must be displayed */} + {!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')} +
+ {more && }
- {more && }
@@ -372,7 +374,7 @@ const Question: FC = ({ id, content, more, useCurrentUserAvatar const userName = userProfile?.name return (
-
+
onClose?.(), [onClose]) return ( - + = ({ onClose={() => { }} isShow closable={false} + wrapperClassName='!z-40' className={cn(s.container, '!w-[362px] !p-0')} >
diff --git a/web/app/components/base/modal/index.tsx b/web/app/components/base/modal/index.tsx index a83bafa5be..7c11089769 100644 --- a/web/app/components/base/modal/index.tsx +++ b/web/app/components/base/modal/index.tsx @@ -26,7 +26,7 @@ export default function Modal({ }: IModal) { return ( - + { onChange={fileChangeHandle} />
{t('datasetCreation.stepOne.uploader.title')}
- {!currentFile && !file && ( -
- {t('datasetCreation.stepOne.uploader.button')} - - {dragging &&
} -
- )} +
+ {!currentFile && !file && ( +
+ {t('datasetCreation.stepOne.uploader.button')} + + {dragging &&
} +
+ )} +
{currentFile && (
{uploading && ( diff --git a/web/app/components/datasets/create/step-two/preview-item/index.tsx b/web/app/components/datasets/create/step-two/preview-item/index.tsx index 4108ef4807..1644e9b102 100644 --- a/web/app/components/datasets/create/step-two/preview-item/index.tsx +++ b/web/app/components/datasets/create/step-two/preview-item/index.tsx @@ -41,7 +41,7 @@ const PreviewItem: FC = ({
- {content} +
{content}
) diff --git a/web/app/components/datasets/documents/detail/completed/style.module.css b/web/app/components/datasets/documents/detail/completed/style.module.css index d1eeaf6726..323315b957 100644 --- a/web/app/components/datasets/documents/detail/completed/style.module.css +++ b/web/app/components/datasets/documents/detail/completed/style.module.css @@ -45,6 +45,7 @@ } .segModalContent { @apply h-96 text-gray-800 text-base overflow-y-scroll; + white-space: pre-line; } .footer { @apply flex items-center justify-between box-border border-t-gray-200 border-t-[0.5px] pt-3 mt-4; diff --git a/web/app/styles/markdown.scss b/web/app/styles/markdown.scss index fdfaae0cf9..ac19b9d76e 100644 --- a/web/app/styles/markdown.scss +++ b/web/app/styles/markdown.scss @@ -54,6 +54,7 @@ font-weight: 400; line-height: 1.5; word-wrap: break-word; + word-break: break-all; user-select: text; } @@ -593,6 +594,7 @@ .markdown-body table th { font-weight: var(--base-text-weight-semibold, 600); + white-space: nowrap; } .markdown-body table th, diff --git a/web/context/app-context.ts b/web/context/app-context.ts deleted file mode 100644 index d31b9fedca..0000000000 --- a/web/context/app-context.ts +++ /dev/null @@ -1,27 +0,0 @@ -'use client' - -import { createContext, useContext } from 'use-context-selector' -import type { App } from '@/types/app' -import type { UserProfileResponse } from '@/models/common' - -export type AppContextValue = { - apps: App[] - mutateApps: () => void - userProfile: UserProfileResponse - mutateUserProfile: () => void -} - -const AppContext = createContext({ - apps: [], - mutateApps: () => { }, - userProfile: { - id: '', - name: '', - email: '', - }, - mutateUserProfile: () => { }, -}) - -export const useAppContext = () => useContext(AppContext) - -export default AppContext diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx new file mode 100644 index 0000000000..90cfc5ec11 --- /dev/null +++ b/web/context/app-context.tsx @@ -0,0 +1,45 @@ +'use client' + +import { createContext, useContext, useContextSelector } from 'use-context-selector' +import type { App } from '@/types/app' +import type { UserProfileResponse } from '@/models/common' +import { createRef, FC, PropsWithChildren } from 'react' + +export const useSelector = (selector: (value: AppContextValue) => T): T => + useContextSelector(AppContext, selector); + +export type AppContextValue = { + apps: App[] + mutateApps: () => void + userProfile: UserProfileResponse + mutateUserProfile: () => void + pageContainerRef: React.RefObject, + useSelector: typeof useSelector, +} + +const AppContext = createContext({ + apps: [], + mutateApps: () => { }, + userProfile: { + id: '', + name: '', + email: '', + }, + mutateUserProfile: () => { }, + pageContainerRef: createRef(), + useSelector, +}) + +export type AppContextProviderProps = PropsWithChildren<{ + value: Omit +}> + +export const AppContextProvider: FC = ({ value, children }) => ( + + {children} + +) + +export const useAppContext = () => useContext(AppContext) + +export default AppContext diff --git a/web/middleware.ts b/web/middleware.ts index 5b17cbd673..39ae80ee76 100644 --- a/web/middleware.ts +++ b/web/middleware.ts @@ -23,7 +23,11 @@ export const getLocale = (request: NextRequest): Locale => { } // match locale - const matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale + let matchedLocale:Locale = i18n.defaultLocale + try { + // If languages is ['*'], Error would happen in match function. + matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale + } catch(e) {} return matchedLocale } diff --git a/web/models/app.ts b/web/models/app.ts index ddafdfbc72..8c5bfd0fab 100644 --- a/web/models/app.ts +++ b/web/models/app.ts @@ -61,6 +61,10 @@ export type SiteConfig = { export type AppListResponse = { data: App[] + has_more: boolean + limit: number + page: number + total: number } export type AppDetailResponse = App diff --git a/web/models/datasets.ts b/web/models/datasets.ts index 8f1206024f..70a4c13ebe 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -29,6 +29,10 @@ export type File = { export type DataSetListResponse = { data: DataSet[] + has_more: boolean + limit: number + page: number + total: number } export type IndexingEstimateResponse = { diff --git a/web/service/apps.ts b/web/service/apps.ts index e9f619787e..f06b7c0ff4 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -4,8 +4,8 @@ import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUse import type { CommonResponse } from '@/models/common' import type { AppMode, ModelConfig } from '@/types/app' -export const fetchAppList: Fetcher }> = ({ params }) => { - return get('apps', params) as Promise +export const fetchAppList: Fetcher }> = ({ url, params }) => { + return get(url, { params }) as Promise } export const fetchAppDetail: Fetcher = ({ url, id }) => {