| 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) |
| onRemove: (id: string) => void | onRemove: (id: string) => void | ||||
| readonly?: boolean | readonly?: boolean | ||||
| } | } | ||||
| // used in universal-chat | |||||
| const CardItem: FC<ICardItemProps> = ({ | const CardItem: FC<ICardItemProps> = ({ | ||||
| className, | className, | ||||
| config, | config, |
| if (value !== '') { | if (value !== '') { | ||||
| formData.append('text', removeCodeBlocks(value)) | formData.append('text', removeCodeBlocks(value)) | ||||
| let url = '/universal-chat/text-to-audio' | |||||
| let url = '' | |||||
| let isPublic = false | let isPublic = false | ||||
| if (params.token) { | if (params.token) { |
| const formData = new FormData() | const formData = new FormData() | ||||
| formData.append('file', mp3File) | formData.append('file', mp3File) | ||||
| let url = '/universal-chat/audio-to-text' | |||||
| let url = '' | |||||
| let isPublic = false | let isPublic = false | ||||
| if (params.token) { | if (params.token) { |
| '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) |
| .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); | |||||
| } |
| '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) |
| .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) | |||||
| } |
| '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) |
| '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) |
| '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) |
| '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) |
| .shadow { | |||||
| box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); | |||||
| } |
| '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) |
| 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 |
| /* 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) |
| '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) |
| .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; | |||||
| } | |||||
| isInstalledApp?: boolean | isInstalledApp?: boolean | ||||
| installedAppInfo?: InstalledApp | installedAppInfo?: InstalledApp | ||||
| isSupportPlugin?: boolean | isSupportPlugin?: boolean | ||||
| isUniversalChat?: boolean | |||||
| } | } | ||||
| const Main: FC<IMainProps> = ({ | const Main: FC<IMainProps> = ({ |
| // import Card from './card' | // import Card from './card' | ||||
| import type { ConversationItem, SiteInfo } from '@/models/share' | import type { ConversationItem, SiteInfo } from '@/models/share' | ||||
| import { fetchConversations } from '@/service/share' | import { fetchConversations } from '@/service/share' | ||||
| import { fetchConversations as fetchUniversalConversations } from '@/service/universal-chat' | |||||
| export type ISidebarProps = { | export type ISidebarProps = { | ||||
| copyRight: string | copyRight: string | ||||
| isClearPinnedConversationList: boolean | isClearPinnedConversationList: boolean | ||||
| isInstalledApp: boolean | isInstalledApp: boolean | ||||
| installedAppId?: string | installedAppId?: string | ||||
| isUniversalChat?: boolean | |||||
| siteInfo: SiteInfo | siteInfo: SiteInfo | ||||
| onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void | onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void | ||||
| onPinnedMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void | onPinnedMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void | ||||
| isClearPinnedConversationList, | isClearPinnedConversationList, | ||||
| isInstalledApp, | isInstalledApp, | ||||
| installedAppId, | installedAppId, | ||||
| isUniversalChat, | |||||
| siteInfo, | siteInfo, | ||||
| onMoreLoaded, | onMoreLoaded, | ||||
| onPinnedMoreLoaded, | onPinnedMoreLoaded, | ||||
| const [hasPinned, setHasPinned] = useState(false) | const [hasPinned, setHasPinned] = useState(false) | ||||
| const checkHasPinned = async () => { | 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) | setHasPinned(res.data.length > 0) | ||||
| } | } | ||||
| checkHasPinned() | checkHasPinned() | ||||
| }, [controlUpdateList]) | }, [controlUpdateList]) | ||||
| const maxListHeight = (isInstalledApp || isUniversalChat) ? 'max-h-[30vh]' : 'max-h-[40vh]' | |||||
| const maxListHeight = (isInstalledApp) ? 'max-h-[30vh]' : 'max-h-[40vh]' | |||||
| return ( | return ( | ||||
| <div | <div | ||||
| className={ | className={ | ||||
| cn( | 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', | 'shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 mobile:h-screen', | ||||
| ) | ) | ||||
| } | } | ||||
| isClearConversationList={isClearPinnedConversationList} | isClearConversationList={isClearPinnedConversationList} | ||||
| isInstalledApp={isInstalledApp} | isInstalledApp={isInstalledApp} | ||||
| installedAppId={installedAppId} | installedAppId={installedAppId} | ||||
| isUniversalChat={isUniversalChat} | |||||
| onMoreLoaded={onPinnedMoreLoaded} | onMoreLoaded={onPinnedMoreLoaded} | ||||
| isNoMore={isPinnedNoMore} | isNoMore={isPinnedNoMore} | ||||
| isPinned={true} | isPinned={true} | ||||
| isClearConversationList={isClearConversationList} | isClearConversationList={isClearConversationList} | ||||
| isInstalledApp={isInstalledApp} | isInstalledApp={isInstalledApp} | ||||
| installedAppId={installedAppId} | installedAppId={installedAppId} | ||||
| isUniversalChat={isUniversalChat} | |||||
| onMoreLoaded={onMoreLoaded} | onMoreLoaded={onMoreLoaded} | ||||
| isNoMore={isNoMore} | isNoMore={isNoMore} | ||||
| isPinned={false} | isPinned={false} | ||||
| </div> | </div> | ||||
| </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> | </div> | ||||
| ) | ) | ||||
| } | } |
| import Item from './item' | import Item from './item' | ||||
| import type { ConversationItem } from '@/models/share' | import type { ConversationItem } from '@/models/share' | ||||
| import { fetchConversations, renameConversation } from '@/service/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' | import Toast from '@/app/components/base/toast' | ||||
| export type IListProps = { | export type IListProps = { | ||||
| onListChanged?: (newList: ConversationItem[]) => void | onListChanged?: (newList: ConversationItem[]) => void | ||||
| isClearConversationList: boolean | isClearConversationList: boolean | ||||
| isInstalledApp: boolean | isInstalledApp: boolean | ||||
| isUniversalChat?: boolean | |||||
| installedAppId?: string | installedAppId?: string | ||||
| onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void | onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void | ||||
| isNoMore: boolean | isNoMore: boolean | ||||
| onListChanged, | onListChanged, | ||||
| isClearConversationList, | isClearConversationList, | ||||
| isInstalledApp, | isInstalledApp, | ||||
| isUniversalChat, | |||||
| installedAppId, | installedAppId, | ||||
| onMoreLoaded, | onMoreLoaded, | ||||
| isNoMore, | isNoMore, | ||||
| let lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined | let lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined | ||||
| if (lastId === '-1') | if (lastId === '-1') | ||||
| lastId = undefined | 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 | const { data: conversations, has_more }: any = res | ||||
| onMoreLoaded({ data: conversations, has_more }) | onMoreLoaded({ data: conversations, has_more }) | ||||
| } | } | ||||
| setIsSaving() | setIsSaving() | ||||
| const currId = currentConversation.id | const currId = currentConversation.id | ||||
| try { | try { | ||||
| if (isUniversalChat) | |||||
| await renameUniversalConversation(currId, newName) | |||||
| else | |||||
| await renameConversation(isInstalledApp, installedAppId, currId, newName) | |||||
| await renameConversation(isInstalledApp, installedAppId, currId, newName) | |||||
| Toast.notify({ | Toast.notify({ | ||||
| type: 'success', | type: 'success', |
| { id: 'claude-instant-1', name: 'claude-instant-1', type: AppType.completion, provider: ProviderType.anthropic }, // set 30k | { 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 | { 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 = [ | export const TONE_LIST = [ | ||||
| { | { | ||||
| id: 1, | id: 1, |
| Programming: 'Programming', | Programming: 'Programming', | ||||
| HR: 'HR', | 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 | export default translation |
| Programming: 'Programação', | Programming: 'Programação', | ||||
| HR: 'RH', | 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 | export default translation |
| Programming: '编程', | Programming: '编程', | ||||
| HR: '人力资源', | 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 | export default translation |
| 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 }> | |||||
| } |