Commit b4cf1d34 authored by Joel's avatar Joel

feat: can show chat

parent 9098d099
import { useState } from 'react'
import produce from 'immer'
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] = useState<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,
setCurrConversationId,
getConversationIdFromStorage,
isNewConversation,
currInputs,
newConversationInputs,
existConversationInputs,
resetNewConversationInputs,
setCurrInputs,
currConversationInfo,
setNewConversationInfo,
setExistConversationInfo,
}
}
export default useConversation
/* eslint-disable @typescript-eslint/no-use-before-define */
'use client'
import type { FC } from 'react'
import React 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 s from './style.module.css'
import { ToastContext } from '@/app/components/base/toast'
import Sidebar from '@/app/components/share/chat/sidebar'
import ConfigSence from '@/app/components/share/chat/config-scence'
import {
delConversation,
fetchAppParams,
fetchChatList,
fetchConversations,
fetchSuggestedQuestions,
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'
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'
const APP_ID = 'universal-chat'
const isUniversalChat = true
export type IMainProps = {}
const Main: FC<IMainProps> = () => {
const { t } = useTranslation()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
/*
* 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,
setCurrConversationId,
getConversationIdFromStorage,
isNewConversation,
currConversationInfo,
currInputs,
newConversationInputs,
// existConversationInputs,
resetNewConversationInputs,
setCurrInputs,
setNewConversationInfo,
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)
notify({ type: 'success', message: t('common.api.success') })
noticeUpdateList()
}
const handleUnpin = async (id: string) => {
await unpinConversation(id)
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)
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 [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false)
const handleStartChat = (inputs: Record<string, any>) => {
createNewChat()
setConversationIdChangeBecauseOfNew(true)
setCurrInputs(inputs)
setChatStarted()
// parse variables in introduction
setChatList(generateNewChatListWithOpenstatement('', inputs))
}
const hasSetInputs = (() => {
if (!isNewConversation)
return true
return isChatStarted
})()
const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string
const conversationIntroduction = currConversationInfo?.introduction || ''
const handleConversationSwitch = () => {
if (!inited)
return
// update inputs of current conversation
let notSyncToStateIntroduction = ''
let notSyncToStateInputs: Record<string, any> | undefined | null = {}
if (!isNewConversation) {
const item = allConversationList.find(item => item.id === currConversationId)
notSyncToStateInputs = item?.inputs || {}
setCurrInputs(notSyncToStateInputs)
notSyncToStateIntroduction = item?.introduction || ''
setExistConversationInfo({
name: item?.name || '',
introduction: notSyncToStateIntroduction,
})
}
else {
notSyncToStateInputs = newConversationInputs
setCurrInputs(notSyncToStateInputs)
}
// update chat list of current conversation
if (!isNewConversation && !conversationIdChangeBecauseOfNew && !isResponsing) {
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({
id: item.id,
content: item.answer,
feedback: item.feedback,
isAnswer: true,
})
})
setChatList(newChatList)
})
}
if (isNewConversation && isChatStarted)
setChatList(generateNewChatListWithOpenstatement())
setControlFocus(Date.now())
}
useEffect(handleConversationSwitch, [currConversationId, inited])
const handleConversationIdChange = (id: string) => {
if (id === '-1') {
createNewChat()
setConversationIdChangeBecauseOfNew(true)
}
else {
setConversationIdChangeBecauseOfNew(false)
}
// trigger handleConversationSwitch
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 canEditInpus = !chatList.some(item => item.isAnswer === false) && isNewConversation
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,
})
}))
}
// 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()
// debugger
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 }: 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)
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 handleSend = async (message: string) => {
if (isResponsing) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
return
}
const data = {
inputs: currInputs,
query: message,
conversation_id: isNewConversation ? null : currConversationId,
}
// qustion
const questionId = `question-${Date.now()}`
const questionItem = {
id: questionId,
content: message,
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 = {
id: `${Date.now()}`,
content: '',
isAnswer: true,
}
let tempNewConversationId = ''
setHasStopResponded(false)
setResponsingTrue()
setIsShowSuggestion(false)
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)
// 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 })
draft.push({ ...responseItem })
})
setChatList(newListWithAnswer)
},
async onCompleted(hasError?: boolean) {
setResponsingFalse()
if (hasError)
return
if (getConversationIdChangeBecauseOfNew()) {
const { data: allConversations }: any = await fetchAllConversations()
setAllConversationList(allConversations)
noticeUpdateList()
}
setConversationIdChangeBecauseOfNew(false)
resetNewConversationInputs()
setChatNotStarted()
setCurrConversationId(tempNewConversationId, APP_ID, true)
if (suggestedQuestionsAfterAnswerConfig?.enabled && !getHasStopResponded()) {
const { data }: any = await fetchSuggestedQuestions(responseItem.id)
setSuggestQuestions(data)
setIsShowSuggestion(true)
}
},
onError() {
setResponsingFalse()
// role back placeholder answer
setChatList(produce(getChatList(), (draft) => {
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
}))
},
})
}
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 renderSidebar = () => {
if (!APP_ID || !promptConfig)
return null
return (
<Sidebar
list={conversationList}
isClearConversationList={isClearConversationList}
pinnedList={pinnedConversationList}
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}
/>
)
}
if (appUnavailable)
return <AppUnavailable isUnknwonReason={isUnknwonReason} />
if (!promptConfig)
return <Loading type='app' />
const Chat: FC = () => {
return (
<div>
chat
<div className='bg-gray-100'>
<div
className={cn(
'flex rounded-t-2xl bg-white overflow-hidden rounded-b-2xl',
)}
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(
s.installedApp,
'flex-grow flex flex-col overflow-y-auto',
)
}>
<ConfigSence
conversationName={conversationName}
hasSetInputs={hasSetInputs}
isPublicVersion={true}
siteInfo={siteInfo}
promptConfig={promptConfig}
onStartChat={handleStartChat}
canEidtInpus={canEditInpus}
savedInputs={currInputs as Record<string, any>}
onInputsChange={setCurrInputs}
/>
{
hasSetInputs && (
<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='h-full overflow-y-auto' ref={chatListDomRef}>
<Chat
chatList={chatList}
onSend={handleSend}
isHideFeedbackEdit
onFeedback={handleFeedback}
isResponsing={isResponsing}
canStopResponsing={!!messageTaskId}
abortResponsing={async () => {
await stopChatMessageResponding(APP_ID, messageTaskId)
setHasStopResponded(true)
setResponsingFalse()
}}
checkCanSend={checkCanSend}
controlFocus={controlFocus}
isShowSuggestion={doShowSuggestion}
suggestionList={suggestQuestions}
isShowSpeechToText={speechToTextConfig?.enabled}
/>
</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(Chat)
export default React.memo(Main)
.installedApp {
height: calc(100vh - 74px);
}
\ No newline at end of file
......@@ -11,6 +11,7 @@ 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
......@@ -22,6 +23,7 @@ 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
......@@ -43,6 +45,7 @@ const Sidebar: FC<ISidebarProps> = ({
isClearPinnedConversationList,
isInstalledApp,
installedAppId,
isUniversalChat,
siteInfo,
onMoreLoaded,
onPinnedMoreLoaded,
......@@ -57,8 +60,14 @@ const Sidebar: FC<ISidebarProps> = ({
const [hasPinned, setHasPinned] = useState(false)
const checkHasPinned = async () => {
const { data }: any = await fetchConversations(isInstalledApp, installedAppId, undefined, true)
setHasPinned(data.length > 0)
let res: any
if (isUniversalChat)
res = await fetchUniversalConversations(undefined, true)
else
res = await fetchConversations(isInstalledApp, installedAppId, undefined, true)
setHasPinned(res.data.length > 0)
}
useEffect(() => {
......@@ -109,6 +118,7 @@ const Sidebar: FC<ISidebarProps> = ({
isClearConversationList={isClearPinnedConversationList}
isInstalledApp={isInstalledApp}
installedAppId={installedAppId}
isUniversalChat={isUniversalChat}
onMoreLoaded={onPinnedMoreLoaded}
isNoMore={isPinnedNoMore}
isPinned={true}
......@@ -131,6 +141,7 @@ const Sidebar: FC<ISidebarProps> = ({
isClearConversationList={isClearConversationList}
isInstalledApp={isInstalledApp}
installedAppId={installedAppId}
isUniversalChat={isUniversalChat}
onMoreLoaded={onMoreLoaded}
isNoMore={isNoMore}
isPinned={false}
......@@ -141,9 +152,11 @@ const Sidebar: FC<ISidebarProps> = ({
</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>
{!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>
)
}
......
......@@ -10,6 +10,7 @@ import cn from 'classnames'
import s from './style.module.css'
import type { ConversationItem } from '@/models/share'
import { fetchConversations } from '@/service/share'
import { fetchConversations as fetchUniversalConversations } from '@/service/universal-chat'
import ItemOperation from '@/app/components/explore/item-operation'
export type IListProps = {
......@@ -19,6 +20,7 @@ export type IListProps = {
list: ConversationItem[]
isClearConversationList: boolean
isInstalledApp: boolean
isUniversalChat?: boolean
installedAppId?: string
onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void
isNoMore: boolean
......@@ -35,6 +37,7 @@ const List: FC<IListProps> = ({
list,
isClearConversationList,
isInstalledApp,
isUniversalChat,
installedAppId,
onMoreLoaded,
isNoMore,
......@@ -49,7 +52,12 @@ const List: FC<IListProps> = ({
async () => {
if (!isNoMore) {
const lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned)
let res: any
if (isUniversalChat)
res = await fetchUniversalConversations(lastId, isPinned)
else
res = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned)
const { data: conversations, has_more }: any = res
onMoreLoaded({ data: conversations, has_more })
}
return { list: [] }
......
'use client'
import type { FC } from 'react'
import React, { useState, useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import TemplateVarPanel, { PanelTitle, VarOpBtnGroup } from '../value-panel'
import s from './style.module.css'
import { AppInfo, ChatBtn, EditBtn, FootLogo, PromptTemplate } from './massive-component'
import type { SiteInfo } from '@/models/share'
import type { PromptConfig } from '@/models/debug'
import { ToastContext } from '@/app/components/base/toast'
import Select from '@/app/components/base/select'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import TemplateVarPanel, { PanelTitle, VarOpBtnGroup } from '../value-panel'
import { AppInfo, PromptTemplate, ChatBtn, EditBtn, FootLogo } from './massive-component'
// regex to match the {{}} and replace it with a span
const regex = /\{\{([^}]+)\}\}/g
......@@ -25,7 +25,7 @@ export type IWelcomeProps = {
canEidtInpus: boolean
savedInputs: Record<string, any>
onInputsChange: (inputs: Record<string, any>) => void
plan: string
plan?: string
}
const Welcome: FC<IWelcomeProps> = ({
......@@ -38,15 +38,15 @@ const Welcome: FC<IWelcomeProps> = ({
onStartChat,
canEidtInpus,
savedInputs,
onInputsChange
onInputsChange,
}) => {
const { t } = useTranslation()
const hasVar = promptConfig.prompt_variables.length > 0
const [isFold, setIsFold] = useState<boolean>(true)
const [inputs, setInputs] = useState<Record<string, any>>((() => {
if (hasSetInputs) {
if (hasSetInputs)
return savedInputs
}
const res: Record<string, any> = {}
if (promptConfig) {
promptConfig.prompt_variables.forEach((item) => {
......@@ -65,7 +65,8 @@ const Welcome: FC<IWelcomeProps> = ({
})
}
setInputs(res)
} else {
}
else {
setInputs(savedInputs)
}
}, [savedInputs])
......@@ -98,24 +99,26 @@ const Welcome: FC<IWelcomeProps> = ({
{promptConfig.prompt_variables.map(item => (
<div className='tablet:flex tablet:!h-9 mobile:space-y-2 tablet:space-y-0 mobile:text-xs tablet:text-sm' key={item.key}>
<label className={`flex-shrink-0 flex items-center mobile:text-gray-700 tablet:text-gray-900 mobile:font-medium pc:font-normal ${s.formLabel}`}>{item.name}</label>
{item.type === 'select' ? (
<Select
className='w-full'
defaultValue={inputs?.[item.key]}
onSelect={(i) => { setInputs({ ...inputs, [item.key]: i.value }) }}
items={(item.options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
) : (
<input
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs?.[item.key] || ''}
onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
className={`w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50`}
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
{item.type === 'select'
? (
<Select
className='w-full'
defaultValue={inputs?.[item.key]}
onSelect={(i) => { setInputs({ ...inputs, [item.key]: i.value }) }}
items={(item.options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
)
: (
<input
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs?.[item.key] || ''}
onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'}
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
</div>
))}
</div>
......@@ -124,21 +127,20 @@ const Welcome: FC<IWelcomeProps> = ({
const canChat = () => {
const prompt_variables = promptConfig?.prompt_variables
if (!inputs || !prompt_variables || prompt_variables?.length === 0) {
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) {
if (hasEmptyInput)
return
}
if (!inputs?.[key]) {
if (!inputs?.[key])
hasEmptyInput = true
}
})
if (hasEmptyInput) {
......@@ -149,9 +151,9 @@ const Welcome: FC<IWelcomeProps> = ({
}
const handleChat = () => {
if (!canChat()) {
if (!canChat())
return
}
onStartChat(inputs)
}
......@@ -211,9 +213,9 @@ const Welcome: FC<IWelcomeProps> = ({
return (
<VarOpBtnGroup
onConfirm={() => {
if (!canChat()) {
if (!canChat())
return
}
onInputsChange(inputs)
setIsFold(true)
}}
......@@ -269,9 +271,9 @@ const Welcome: FC<IWelcomeProps> = ({
}
const renderHasSetInputsPrivate = () => {
if (!canEidtInpus || !hasVar) {
if (!canEidtInpus || !hasVar)
return null
}
return (
<TemplateVarPanel
isFold={isFold}
......@@ -293,9 +295,9 @@ const Welcome: FC<IWelcomeProps> = ({
}
const renderHasSetInputs = () => {
if (!isPublicVersion && !canEidtInpus || !hasVar) {
if ((!isPublicVersion && !canEidtInpus) || !hasVar)
return null
}
return (
<div
className='pt-[88px] mb-5'
......@@ -312,11 +314,13 @@ const Welcome: FC<IWelcomeProps> = ({
{
!hasSetInputs && (
<div className='mobile:pt-[72px] tablet:pt-[128px] pc:pt-[200px]'>
{hasVar ? (
renderVarPanel()
) : (
renderNoVarPanel()
)}
{hasVar
? (
renderVarPanel()
)
: (
renderNoVarPanel()
)}
</div>
)
}
......
import type { IOnCompleted, IOnData, IOnError } from './base'
import {
del, get, patch, post, ssePost,
} from './base'
import type { Feedbacktype } from '@/app/components/app/chat'
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, getAbortController }: {
onData: IOnData
onCompleted: IOnCompleted
onError: IOnError
getAbortController?: (abortController: AbortController) => void
}) => {
return ssePost(getUrl('chat-messages'), {
body: {
...body,
response_mode: 'streaming',
},
}, { onData, onCompleted, onError, getAbortController })
}
export const stopChatMessageResponding = async (appId: string, 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 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 }>
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment