| @@ -1,13 +0,0 @@ | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import UniversalChat from '@/app/components/explore/universal-chat' | |||
| const Chat: FC = () => { | |||
| return ( | |||
| <div className='h-full p-2'> | |||
| <UniversalChat /> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Chat) | |||
| @@ -16,7 +16,6 @@ export type ICardItemProps = { | |||
| onRemove: (id: string) => void | |||
| readonly?: boolean | |||
| } | |||
| // used in universal-chat | |||
| const CardItem: FC<ICardItemProps> = ({ | |||
| className, | |||
| config, | |||
| @@ -33,7 +33,7 @@ const AudioBtn = ({ | |||
| if (value !== '') { | |||
| formData.append('text', removeCodeBlocks(value)) | |||
| let url = '/universal-chat/text-to-audio' | |||
| let url = '' | |||
| let isPublic = false | |||
| if (params.token) { | |||
| @@ -86,7 +86,7 @@ const VoiceInput = ({ | |||
| const formData = new FormData() | |||
| formData.append('file', mp3File) | |||
| let url = '/universal-chat/audio-to-text' | |||
| let url = '' | |||
| let isPublic = false | |||
| if (params.token) { | |||
| @@ -1,38 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import cn from 'classnames' | |||
| import { useTranslation } from 'react-i18next' | |||
| import s from './style.module.css' | |||
| import Config from '@/app/components/explore/universal-chat/config' | |||
| import type { DataSet } from '@/models/datasets' | |||
| type Props = { | |||
| modelId: string | |||
| providerName: string | |||
| plugins: Record<string, boolean> | |||
| dataSets: DataSet[] | |||
| } | |||
| const ConfigViewPanel: FC<Props> = ({ | |||
| modelId, | |||
| providerName, | |||
| plugins, | |||
| dataSets, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className={cn('absolute top-9 right-0 z-20 p-4 bg-white rounded-2xl shadow-md', s.panelBorder)}> | |||
| <div className='w-[368px]'> | |||
| <Config | |||
| readonly | |||
| modelId={modelId} | |||
| providerName={providerName} | |||
| plugins={plugins} | |||
| dataSets={dataSets} | |||
| /> | |||
| <div className='mt-3 text-xs leading-[18px] text-500 font-normal'>{t('explore.universalChat.viewConfigDetailTip')}</div> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(ConfigViewPanel) | |||
| @@ -1,9 +0,0 @@ | |||
| .btn { | |||
| background: url(~@/assets/action.svg) center center no-repeat transparent; | |||
| background-size: 16px 16px; | |||
| /* mask-image: ; */ | |||
| } | |||
| .panelBorder { | |||
| border: 0.5px solid rgba(0, 0, 0, .05); | |||
| } | |||
| @@ -1,107 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import cn from 'classnames' | |||
| import { useBoolean, useClickAway } from 'ahooks' | |||
| import s from './style.module.css' | |||
| import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' | |||
| import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' | |||
| import { Google, WebReader, Wikipedia } from '@/app/components/base/icons/src/public/plugins' | |||
| import ConfigDetail from '@/app/components/explore/universal-chat/config-view/detail' | |||
| import type { DataSet } from '@/models/datasets' | |||
| import { useAgentThoughtCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' | |||
| export type ISummaryProps = { | |||
| modelId: string | |||
| providerName: string | |||
| plugins: Record<string, boolean> | |||
| dataSets: DataSet[] | |||
| } | |||
| const getColorInfo = (modelId: string) => { | |||
| if (modelId === 'gpt-4') | |||
| return s.gpt4 | |||
| if (modelId === 'claude-2') | |||
| return s.claude | |||
| return s.gpt3 | |||
| } | |||
| const getPlugIcon = (pluginId: string) => { | |||
| const className = 'w-4 h-4' | |||
| switch (pluginId) { | |||
| case 'google_search': | |||
| return <Google className={className} /> | |||
| case 'web_reader': | |||
| return <WebReader className={className} /> | |||
| case 'wikipedia': | |||
| return <Wikipedia className={className} /> | |||
| default: | |||
| return null | |||
| } | |||
| } | |||
| const Summary: FC<ISummaryProps> = ({ | |||
| modelId, | |||
| providerName, | |||
| plugins, | |||
| dataSets, | |||
| }) => { | |||
| const { | |||
| currentModel: currModel, | |||
| currentProvider, | |||
| } = useAgentThoughtCurrentProviderAndModelAndModelList( | |||
| { provider: providerName, model: modelId }, | |||
| ) | |||
| // current_datetime is not configable and do not have icon | |||
| const pluginIds = Object.keys(plugins).filter(key => plugins[key] && key !== 'current_datetime') | |||
| const [isShowConfig, { setFalse: hideConfig, toggle: toggleShowConfig }] = useBoolean(false) | |||
| const configContentRef = React.useRef(null) | |||
| useClickAway(() => { | |||
| hideConfig() | |||
| }, configContentRef) | |||
| return ( | |||
| <div ref={configContentRef} className='relative'> | |||
| <div onClick={toggleShowConfig} className={cn(getColorInfo(modelId), 'flex items-center px-1 h-8 rounded-lg border cursor-pointer')}> | |||
| <ModelIcon | |||
| provider={currentProvider} | |||
| modelName={currModel?.model} | |||
| className='!w-6 !h-6' | |||
| /> | |||
| <div className='ml-2 text-[13px] font-medium text-gray-900'> | |||
| <ModelName | |||
| modelItem={currModel!} | |||
| /> | |||
| </div> | |||
| { | |||
| pluginIds.length > 0 && ( | |||
| <div className='ml-1.5 flex items-center'> | |||
| <div className='mr-1 h-3 w-[1px] bg-[#000] opacity-[0.05]'></div> | |||
| <div className='flex space-x-1'> | |||
| {pluginIds.map(pluginId => ( | |||
| <div | |||
| key={pluginId} | |||
| className={`flex items-center justify-center w-6 h-6 rounded-md ${s.border} bg-white`} | |||
| > | |||
| {getPlugIcon(pluginId)}</div> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| {isShowConfig && ( | |||
| <ConfigDetail | |||
| modelId={modelId} | |||
| providerName={providerName} | |||
| plugins={plugins} | |||
| dataSets={dataSets} | |||
| /> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Summary) | |||
| @@ -1,21 +0,0 @@ | |||
| .border { | |||
| border: 1px solid rgba(0, 0, 0, 0.05); | |||
| } | |||
| .gpt3 { | |||
| background: linear-gradient(0deg, #D3F8DF, #D3F8DF), | |||
| linear-gradient(0deg, #EDFCF2, #EDFCF2); | |||
| border: 1px solid rgba(211, 248, 223, 1) | |||
| } | |||
| .gpt4 { | |||
| background: linear-gradient(0deg, #EBE9FE, #EBE9FE), | |||
| linear-gradient(0deg, #F4F3FF, #F4F3FF); | |||
| border: 1px solid rgba(235, 233, 254, 1) | |||
| } | |||
| .claude { | |||
| background: linear-gradient(0deg, #F9EBDF, #F9EBDF), | |||
| linear-gradient(0deg, #FCF3EB, #FCF3EB); | |||
| border: 1px solid rgba(249, 235, 223, 1) | |||
| } | |||
| @@ -1,94 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useBoolean } from 'ahooks' | |||
| import { isEqual } from 'lodash-es' | |||
| import produce from 'immer' | |||
| import FeaturePanel from '@/app/components/app/configuration/base/feature-panel' | |||
| import OperationBtn from '@/app/components/app/configuration/base/operation-btn' | |||
| import CardItem from '@/app/components/app/configuration/dataset-config/card-item' | |||
| import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset' | |||
| import type { DataSet } from '@/models/datasets' | |||
| type Props = { | |||
| readonly?: boolean | |||
| dataSets: DataSet[] | |||
| onChange?: (data: DataSet[]) => void | |||
| } | |||
| const DatasetConfig: FC<Props> = ({ | |||
| readonly, | |||
| dataSets, | |||
| onChange, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const selectedIds = dataSets.map(item => item.id) | |||
| const hasData = dataSets.length > 0 | |||
| const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false) | |||
| const handleSelect = (data: DataSet[]) => { | |||
| if (isEqual(data.map(item => item.id), dataSets.map(item => item.id))) { | |||
| hideSelectDataSet() | |||
| return | |||
| } | |||
| if (data.find(item => !item.name)) { // has not loaded selected dataset | |||
| const newSelected = produce(data, (draft) => { | |||
| data.forEach((item, index) => { | |||
| if (!item.name) { // not fetched database | |||
| const newItem = dataSets.find(i => i.id === item.id) | |||
| if (newItem) | |||
| draft[index] = newItem | |||
| } | |||
| }) | |||
| }) | |||
| onChange?.(newSelected) | |||
| } | |||
| else { | |||
| onChange?.(data) | |||
| } | |||
| hideSelectDataSet() | |||
| } | |||
| const onRemove = (id: string) => { | |||
| onChange?.(dataSets.filter(item => item.id !== id)) | |||
| } | |||
| return ( | |||
| <FeaturePanel | |||
| className='mt-3' | |||
| title={t('appDebug.feature.dataSet.title')} | |||
| headerRight={!readonly && <OperationBtn type="add" onClick={showSelectDataSet} />} | |||
| hasHeaderBottomBorder={!hasData} | |||
| > | |||
| {hasData | |||
| ? ( | |||
| <div className='max-h-[220px] overflow-y-auto'> | |||
| {dataSets.map(item => ( | |||
| <CardItem | |||
| className="mb-2 !w-full" | |||
| key={item.id} | |||
| config={item} | |||
| onRemove={onRemove} | |||
| readonly={readonly} | |||
| /> | |||
| ))} | |||
| </div> | |||
| ) | |||
| : ( | |||
| <div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.feature.dataSet.noData')}</div> | |||
| )} | |||
| {isShowSelectDataSet && ( | |||
| <SelectDataSet | |||
| isShow={isShowSelectDataSet} | |||
| onClose={hideSelectDataSet} | |||
| selectedIds={selectedIds} | |||
| onSelect={handleSelect} | |||
| /> | |||
| )} | |||
| </FeaturePanel> | |||
| ) | |||
| } | |||
| export default React.memo(DatasetConfig) | |||
| @@ -1,55 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import ModelConfig from './model-config' | |||
| import DataConfig from './data-config' | |||
| import PluginConfig from './plugins-config' | |||
| import type { DataSet } from '@/models/datasets' | |||
| export type IConfigProps = { | |||
| className?: string | |||
| readonly?: boolean | |||
| modelId: string | |||
| providerName: string | |||
| onModelChange?: (modelId: string, providerName: string) => void | |||
| plugins: Record<string, boolean> | |||
| onPluginChange?: (key: string, value: boolean) => void | |||
| dataSets: DataSet[] | |||
| onDataSetsChange?: (contexts: DataSet[]) => void | |||
| } | |||
| const Config: FC<IConfigProps> = ({ | |||
| className, | |||
| readonly, | |||
| modelId, | |||
| providerName, | |||
| onModelChange, | |||
| plugins, | |||
| onPluginChange, | |||
| dataSets, | |||
| onDataSetsChange, | |||
| }) => { | |||
| return ( | |||
| <div className={className}> | |||
| <ModelConfig | |||
| readonly={readonly} | |||
| modelId={modelId} | |||
| providerName={providerName} | |||
| onChange={onModelChange} | |||
| /> | |||
| <PluginConfig | |||
| readonly={readonly} | |||
| config={plugins} | |||
| onChange={onPluginChange} | |||
| /> | |||
| {(!readonly || (readonly && dataSets.length > 0)) && ( | |||
| <DataConfig | |||
| readonly={readonly} | |||
| dataSets={dataSets} | |||
| onChange={onDataSetsChange} | |||
| /> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Config) | |||
| @@ -1,39 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector' | |||
| import { useProviderContext } from '@/context/provider-context' | |||
| export type IModelConfigProps = { | |||
| modelId: string | |||
| providerName: string | |||
| onChange?: (modelId: string, providerName: string) => void | |||
| readonly?: boolean | |||
| } | |||
| const ModelConfig: FC<IModelConfigProps> = ({ | |||
| modelId, | |||
| providerName, | |||
| onChange, | |||
| readonly, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { agentThoughtModelList } = useProviderContext() | |||
| return ( | |||
| <div className='flex items-center justify-between h-[52px] px-3 rounded-xl bg-gray-50'> | |||
| <div className='text-sm font-semibold text-gray-800'>{t('explore.universalChat.model')}</div> | |||
| <ModelSelector | |||
| triggerClassName={`${readonly && '!cursor-not-allowed !opacity-60'}`} | |||
| defaultModel={{ provider: providerName, model: modelId }} | |||
| modelList={agentThoughtModelList} | |||
| onSelect={(model) => { | |||
| onChange?.(model.model, model.provider) | |||
| }} | |||
| readonly={readonly} | |||
| /> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(ModelConfig) | |||
| @@ -1,103 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useEffect } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import Item from './item' | |||
| import FeaturePanel from '@/app/components/app/configuration/base/feature-panel' | |||
| import { Google, WebReader, Wikipedia } from '@/app/components/base/icons/src/public/plugins' | |||
| import { getToolProviders } from '@/service/explore' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { useModalContext } from '@/context/modal-context' | |||
| export type IPluginsProps = { | |||
| readonly?: boolean | |||
| config: Record<string, boolean> | |||
| onChange?: (key: string, value: boolean) => void | |||
| } | |||
| const plugins = [ | |||
| { key: 'google_search', icon: <Google /> }, | |||
| { key: 'web_reader', icon: <WebReader /> }, | |||
| { key: 'wikipedia', icon: <Wikipedia /> }, | |||
| ] as const | |||
| const Plugins: FC<IPluginsProps> = ({ | |||
| readonly, | |||
| config, | |||
| onChange, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { setShowAccountSettingModal } = useModalContext() | |||
| const [isLoading, setIsLoading] = React.useState(!readonly) | |||
| const [isSerpApiValid, setIsSerpApiValid] = React.useState(false) | |||
| const checkSerpApiKey = async () => { | |||
| if (readonly) | |||
| return | |||
| const provides: any = await getToolProviders() | |||
| const isSerpApiValid = !!provides.find((v: any) => v.tool_name === 'serpapi' && v.is_enabled) | |||
| setIsSerpApiValid(isSerpApiValid) | |||
| setIsLoading(false) | |||
| } | |||
| useEffect(() => { | |||
| checkSerpApiKey() | |||
| }, []) | |||
| const itemConfigs = plugins.map((plugin) => { | |||
| const res: Record<string, any> = { ...plugin } | |||
| const { key } = plugin | |||
| res.name = t(`explore.universalChat.plugins.${key}.name`) | |||
| if (key === 'web_reader') | |||
| res.description = t(`explore.universalChat.plugins.${key}.description`) | |||
| if (key === 'google_search' && !isSerpApiValid && !readonly) { | |||
| res.readonly = true | |||
| res.more = ( | |||
| <div className='border-t border-[#FEF0C7] flex items-center h-[34px] pl-2 bg-[#FFFAEB] text-gray-700 text-xs '> | |||
| <span className='whitespace-pre'>{t('explore.universalChat.plugins.google_search.more.left')}</span> | |||
| <span className='cursor-pointer text-[#155EEF]' onClick={() => setShowAccountSettingModal({ payload: 'plugin', onCancelCallback: async () => await checkSerpApiKey() })}>{t('explore.universalChat.plugins.google_search.more.link')}</span> | |||
| <span className='whitespace-pre'>{t('explore.universalChat.plugins.google_search.more.right')}</span> | |||
| </div> | |||
| ) | |||
| } | |||
| return res | |||
| }) | |||
| const enabledPluginNum = Object.values(config).filter(v => v).length | |||
| return ( | |||
| <> | |||
| <FeaturePanel | |||
| className='mt-3' | |||
| title={ | |||
| <div className='flex space-x-1'> | |||
| <div>{t('explore.universalChat.plugins.name')}</div> | |||
| <div className='text-[13px] font-normal text-gray-500'>({enabledPluginNum}/{plugins.length})</div> | |||
| </div>} | |||
| hasHeaderBottomBorder={false} | |||
| > | |||
| {isLoading | |||
| ? ( | |||
| <div className='flex items-center h-[166px]'> | |||
| <Loading type='area' /> | |||
| </div> | |||
| ) | |||
| : (<div className='space-y-2'> | |||
| {itemConfigs.map(item => ( | |||
| <Item | |||
| key={item.key} | |||
| icon={item.icon} | |||
| name={item.name} | |||
| description={item.description} | |||
| more={item.more} | |||
| enabled={config[item.key]} | |||
| onChange={enabled => onChange?.(item.key, enabled)} | |||
| readonly={readonly || item.readonly} | |||
| /> | |||
| ))} | |||
| </div>)} | |||
| </FeaturePanel> | |||
| </> | |||
| ) | |||
| } | |||
| export default React.memo(Plugins) | |||
| @@ -1,3 +0,0 @@ | |||
| .shadow { | |||
| box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); | |||
| } | |||
| @@ -1,43 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import cn from 'classnames' | |||
| import s from './item.module.css' | |||
| import Switch from '@/app/components/base/switch' | |||
| export type IItemProps = { | |||
| icon: React.ReactNode | |||
| name: string | |||
| description?: string | |||
| more?: React.ReactNode | |||
| enabled: boolean | |||
| onChange: (enabled: boolean) => void | |||
| readonly?: boolean | |||
| } | |||
| const Item: FC<IItemProps> = ({ | |||
| icon, | |||
| name, | |||
| description, | |||
| more, | |||
| enabled, | |||
| onChange, | |||
| readonly, | |||
| }) => { | |||
| return ( | |||
| <div className={cn('bg-white rounded-xl border border-gray-200 overflow-hidden', s.shadow)}> | |||
| <div className='flex justify-between items-center min-h-[48px] px-2'> | |||
| <div className='flex items-center space-x-2'> | |||
| {icon} | |||
| <div className='leading-[18px]'> | |||
| <div className='text-[13px] font-medium text-gray-800'>{name}</div> | |||
| {description && <div className='text-xs leading-[18px] text-gray-500'>{description}</div>} | |||
| </div> | |||
| </div> | |||
| <Switch size='md' defaultValue={enabled} onChange={onChange} disabled={readonly} /> | |||
| </div> | |||
| {more} | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Item) | |||
| @@ -1,73 +0,0 @@ | |||
| import { useState } from 'react' | |||
| import produce from 'immer' | |||
| import { useGetState } from 'ahooks' | |||
| import type { ConversationItem } from '@/models/share' | |||
| const storageConversationIdKey = 'conversationIdInfo' | |||
| type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'> | |||
| function useConversation() { | |||
| const [conversationList, setConversationList] = useState<ConversationItem[]>([]) | |||
| const [pinnedConversationList, setPinnedConversationList] = useState<ConversationItem[]>([]) | |||
| const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState<string>('-1') | |||
| // when set conversation id, we do not have set appId | |||
| const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => { | |||
| doSetCurrConversationId(id) | |||
| if (isSetToLocalStroge && id !== '-1') { | |||
| // conversationIdInfo: {[appId1]: conversationId1, [appId2]: conversationId2} | |||
| const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {} | |||
| conversationIdInfo[appId] = id | |||
| globalThis.localStorage?.setItem(storageConversationIdKey, JSON.stringify(conversationIdInfo)) | |||
| } | |||
| } | |||
| const getConversationIdFromStorage = (appId: string) => { | |||
| const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {} | |||
| const id = conversationIdInfo[appId] | |||
| return id | |||
| } | |||
| const isNewConversation = currConversationId === '-1' | |||
| // input can be updated by user | |||
| const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null) | |||
| const resetNewConversationInputs = () => { | |||
| if (!newConversationInputs) | |||
| return | |||
| setNewConversationInputs(produce(newConversationInputs, (draft) => { | |||
| Object.keys(draft).forEach((key) => { | |||
| draft[key] = '' | |||
| }) | |||
| })) | |||
| } | |||
| const [existConversationInputs, setExistConversationInputs] = useState<Record<string, any> | null>(null) | |||
| const currInputs = isNewConversation ? newConversationInputs : existConversationInputs | |||
| const setCurrInputs = isNewConversation ? setNewConversationInputs : setExistConversationInputs | |||
| // info is muted | |||
| const [newConversationInfo, setNewConversationInfo] = useState<ConversationInfoType | null>(null) | |||
| const [existConversationInfo, setExistConversationInfo] = useState<ConversationInfoType | null>(null) | |||
| const currConversationInfo = isNewConversation ? newConversationInfo : existConversationInfo | |||
| return { | |||
| conversationList, | |||
| setConversationList, | |||
| pinnedConversationList, | |||
| setPinnedConversationList, | |||
| currConversationId, | |||
| getCurrConversationId, | |||
| setCurrConversationId, | |||
| getConversationIdFromStorage, | |||
| isNewConversation, | |||
| currInputs, | |||
| newConversationInputs, | |||
| existConversationInputs, | |||
| resetNewConversationInputs, | |||
| setCurrInputs, | |||
| currConversationInfo, | |||
| setNewConversationInfo, | |||
| existConversationInfo, | |||
| setExistConversationInfo, | |||
| } | |||
| } | |||
| export default useConversation | |||
| @@ -1,831 +0,0 @@ | |||
| /* eslint-disable react-hooks/exhaustive-deps */ | |||
| /* eslint-disable @typescript-eslint/no-use-before-define */ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useEffect, useRef, useState } from 'react' | |||
| import cn from 'classnames' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useContext } from 'use-context-selector' | |||
| import produce from 'immer' | |||
| import { useBoolean, useGetState } from 'ahooks' | |||
| import AppUnavailable from '../../base/app-unavailable' | |||
| import useConversation from './hooks/use-conversation' | |||
| import Init from './init' | |||
| import { ToastContext } from '@/app/components/base/toast' | |||
| import Sidebar from '@/app/components/share/chat/sidebar' | |||
| import { | |||
| delConversation, | |||
| fetchAppParams, | |||
| fetchChatList, | |||
| fetchConversations, | |||
| fetchSuggestedQuestions, | |||
| generationConversationName, | |||
| pinConversation, | |||
| sendChatMessage, | |||
| stopChatMessageResponding, | |||
| unpinConversation, | |||
| updateFeedback, | |||
| } from '@/service/universal-chat' | |||
| import type { ConversationItem, SiteInfo } from '@/models/share' | |||
| import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug' | |||
| import type { Feedbacktype, IChatItem } from '@/app/components/app/chat/type' | |||
| import Chat from '@/app/components/app/chat' | |||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' | |||
| import { userInputsFormToPromptVariables } from '@/utils/model-config' | |||
| import Confirm from '@/app/components/base/confirm' | |||
| import type { DataSet } from '@/models/datasets' | |||
| import ConfigSummary from '@/app/components/explore/universal-chat/config-view/summary' | |||
| import { fetchDatasets } from '@/service/datasets' | |||
| import ItemOperation from '@/app/components/explore/item-operation' | |||
| import { useCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' | |||
| import { useProviderContext } from '@/context/provider-context' | |||
| const APP_ID = 'universal-chat' | |||
| const DEFAULT_PLUGIN = { | |||
| google_search: false, | |||
| web_reader: true, | |||
| wikipedia: true, | |||
| } | |||
| // Old configuration structure is not compatible with the current configuration | |||
| localStorage.removeItem('universal-chat-config') | |||
| const CONFIG_KEY = 'universal-chat-config-2' | |||
| type CONFIG = { | |||
| providerName: string | |||
| modelId: string | |||
| plugin: { | |||
| google_search: boolean | |||
| web_reader: boolean | |||
| wikipedia: boolean | |||
| } | |||
| } | |||
| let prevConfig: null | CONFIG = localStorage.getItem(CONFIG_KEY) ? JSON.parse(localStorage.getItem(CONFIG_KEY) as string) as CONFIG : null | |||
| const setPrevConfig = (config: CONFIG) => { | |||
| prevConfig = config | |||
| localStorage.setItem(CONFIG_KEY, JSON.stringify(prevConfig)) | |||
| } | |||
| export type IMainProps = {} | |||
| const Main: FC<IMainProps> = () => { | |||
| const { t } = useTranslation() | |||
| const media = useBreakpoints() | |||
| const isMobile = media === MediaType.mobile | |||
| const { agentThoughtModelList } = useProviderContext() | |||
| const getInitConfig = (type: 'model' | 'plugin') => { | |||
| if (type === 'model') { | |||
| return { | |||
| providerName: prevConfig?.providerName || agentThoughtModelList[0]?.provider, | |||
| modelId: prevConfig?.modelId || agentThoughtModelList[0]?.models[0]?.model, | |||
| } | |||
| } | |||
| if (type === 'plugin') | |||
| return prevConfig?.plugin || DEFAULT_PLUGIN | |||
| } | |||
| useEffect(() => { | |||
| document.title = `${t('explore.sidebar.chat')} - Dify` | |||
| }, []) | |||
| /* | |||
| * app info | |||
| */ | |||
| const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | |||
| const [isUnknwonReason, setIsUnknwonReason] = useState<boolean>(false) | |||
| const siteInfo: SiteInfo = ( | |||
| { | |||
| title: 'universal Chatbot', | |||
| icon: '', | |||
| icon_background: '', | |||
| description: '', | |||
| default_language: 'en', // TODO | |||
| prompt_public: true, | |||
| } | |||
| ) | |||
| const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null) | |||
| const [inited, setInited] = useState<boolean>(false) | |||
| // in mobile, show sidebar by click button | |||
| const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false) | |||
| /* | |||
| * conversation info | |||
| */ | |||
| const [allConversationList, setAllConversationList] = useState<ConversationItem[]>([]) | |||
| const [isClearConversationList, { setTrue: clearConversationListTrue, setFalse: clearConversationListFalse }] = useBoolean(false) | |||
| const [isClearPinnedConversationList, { setTrue: clearPinnedConversationListTrue, setFalse: clearPinnedConversationListFalse }] = useBoolean(false) | |||
| const { | |||
| conversationList, | |||
| setConversationList, | |||
| pinnedConversationList, | |||
| setPinnedConversationList, | |||
| currConversationId, | |||
| getCurrConversationId, | |||
| setCurrConversationId, | |||
| getConversationIdFromStorage, | |||
| isNewConversation, | |||
| currConversationInfo, | |||
| currInputs, | |||
| newConversationInputs, | |||
| // existConversationInputs, | |||
| resetNewConversationInputs, | |||
| setCurrInputs, | |||
| setNewConversationInfo, | |||
| existConversationInfo, | |||
| setExistConversationInfo, | |||
| } = useConversation() | |||
| const [hasMore, setHasMore] = useState<boolean>(true) | |||
| const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true) | |||
| const onMoreLoaded = ({ data: conversations, has_more }: any) => { | |||
| setHasMore(has_more) | |||
| if (isClearConversationList) { | |||
| setConversationList(conversations) | |||
| clearConversationListFalse() | |||
| } | |||
| else { | |||
| setConversationList([...conversationList, ...conversations]) | |||
| } | |||
| } | |||
| const onPinnedMoreLoaded = ({ data: conversations, has_more }: any) => { | |||
| setHasPinnedMore(has_more) | |||
| if (isClearPinnedConversationList) { | |||
| setPinnedConversationList(conversations) | |||
| clearPinnedConversationListFalse() | |||
| } | |||
| else { | |||
| setPinnedConversationList([...pinnedConversationList, ...conversations]) | |||
| } | |||
| } | |||
| const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0) | |||
| const noticeUpdateList = () => { | |||
| setHasMore(true) | |||
| clearConversationListTrue() | |||
| setHasPinnedMore(true) | |||
| clearPinnedConversationListTrue() | |||
| setControlUpdateConversationList(Date.now()) | |||
| } | |||
| const handlePin = async (id: string) => { | |||
| await pinConversation(id) | |||
| setControlItemOpHide(Date.now()) | |||
| notify({ type: 'success', message: t('common.api.success') }) | |||
| noticeUpdateList() | |||
| } | |||
| const handleUnpin = async (id: string) => { | |||
| await unpinConversation(id) | |||
| setControlItemOpHide(Date.now()) | |||
| notify({ type: 'success', message: t('common.api.success') }) | |||
| noticeUpdateList() | |||
| } | |||
| const [isShowConfirm, { setTrue: showConfirm, setFalse: hideConfirm }] = useBoolean(false) | |||
| const [toDeleteConversationId, setToDeleteConversationId] = useState('') | |||
| const handleDelete = (id: string) => { | |||
| setToDeleteConversationId(id) | |||
| hideSidebar() // mobile | |||
| showConfirm() | |||
| } | |||
| const didDelete = async () => { | |||
| await delConversation(toDeleteConversationId) | |||
| setControlItemOpHide(Date.now()) | |||
| notify({ type: 'success', message: t('common.api.success') }) | |||
| hideConfirm() | |||
| if (currConversationId === toDeleteConversationId) | |||
| handleConversationIdChange('-1') | |||
| noticeUpdateList() | |||
| } | |||
| const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null) | |||
| const [speechToTextConfig, setSpeechToTextConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null) | |||
| const [citationConfig, setCitationConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null) | |||
| const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false) | |||
| const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string | |||
| const conversationIntroduction = currConversationInfo?.introduction || '' | |||
| const handleConversationSwitch = async () => { | |||
| if (!inited) | |||
| return | |||
| // update inputs of current conversation | |||
| let notSyncToStateIntroduction = '' | |||
| let notSyncToStateInputs: Record<string, any> | undefined | null = {} | |||
| // debugger | |||
| if (!isNewConversation) { | |||
| const item = allConversationList.find(item => item.id === currConversationId) as any | |||
| notSyncToStateInputs = item?.inputs || {} | |||
| // setCurrInputs(notSyncToStateInputs) | |||
| notSyncToStateIntroduction = item?.introduction || '' | |||
| setExistConversationInfo({ | |||
| name: item?.name || '', | |||
| introduction: notSyncToStateIntroduction, | |||
| }) | |||
| const modelConfig = item?.model_config | |||
| if (modelConfig) { | |||
| setModeId(modelConfig.model_id) | |||
| const pluginConfig: Record<string, boolean> = {} | |||
| const datasetIds: string[] = [] | |||
| modelConfig.agent_mode.tools.forEach((item: any) => { | |||
| const pluginName = Object.keys(item)[0] | |||
| if (pluginName === 'dataset') | |||
| datasetIds.push(item.dataset.id) | |||
| else | |||
| pluginConfig[pluginName] = item[pluginName].enabled | |||
| }) | |||
| setPlugins(pluginConfig) | |||
| if (datasetIds.length > 0) { | |||
| const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasetIds } }) | |||
| setDateSets(data) | |||
| } | |||
| else { | |||
| setDateSets([]) | |||
| } | |||
| } | |||
| else { | |||
| configSetDefaultValue() | |||
| } | |||
| } | |||
| else { | |||
| configSetDefaultValue() | |||
| notSyncToStateInputs = newConversationInputs | |||
| setCurrInputs(notSyncToStateInputs) | |||
| } | |||
| // update chat list of current conversation | |||
| if (!isNewConversation && !conversationIdChangeBecauseOfNew) { | |||
| fetchChatList(currConversationId).then((res: any) => { | |||
| const { data } = res | |||
| const newChatList: IChatItem[] = generateNewChatListWithOpenstatement(notSyncToStateIntroduction, notSyncToStateInputs) | |||
| data.forEach((item: any) => { | |||
| newChatList.push({ | |||
| id: `question-${item.id}`, | |||
| content: item.query, | |||
| isAnswer: false, | |||
| }) | |||
| newChatList.push({ | |||
| ...item, | |||
| id: item.id, | |||
| content: item.answer, | |||
| feedback: item.feedback, | |||
| isAnswer: true, | |||
| citation: item.retriever_resources, | |||
| }) | |||
| }) | |||
| setChatList(newChatList) | |||
| setErrorHappened(false) | |||
| }) | |||
| } | |||
| if (isNewConversation) { | |||
| setChatList(generateNewChatListWithOpenstatement()) | |||
| setErrorHappened(false) | |||
| } | |||
| setControlFocus(Date.now()) | |||
| } | |||
| useEffect(() => { | |||
| handleConversationSwitch() | |||
| }, [currConversationId, inited]) | |||
| const handleConversationIdChange = (id: string) => { | |||
| if (id === '-1') { | |||
| createNewChat() | |||
| setConversationIdChangeBecauseOfNew(true) | |||
| } | |||
| else { | |||
| setConversationIdChangeBecauseOfNew(false) | |||
| } | |||
| // trigger handleConversationSwitch | |||
| setCurrConversationId(id, APP_ID) | |||
| setIsShowSuggestion(false) | |||
| hideSidebar() | |||
| } | |||
| /* | |||
| * chat info. chat is under conversation. | |||
| */ | |||
| const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([]) | |||
| const chatListDomRef = useRef<HTMLDivElement>(null) | |||
| useEffect(() => { | |||
| // scroll to bottom | |||
| if (chatListDomRef.current) | |||
| chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight | |||
| }, [chatList, currConversationId]) | |||
| // user can not edit inputs if user had send message | |||
| const createNewChat = async () => { | |||
| // if new chat is already exist, do not create new chat | |||
| abortController?.abort() | |||
| setResponsingFalse() | |||
| if (conversationList.some(item => item.id === '-1')) | |||
| return | |||
| setConversationList(produce(conversationList, (draft) => { | |||
| draft.unshift({ | |||
| id: '-1', | |||
| name: t('share.chat.newChatDefaultName'), | |||
| inputs: newConversationInputs, | |||
| introduction: conversationIntroduction, | |||
| }) | |||
| })) | |||
| configSetDefaultValue() | |||
| } | |||
| // sometime introduction is not applied to state | |||
| const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => { | |||
| let caculatedIntroduction = introduction || conversationIntroduction || '' | |||
| const caculatedPromptVariables = inputs || currInputs || null | |||
| if (caculatedIntroduction && caculatedPromptVariables) | |||
| caculatedIntroduction = replaceStringWithValues(caculatedIntroduction, promptConfig?.prompt_variables || [], caculatedPromptVariables) | |||
| const openstatement = { | |||
| id: `${Date.now()}`, | |||
| content: caculatedIntroduction, | |||
| isAnswer: true, | |||
| feedbackDisabled: true, | |||
| isOpeningStatement: true, | |||
| } | |||
| if (caculatedIntroduction) | |||
| return [openstatement] | |||
| return [] | |||
| } | |||
| const fetchAllConversations = () => { | |||
| return fetchConversations(undefined, undefined, 100) | |||
| } | |||
| const fetchInitData = async () => { | |||
| return Promise.all([fetchAllConversations(), fetchAppParams()]) | |||
| } | |||
| // init | |||
| useEffect(() => { | |||
| (async () => { | |||
| try { | |||
| const [conversationData, appParams]: any = await fetchInitData() | |||
| const prompt_template = '' | |||
| // handle current conversation id | |||
| const { data: allConversations } = conversationData as { data: ConversationItem[]; has_more: boolean } | |||
| const _conversationId = getConversationIdFromStorage(APP_ID) | |||
| const isNotNewConversation = allConversations.some(item => item.id === _conversationId) | |||
| setAllConversationList(allConversations) | |||
| // fetch new conversation info | |||
| const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource }: any = appParams | |||
| const prompt_variables = userInputsFormToPromptVariables(user_input_form) | |||
| setNewConversationInfo({ | |||
| name: t('share.chat.newChatDefaultName'), | |||
| introduction, | |||
| }) | |||
| setPromptConfig({ | |||
| prompt_template, | |||
| prompt_variables, | |||
| } as PromptConfig) | |||
| setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer) | |||
| setSpeechToTextConfig(speech_to_text) | |||
| setCitationConfig(retriever_resource) | |||
| if (isNotNewConversation) | |||
| setCurrConversationId(_conversationId, APP_ID, false) | |||
| setInited(true) | |||
| } | |||
| catch (e: any) { | |||
| if (e.status === 404) { | |||
| setAppUnavailable(true) | |||
| } | |||
| else { | |||
| setIsUnknwonReason(true) | |||
| setAppUnavailable(true) | |||
| } | |||
| } | |||
| })() | |||
| }, []) | |||
| const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) | |||
| const [abortController, setAbortController] = useState<AbortController | null>(null) | |||
| const { notify } = useContext(ToastContext) | |||
| const logError = (message: string) => { | |||
| notify({ type: 'error', message }) | |||
| } | |||
| const checkCanSend = () => { | |||
| if (currConversationId !== '-1') | |||
| return true | |||
| const prompt_variables = promptConfig?.prompt_variables | |||
| const inputs = currInputs | |||
| if (!inputs || !prompt_variables || prompt_variables?.length === 0) | |||
| return true | |||
| let hasEmptyInput = false | |||
| const requiredVars = prompt_variables?.filter(({ key, name, required }) => { | |||
| const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) | |||
| return res | |||
| }) || [] // compatible with old version | |||
| requiredVars.forEach(({ key }) => { | |||
| if (hasEmptyInput) | |||
| return | |||
| if (!inputs?.[key]) | |||
| hasEmptyInput = true | |||
| }) | |||
| if (hasEmptyInput) { | |||
| logError(t('appDebug.errorMessage.valueOfVarRequired')) | |||
| return false | |||
| } | |||
| return !hasEmptyInput | |||
| } | |||
| const [controlFocus, setControlFocus] = useState(0) | |||
| const [isShowSuggestion, setIsShowSuggestion] = useState(false) | |||
| const doShowSuggestion = isShowSuggestion && !isResponsing | |||
| const [suggestQuestions, setSuggestQuestions] = useState<string[]>([]) | |||
| const [messageTaskId, setMessageTaskId] = useState('') | |||
| const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false) | |||
| const [errorHappened, setErrorHappened] = useState(false) | |||
| const [isResponsingConIsCurrCon, setIsResponsingConCurrCon, getIsResponsingConIsCurrCon] = useGetState(true) | |||
| const initConfig = getInitConfig('model') | |||
| const [modelId, setModeId] = useState<string>((initConfig as any)?.modelId as string) | |||
| const [providerName, setProviderName] = useState<string>((initConfig as any)?.providerName) | |||
| const { currentModel } = useCurrentProviderAndModel( | |||
| agentThoughtModelList, | |||
| { provider: providerName, model: modelId }, | |||
| ) | |||
| const handleSend = async (message: string) => { | |||
| if (isNewConversation) { | |||
| const isModelSelected = modelId && !!currentModel | |||
| if (!isModelSelected) { | |||
| notify({ type: 'error', message: t('appDebug.errorMessage.notSelectModel') }) | |||
| return | |||
| } | |||
| setPrevConfig({ | |||
| modelId, | |||
| providerName, | |||
| plugin: plugins as any, | |||
| }) | |||
| } | |||
| if (isResponsing) { | |||
| notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) | |||
| return | |||
| } | |||
| const formattedPlugins = Object.keys(plugins).map(key => ({ | |||
| [key]: { | |||
| enabled: plugins[key], | |||
| }, | |||
| })) | |||
| const formattedDataSets = dataSets.map(({ id }) => { | |||
| return { | |||
| dataset: { | |||
| enabled: true, | |||
| id, | |||
| }, | |||
| } | |||
| }) | |||
| const data = { | |||
| query: message, | |||
| conversation_id: isNewConversation ? null : currConversationId, | |||
| model: modelId, | |||
| provider: providerName, | |||
| tools: [...formattedPlugins, ...formattedDataSets], | |||
| } | |||
| // qustion | |||
| const questionId = `question-${Date.now()}` | |||
| const questionItem = { | |||
| id: questionId, | |||
| content: message, | |||
| agent_thoughts: [], | |||
| isAnswer: false, | |||
| } | |||
| const placeholderAnswerId = `answer-placeholder-${Date.now()}` | |||
| const placeholderAnswerItem = { | |||
| id: placeholderAnswerId, | |||
| content: '', | |||
| isAnswer: true, | |||
| } | |||
| const newList = [...getChatList(), questionItem, placeholderAnswerItem] | |||
| setChatList(newList) | |||
| // answer | |||
| const responseItem: IChatItem = { | |||
| id: `${Date.now()}`, | |||
| content: '', | |||
| agent_thoughts: [], | |||
| isAnswer: true, | |||
| } | |||
| const prevTempNewConversationId = getCurrConversationId() || '-1' | |||
| let tempNewConversationId = prevTempNewConversationId | |||
| setHasStopResponded(false) | |||
| setResponsingTrue() | |||
| setErrorHappened(false) | |||
| setIsShowSuggestion(false) | |||
| setIsResponsingConCurrCon(true) | |||
| sendChatMessage(data, { | |||
| getAbortController: (abortController) => { | |||
| setAbortController(abortController) | |||
| }, | |||
| onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { | |||
| responseItem.content = responseItem.content + message | |||
| responseItem.id = messageId | |||
| if (isFirstMessage && newConversationId) | |||
| tempNewConversationId = newConversationId | |||
| setMessageTaskId(taskId) | |||
| // has switched to other conversation | |||
| if (prevTempNewConversationId !== getCurrConversationId()) { | |||
| setIsResponsingConCurrCon(false) | |||
| return | |||
| } | |||
| // closesure new list is outdated. | |||
| const newListWithAnswer = produce( | |||
| getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), | |||
| (draft) => { | |||
| if (!draft.find(item => item.id === questionId)) | |||
| draft.push({ ...questionItem } as any) | |||
| draft.push({ ...responseItem }) | |||
| }) | |||
| setChatList(newListWithAnswer) | |||
| }, | |||
| async onCompleted(hasError?: boolean) { | |||
| if (hasError) { | |||
| setResponsingFalse() | |||
| return | |||
| } | |||
| if (getConversationIdChangeBecauseOfNew()) { | |||
| const { data: allConversations }: any = await fetchAllConversations() | |||
| const newItem: any = await generationConversationName(allConversations[0].id) | |||
| const newAllConversations = produce(allConversations, (draft: any) => { | |||
| draft[0].name = newItem.name | |||
| }) | |||
| setAllConversationList(newAllConversations as any) | |||
| noticeUpdateList() | |||
| } | |||
| setConversationIdChangeBecauseOfNew(false) | |||
| resetNewConversationInputs() | |||
| setCurrConversationId(tempNewConversationId, APP_ID, true) | |||
| if (getIsResponsingConIsCurrCon() && suggestedQuestionsAfterAnswerConfig?.enabled && !getHasStopResponded()) { | |||
| const { data }: any = await fetchSuggestedQuestions(responseItem.id) | |||
| setSuggestQuestions(data) | |||
| setIsShowSuggestion(true) | |||
| } | |||
| setResponsingFalse() | |||
| }, | |||
| onThought(thought) { | |||
| // thought finished then start to return message. Warning: use push agent_thoughts.push would caused problem when the thought is more then 2 | |||
| responseItem.id = thought.message_id; | |||
| (responseItem as any).agent_thoughts = [...(responseItem as any).agent_thoughts, thought] // .push(thought) | |||
| // has switched to other conversation | |||
| if (prevTempNewConversationId !== getCurrConversationId()) { | |||
| setIsResponsingConCurrCon(false) | |||
| return | |||
| } | |||
| const newListWithAnswer = produce( | |||
| getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), | |||
| (draft) => { | |||
| if (!draft.find(item => item.id === questionId)) | |||
| draft.push({ ...questionItem }) | |||
| draft.push({ ...responseItem }) | |||
| }) | |||
| setChatList(newListWithAnswer) | |||
| }, | |||
| onMessageEnd: (messageEnd) => { | |||
| responseItem.citation = messageEnd.metadata?.retriever_resources | |||
| const newListWithAnswer = produce( | |||
| getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), | |||
| (draft) => { | |||
| if (!draft.find(item => item.id === questionId)) | |||
| draft.push({ ...questionItem }) | |||
| draft.push({ ...responseItem }) | |||
| }) | |||
| setChatList(newListWithAnswer) | |||
| }, | |||
| onError() { | |||
| setErrorHappened(true) | |||
| // role back placeholder answer | |||
| setChatList(produce(getChatList(), (draft) => { | |||
| draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) | |||
| })) | |||
| setResponsingFalse() | |||
| }, | |||
| }) | |||
| } | |||
| const handleFeedback = async (messageId: string, feedback: Feedbacktype) => { | |||
| await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }) | |||
| const newChatList = chatList.map((item) => { | |||
| if (item.id === messageId) { | |||
| return { | |||
| ...item, | |||
| feedback, | |||
| } | |||
| } | |||
| return item | |||
| }) | |||
| setChatList(newChatList) | |||
| notify({ type: 'success', message: t('common.api.success') }) | |||
| } | |||
| const [controlChatUpdateAllConversation, setControlChatUpdateAllConversation] = useState(0) | |||
| useEffect(() => { | |||
| (async () => { | |||
| if (controlChatUpdateAllConversation && !isNewConversation) { | |||
| const { data: allConversations } = await fetchAllConversations() as { data: ConversationItem[]; has_more: boolean } | |||
| const item = allConversations.find(item => item.id === currConversationId) | |||
| setAllConversationList(allConversations) | |||
| if (item) { | |||
| setExistConversationInfo({ | |||
| ...existConversationInfo, | |||
| name: item?.name || '', | |||
| } as any) | |||
| } | |||
| } | |||
| })() | |||
| }, [controlChatUpdateAllConversation]) | |||
| const renderSidebar = () => { | |||
| if (!APP_ID || !promptConfig) | |||
| return null | |||
| return ( | |||
| <Sidebar | |||
| list={conversationList} | |||
| onListChanged={(list) => { | |||
| setConversationList(list) | |||
| setControlChatUpdateAllConversation(Date.now()) | |||
| }} | |||
| isClearConversationList={isClearConversationList} | |||
| pinnedList={pinnedConversationList} | |||
| onPinnedListChanged={(list) => { | |||
| setPinnedConversationList(list) | |||
| setControlChatUpdateAllConversation(Date.now()) | |||
| }} | |||
| isClearPinnedConversationList={isClearPinnedConversationList} | |||
| onMoreLoaded={onMoreLoaded} | |||
| onPinnedMoreLoaded={onPinnedMoreLoaded} | |||
| isNoMore={!hasMore} | |||
| isPinnedNoMore={!hasPinnedMore} | |||
| onCurrentIdChange={handleConversationIdChange} | |||
| currentId={currConversationId} | |||
| copyRight={''} | |||
| isInstalledApp={false} | |||
| isUniversalChat | |||
| installedAppId={''} | |||
| siteInfo={siteInfo} | |||
| onPin={handlePin} | |||
| onUnpin={handleUnpin} | |||
| controlUpdateList={controlUpdateConversationList} | |||
| onDelete={handleDelete} | |||
| onStartChat={() => handleConversationIdChange('-1')} | |||
| /> | |||
| ) | |||
| } | |||
| // const currModel = MODEL_LIST.find(item => item.id === modelId) | |||
| const [plugins, setPlugins] = useState<Record<string, boolean>>(getInitConfig('plugin') as Record<string, boolean>) | |||
| const handlePluginsChange = (key: string, value: boolean) => { | |||
| setPlugins({ | |||
| ...plugins, | |||
| [key]: value, | |||
| }) | |||
| } | |||
| const [dataSets, setDateSets] = useState<DataSet[]>([]) | |||
| const configSetDefaultValue = () => { | |||
| const initConfig = getInitConfig('model') | |||
| setModeId((initConfig as any)?.modelId as string) | |||
| setProviderName((initConfig as any)?.providerName) | |||
| setPlugins(getInitConfig('plugin') as any) | |||
| setDateSets([]) | |||
| } | |||
| const isCurrConversationPinned = !!pinnedConversationList.find(item => item.id === currConversationId) | |||
| const [controlItemOpHide, setControlItemOpHide] = useState(0) | |||
| if (appUnavailable) | |||
| return <AppUnavailable isUnknwonReason={isUnknwonReason} /> | |||
| if (!promptConfig) | |||
| return <Loading type='app' /> | |||
| return ( | |||
| <div className='bg-gray-100 h-full'> | |||
| <div | |||
| className={cn( | |||
| 'flex rounded-t-2xl bg-white overflow-hidden rounded-b-2xl h-full', | |||
| )} | |||
| style={{ | |||
| boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)', | |||
| }} | |||
| > | |||
| {/* sidebar */} | |||
| {!isMobile && renderSidebar()} | |||
| {isMobile && isShowSidebar && ( | |||
| <div className='fixed inset-0 z-50' | |||
| style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }} | |||
| onClick={hideSidebar} | |||
| > | |||
| <div className='inline-block' onClick={e => e.stopPropagation()}> | |||
| {renderSidebar()} | |||
| </div> | |||
| </div> | |||
| )} | |||
| {/* main */} | |||
| <div className={cn( | |||
| 'h-full flex-grow flex flex-col overflow-y-auto', | |||
| ) | |||
| }> | |||
| {(!isNewConversation || isResponsing || errorHappened) && ( | |||
| <div className='mb-5 antialiased font-sans shrink-0 relative mobile:min-h-[48px] tablet:min-h-[64px]'> | |||
| <div className='absolute z-10 top-0 left-0 right-0 flex items-center justify-between border-b border-gray-100 mobile:h-12 tablet:h-16 px-8 bg-white'> | |||
| <div className='text-gray-900'>{conversationName}</div> | |||
| <div className='flex items-center shrink-0 ml-2 space-x-2'> | |||
| <ConfigSummary | |||
| modelId={modelId} | |||
| providerName={providerName} | |||
| plugins={plugins} | |||
| dataSets={dataSets} | |||
| /> | |||
| <div className={cn('flex w-8 h-8 justify-center items-center shrink-0 rounded-lg border border-gray-200')} onClick={e => e.stopPropagation()}> | |||
| <ItemOperation | |||
| key={controlItemOpHide} | |||
| className='!w-8 !h-8' | |||
| isPinned={isCurrConversationPinned} | |||
| togglePin={() => isCurrConversationPinned ? handleUnpin(currConversationId) : handlePin(currConversationId)} | |||
| isShowDelete | |||
| onDelete={() => handleDelete(currConversationId)} | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| )} | |||
| <div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}> | |||
| <div className={cn('pc:w-[794px] max-w-full mobile:w-full mx-auto h-full overflow-y-auto')} ref={chatListDomRef}> | |||
| <Chat | |||
| isShowConfigElem={isNewConversation && chatList.length === 0} | |||
| configElem={<Init | |||
| modelId={modelId} | |||
| providerName={providerName} | |||
| onModelChange={(modelId, providerName) => { | |||
| setModeId(modelId) | |||
| setProviderName(providerName) | |||
| }} | |||
| plugins={plugins} | |||
| onPluginChange={handlePluginsChange} | |||
| dataSets={dataSets} | |||
| onDataSetsChange={setDateSets} | |||
| />} | |||
| chatList={chatList} | |||
| onSend={handleSend} | |||
| isHideFeedbackEdit | |||
| onFeedback={handleFeedback} | |||
| isResponsing={isResponsing} | |||
| canStopResponsing={!!messageTaskId && isResponsingConIsCurrCon} | |||
| abortResponsing={async () => { | |||
| await stopChatMessageResponding(messageTaskId) | |||
| setHasStopResponded(true) | |||
| setResponsingFalse() | |||
| }} | |||
| checkCanSend={checkCanSend} | |||
| controlFocus={controlFocus} | |||
| isShowSuggestion={doShowSuggestion} | |||
| suggestionList={suggestQuestions} | |||
| isShowSpeechToText={speechToTextConfig?.enabled} | |||
| isShowCitation={citationConfig?.enabled} | |||
| dataSets={dataSets} | |||
| /> | |||
| </div> | |||
| </div> | |||
| {isShowConfirm && ( | |||
| <Confirm | |||
| title={t('share.chat.deleteConversation.title')} | |||
| content={t('share.chat.deleteConversation.content')} | |||
| isShow={isShowConfirm} | |||
| onClose={hideConfirm} | |||
| onConfirm={didDelete} | |||
| onCancel={hideConfirm} | |||
| /> | |||
| )} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Main) | |||
| @@ -1,43 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import cn from 'classnames' | |||
| import type { IConfigProps } from '../config' | |||
| import Config from '../config' | |||
| import s from './style.module.css' | |||
| const Line = ( | |||
| <svg width="100%" height="1" viewBox="0 0 720 1" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <line y1="0.5" x2="720" y2="0.5" stroke="url(#paint0_linear_6845_53470)"/> | |||
| <defs> | |||
| <linearGradient id="paint0_linear_6845_53470" x1="0" y1="1" x2="720" y2="1" gradientUnits="userSpaceOnUse"> | |||
| <stop stopColor="#F2F4F7" stopOpacity="0"/> | |||
| <stop offset="0.491667" stopColor="#F2F4F7"/> | |||
| <stop offset="1" stopColor="#F2F4F7" stopOpacity="0"/> | |||
| </linearGradient> | |||
| </defs> | |||
| </svg> | |||
| ) | |||
| const Init: FC<IConfigProps> = ({ | |||
| ...configProps | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className='h-full flex items-center justify-center'> | |||
| <div> | |||
| <div className='text-center'> | |||
| <div className={cn(s.textGradient, 'mb-2 leading-[32px] font-semibold text-[24px]')}>{t('explore.universalChat.welcome')}</div> | |||
| <div className='mb-2 font-normal text-sm text-gray-500'>{t('explore.universalChat.welcomeDescribe')}</div> | |||
| </div> | |||
| <div className='flex mb-2 h-8 items-center'> | |||
| {Line} | |||
| </div> | |||
| <Config {...configProps} /> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Init) | |||
| @@ -1,9 +0,0 @@ | |||
| .textGradient { | |||
| background: linear-gradient(to right, rgba(16, 74, 225, 1) 0, rgba(0, 152, 238, 1) 100%); | |||
| -webkit-background-clip: text; | |||
| -webkit-text-fill-color: transparent; | |||
| background-clip: text; | |||
| text-fill-color: transparent; | |||
| } | |||
| @@ -58,7 +58,6 @@ export type IMainProps = { | |||
| isInstalledApp?: boolean | |||
| installedAppInfo?: InstalledApp | |||
| isSupportPlugin?: boolean | |||
| isUniversalChat?: boolean | |||
| } | |||
| const Main: FC<IMainProps> = ({ | |||
| @@ -11,7 +11,6 @@ import AppInfo from '@/app/components/share/chat/sidebar/app-info' | |||
| // import Card from './card' | |||
| import type { ConversationItem, SiteInfo } from '@/models/share' | |||
| import { fetchConversations } from '@/service/share' | |||
| import { fetchConversations as fetchUniversalConversations } from '@/service/universal-chat' | |||
| export type ISidebarProps = { | |||
| copyRight: string | |||
| @@ -25,7 +24,6 @@ export type ISidebarProps = { | |||
| isClearPinnedConversationList: boolean | |||
| isInstalledApp: boolean | |||
| installedAppId?: string | |||
| isUniversalChat?: boolean | |||
| siteInfo: SiteInfo | |||
| onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void | |||
| onPinnedMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void | |||
| @@ -50,7 +48,6 @@ const Sidebar: FC<ISidebarProps> = ({ | |||
| isClearPinnedConversationList, | |||
| isInstalledApp, | |||
| installedAppId, | |||
| isUniversalChat, | |||
| siteInfo, | |||
| onMoreLoaded, | |||
| onPinnedMoreLoaded, | |||
| @@ -66,13 +63,7 @@ const Sidebar: FC<ISidebarProps> = ({ | |||
| const [hasPinned, setHasPinned] = useState(false) | |||
| const checkHasPinned = async () => { | |||
| let res: any | |||
| if (isUniversalChat) | |||
| res = await fetchUniversalConversations(undefined, true) | |||
| else | |||
| res = await fetchConversations(isInstalledApp, installedAppId, undefined, true) | |||
| const res = await fetchConversations(isInstalledApp, installedAppId, undefined, true) as any | |||
| setHasPinned(res.data.length > 0) | |||
| } | |||
| @@ -85,13 +76,13 @@ const Sidebar: FC<ISidebarProps> = ({ | |||
| checkHasPinned() | |||
| }, [controlUpdateList]) | |||
| const maxListHeight = (isInstalledApp || isUniversalChat) ? 'max-h-[30vh]' : 'max-h-[40vh]' | |||
| const maxListHeight = (isInstalledApp) ? 'max-h-[30vh]' : 'max-h-[40vh]' | |||
| return ( | |||
| <div | |||
| className={ | |||
| cn( | |||
| (isInstalledApp || isUniversalChat) ? 'tablet:h-[calc(100vh_-_74px)]' : '', | |||
| (isInstalledApp) ? 'tablet:h-[calc(100vh_-_74px)]' : '', | |||
| 'shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 mobile:h-screen', | |||
| ) | |||
| } | |||
| @@ -125,7 +116,6 @@ const Sidebar: FC<ISidebarProps> = ({ | |||
| isClearConversationList={isClearPinnedConversationList} | |||
| isInstalledApp={isInstalledApp} | |||
| installedAppId={installedAppId} | |||
| isUniversalChat={isUniversalChat} | |||
| onMoreLoaded={onPinnedMoreLoaded} | |||
| isNoMore={isPinnedNoMore} | |||
| isPinned={true} | |||
| @@ -149,7 +139,6 @@ const Sidebar: FC<ISidebarProps> = ({ | |||
| isClearConversationList={isClearConversationList} | |||
| isInstalledApp={isInstalledApp} | |||
| installedAppId={installedAppId} | |||
| isUniversalChat={isUniversalChat} | |||
| onMoreLoaded={onMoreLoaded} | |||
| isNoMore={isNoMore} | |||
| isPinned={false} | |||
| @@ -160,11 +149,9 @@ const Sidebar: FC<ISidebarProps> = ({ | |||
| </div> | |||
| </div> | |||
| {!isUniversalChat && ( | |||
| <div className="flex flex-shrink-0 pr-4 pb-4 pl-4"> | |||
| <div className="text-gray-400 font-normal text-xs">© {copyRight} {(new Date()).getFullYear()}</div> | |||
| </div> | |||
| )} | |||
| <div className="flex flex-shrink-0 pr-4 pb-4 pl-4"> | |||
| <div className="text-gray-400 font-normal text-xs">© {copyRight} {(new Date()).getFullYear()}</div> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -9,7 +9,6 @@ import RenameModal from '../rename-modal' | |||
| import Item from './item' | |||
| import type { ConversationItem } from '@/models/share' | |||
| import { fetchConversations, renameConversation } from '@/service/share' | |||
| import { fetchConversations as fetchUniversalConversations, renameConversation as renameUniversalConversation } from '@/service/universal-chat' | |||
| import Toast from '@/app/components/base/toast' | |||
| export type IListProps = { | |||
| @@ -20,7 +19,6 @@ export type IListProps = { | |||
| onListChanged?: (newList: ConversationItem[]) => void | |||
| isClearConversationList: boolean | |||
| isInstalledApp: boolean | |||
| isUniversalChat?: boolean | |||
| installedAppId?: string | |||
| onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void | |||
| isNoMore: boolean | |||
| @@ -38,7 +36,6 @@ const List: FC<IListProps> = ({ | |||
| onListChanged, | |||
| isClearConversationList, | |||
| isInstalledApp, | |||
| isUniversalChat, | |||
| installedAppId, | |||
| onMoreLoaded, | |||
| isNoMore, | |||
| @@ -56,11 +53,7 @@ const List: FC<IListProps> = ({ | |||
| let lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined | |||
| if (lastId === '-1') | |||
| lastId = undefined | |||
| let res: any | |||
| if (isUniversalChat) | |||
| res = await fetchUniversalConversations(lastId, isPinned) | |||
| else | |||
| res = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned) | |||
| const res = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned) as any | |||
| const { data: conversations, has_more }: any = res | |||
| onMoreLoaded({ data: conversations, has_more }) | |||
| } | |||
| @@ -93,11 +86,7 @@ const List: FC<IListProps> = ({ | |||
| setIsSaving() | |||
| const currId = currentConversation.id | |||
| try { | |||
| if (isUniversalChat) | |||
| await renameUniversalConversation(currId, newName) | |||
| else | |||
| await renameConversation(isInstalledApp, installedAppId, currId, newName) | |||
| await renameConversation(isInstalledApp, installedAppId, currId, newName) | |||
| Toast.notify({ | |||
| type: 'success', | |||
| @@ -52,8 +52,6 @@ export const MODEL_LIST = [ | |||
| { id: 'claude-instant-1', name: 'claude-instant-1', type: AppType.completion, provider: ProviderType.anthropic }, // set 30k | |||
| { id: 'claude-2', name: 'claude-2', type: AppType.completion, provider: ProviderType.anthropic }, // set 30k | |||
| ] | |||
| const UNIVERSAL_CHAT_MODEL_ID_LIST = ['gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-4', 'claude-2'] | |||
| export const UNIVERSAL_CHAT_MODEL_LIST = MODEL_LIST.filter(({ id, type }) => UNIVERSAL_CHAT_MODEL_ID_LIST.includes(id) && (type === AppType.chat)) | |||
| export const TONE_LIST = [ | |||
| { | |||
| id: 1, | |||
| @@ -36,45 +36,6 @@ const translation = { | |||
| Programming: 'Programming', | |||
| HR: 'HR', | |||
| }, | |||
| universalChat: { | |||
| welcome: 'Start chat with Dify', | |||
| welcomeDescribe: 'Your AI conversation companion for personalized assistance', | |||
| model: 'Model', | |||
| plugins: { | |||
| name: 'Plugins', | |||
| google_search: { | |||
| name: 'Google Search', | |||
| more: { | |||
| left: 'Enable the plugin, ', | |||
| link: 'set up your SerpAPI key', | |||
| right: ' first.', | |||
| }, | |||
| }, | |||
| web_reader: { | |||
| name: 'Web Reader', | |||
| description: 'Get needed information from any web link', | |||
| }, | |||
| wikipedia: { | |||
| name: 'Wikipedia', | |||
| }, | |||
| }, | |||
| thought: { | |||
| show: 'Show', | |||
| hide: 'Hide', | |||
| processOfThought: ' the process of thinking', | |||
| res: { | |||
| webReader: { | |||
| normal: 'Reading {url}', | |||
| hasPageInfo: 'Reading next page of {url}', | |||
| }, | |||
| google: 'Searching Google {{query}}', | |||
| wikipedia: 'Searching Wikipedia {{query}}', | |||
| dataset: 'Retrieving Knowledge {datasetName}', | |||
| date: 'Searching date', | |||
| }, | |||
| }, | |||
| viewConfigDetailTip: 'In conversation, cannot change above settings', | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -36,45 +36,6 @@ const translation = { | |||
| Programming: 'Programação', | |||
| HR: 'RH', | |||
| }, | |||
| universalChat: { | |||
| welcome: 'Iniciar chat com Dify', | |||
| welcomeDescribe: 'Seu companheiro de conversa de IA para assistência personalizada', | |||
| model: 'Modelo', | |||
| plugins: { | |||
| name: 'Plugins', | |||
| google_search: { | |||
| name: 'Pesquisa do Google', | |||
| more: { | |||
| left: 'Ative o plugin, ', | |||
| link: 'configure sua chave SerpAPI', | |||
| right: ' primeiro.', | |||
| }, | |||
| }, | |||
| web_reader: { | |||
| name: 'Leitor da Web', | |||
| description: 'Obtenha informações necessárias de qualquer link da web', | |||
| }, | |||
| wikipedia: { | |||
| name: 'Wikipedia', | |||
| }, | |||
| }, | |||
| thought: { | |||
| show: 'Mostrar', | |||
| hide: 'Ocultar', | |||
| processOfThought: ' o processo de pensamento', | |||
| res: { | |||
| webReader: { | |||
| normal: 'Lendo {url}', | |||
| hasPageInfo: 'Lendo próxima página de {url}', | |||
| }, | |||
| google: 'Pesquisando no Google {{query}}', | |||
| wikipedia: 'Pesquisando na Wikipedia {{query}}', | |||
| dataset: 'Recuperando Conhecimento {datasetName}', | |||
| date: 'Pesquisando data', | |||
| }, | |||
| }, | |||
| viewConfigDetailTip: 'Na conversa, não é possível alterar as configurações acima', | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -36,45 +36,6 @@ const translation = { | |||
| Programming: '编程', | |||
| HR: '人力资源', | |||
| }, | |||
| universalChat: { | |||
| welcome: '开始和 Dify 聊天吧', | |||
| welcomeDescribe: '您的 AI 对话伴侣,为您提供个性化的帮助', | |||
| model: '模型', | |||
| plugins: { | |||
| name: '插件', | |||
| google_search: { | |||
| name: '谷歌搜索', | |||
| more: { | |||
| left: '启用插件,首先', | |||
| link: '设置您的 SerpAPI 密钥', | |||
| right: '', | |||
| }, | |||
| }, | |||
| web_reader: { | |||
| name: '解析链接', | |||
| description: '从任何网页链接获取所需信息', | |||
| }, | |||
| wikipedia: { | |||
| name: '维基百科', | |||
| }, | |||
| }, | |||
| thought: { | |||
| show: '显示', | |||
| hide: '隐藏', | |||
| processOfThought: '思考过程', | |||
| res: { | |||
| webReader: { | |||
| normal: '解析链接 {url}', | |||
| hasPageInfo: '解析链接 {url} 的下一页', | |||
| }, | |||
| google: '搜索谷歌 {{query}}', | |||
| wikipedia: '搜索维基百科 {{query}}', | |||
| dataset: '检索知识库 {datasetName}', | |||
| date: '查询日期', | |||
| }, | |||
| }, | |||
| viewConfigDetailTip: '在对话中,无法更改上述设置', | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -1,84 +0,0 @@ | |||
| import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd, IOnThought } from './base' | |||
| import { | |||
| del, get, patch, post, ssePost, | |||
| } from './base' | |||
| import type { Feedbacktype } from '@/app/components/app/chat/type' | |||
| const baseUrl = 'universal-chat' | |||
| function getUrl(url: string) { | |||
| return `${baseUrl}/${url.startsWith('/') ? url.slice(1) : url}` | |||
| } | |||
| export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, onThought, onMessageEnd, getAbortController }: { | |||
| onData: IOnData | |||
| onCompleted: IOnCompleted | |||
| onError: IOnError | |||
| onThought: IOnThought | |||
| onMessageEnd: IOnMessageEnd | |||
| getAbortController?: (abortController: AbortController) => void | |||
| }) => { | |||
| return ssePost(getUrl('messages'), { | |||
| body: { | |||
| ...body, | |||
| response_mode: 'streaming', | |||
| }, | |||
| }, { onData, onCompleted, onThought, onError, getAbortController, onMessageEnd }) | |||
| } | |||
| export const stopChatMessageResponding = async (taskId: string) => { | |||
| return post(getUrl(`messages/${taskId}/stop`)) | |||
| } | |||
| export const fetchConversations = async (last_id?: string, pinned?: boolean, limit?: number) => { | |||
| return get(getUrl('conversations'), { params: { ...{ limit: limit || 20 }, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } }) | |||
| } | |||
| export const pinConversation = async (id: string) => { | |||
| return patch(getUrl(`conversations/${id}/pin`)) | |||
| } | |||
| export const unpinConversation = async (id: string) => { | |||
| return patch(getUrl(`conversations/${id}/unpin`)) | |||
| } | |||
| export const delConversation = async (id: string) => { | |||
| return del(getUrl(`conversations/${id}`)) | |||
| } | |||
| export const renameConversation = async (id: string, name: string) => { | |||
| return post(getUrl(`conversations/${id}/name`), { body: { name } }) | |||
| } | |||
| export const generationConversationName = async (id: string) => { | |||
| return post(getUrl(`conversations/${id}/name`), { body: { auto_generate: true } }) | |||
| } | |||
| export const fetchChatList = async (conversationId: string) => { | |||
| return get(getUrl('messages'), { params: { conversation_id: conversationId, limit: 20, last_id: '' } }) | |||
| } | |||
| // init value. wait for server update | |||
| export const fetchAppParams = async () => { | |||
| return get(getUrl('parameters')) | |||
| } | |||
| export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => { | |||
| return post(getUrl(url), { body }) | |||
| } | |||
| export const fetchMoreLikeThis = async (messageId: string) => { | |||
| return get(getUrl(`/messages/${messageId}/more-like-this`), { | |||
| params: { | |||
| response_mode: 'blocking', | |||
| }, | |||
| }) | |||
| } | |||
| export const fetchSuggestedQuestions = (messageId: string) => { | |||
| return get(getUrl(`/messages/${messageId}/suggested-questions`)) | |||
| } | |||
| export const audioToText = (url: string, body: FormData) => { | |||
| return post(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ text: string }> | |||
| } | |||