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
.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' ...@@ -11,6 +11,7 @@ import AppInfo from '@/app/components/share/chat/sidebar/app-info'
// 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
...@@ -22,6 +23,7 @@ export type ISidebarProps = { ...@@ -22,6 +23,7 @@ export type ISidebarProps = {
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
...@@ -43,6 +45,7 @@ const Sidebar: FC<ISidebarProps> = ({ ...@@ -43,6 +45,7 @@ const Sidebar: FC<ISidebarProps> = ({
isClearPinnedConversationList, isClearPinnedConversationList,
isInstalledApp, isInstalledApp,
installedAppId, installedAppId,
isUniversalChat,
siteInfo, siteInfo,
onMoreLoaded, onMoreLoaded,
onPinnedMoreLoaded, onPinnedMoreLoaded,
...@@ -57,8 +60,14 @@ const Sidebar: FC<ISidebarProps> = ({ ...@@ -57,8 +60,14 @@ const Sidebar: FC<ISidebarProps> = ({
const [hasPinned, setHasPinned] = useState(false) const [hasPinned, setHasPinned] = useState(false)
const checkHasPinned = async () => { const checkHasPinned = async () => {
const { data }: any = await fetchConversations(isInstalledApp, installedAppId, undefined, true) let res: any
setHasPinned(data.length > 0) if (isUniversalChat)
res = await fetchUniversalConversations(undefined, true)
else
res = await fetchConversations(isInstalledApp, installedAppId, undefined, true)
setHasPinned(res.data.length > 0)
} }
useEffect(() => { useEffect(() => {
...@@ -109,6 +118,7 @@ const Sidebar: FC<ISidebarProps> = ({ ...@@ -109,6 +118,7 @@ const Sidebar: FC<ISidebarProps> = ({
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}
...@@ -131,6 +141,7 @@ const Sidebar: FC<ISidebarProps> = ({ ...@@ -131,6 +141,7 @@ const Sidebar: FC<ISidebarProps> = ({
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}
...@@ -141,9 +152,11 @@ const Sidebar: FC<ISidebarProps> = ({ ...@@ -141,9 +152,11 @@ const Sidebar: FC<ISidebarProps> = ({
</div> </div>
</div> </div>
<div className="flex flex-shrink-0 pr-4 pb-4 pl-4"> {!isUniversalChat && (
<div className="text-gray-400 font-normal text-xs">© {copyRight} {(new Date()).getFullYear()}</div> <div className="flex flex-shrink-0 pr-4 pb-4 pl-4">
</div> <div className="text-gray-400 font-normal text-xs">© {copyRight} {(new Date()).getFullYear()}</div>
</div>
)}
</div> </div>
) )
} }
......
...@@ -10,6 +10,7 @@ import cn from 'classnames' ...@@ -10,6 +10,7 @@ import cn from 'classnames'
import s from './style.module.css' import s from './style.module.css'
import type { ConversationItem } from '@/models/share' import type { ConversationItem } from '@/models/share'
import { fetchConversations } from '@/service/share' import { fetchConversations } from '@/service/share'
import { fetchConversations as fetchUniversalConversations } from '@/service/universal-chat'
import ItemOperation from '@/app/components/explore/item-operation' import ItemOperation from '@/app/components/explore/item-operation'
export type IListProps = { export type IListProps = {
...@@ -19,6 +20,7 @@ export type IListProps = { ...@@ -19,6 +20,7 @@ export type IListProps = {
list: ConversationItem[] list: ConversationItem[]
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
...@@ -35,6 +37,7 @@ const List: FC<IListProps> = ({ ...@@ -35,6 +37,7 @@ const List: FC<IListProps> = ({
list, list,
isClearConversationList, isClearConversationList,
isInstalledApp, isInstalledApp,
isUniversalChat,
installedAppId, installedAppId,
onMoreLoaded, onMoreLoaded,
isNoMore, isNoMore,
...@@ -49,7 +52,12 @@ const List: FC<IListProps> = ({ ...@@ -49,7 +52,12 @@ const List: FC<IListProps> = ({
async () => { async () => {
if (!isNoMore) { if (!isNoMore) {
const lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined 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 }) onMoreLoaded({ data: conversations, has_more })
} }
return { list: [] } return { list: [] }
......
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useState, useEffect } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import TemplateVarPanel, { PanelTitle, VarOpBtnGroup } from '../value-panel'
import s from './style.module.css' import s from './style.module.css'
import { AppInfo, ChatBtn, EditBtn, FootLogo, PromptTemplate } from './massive-component'
import type { SiteInfo } from '@/models/share' import type { SiteInfo } from '@/models/share'
import type { PromptConfig } from '@/models/debug' import type { PromptConfig } from '@/models/debug'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import Select from '@/app/components/base/select' import Select from '@/app/components/base/select'
import { DEFAULT_VALUE_MAX_LEN } from '@/config' 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 // regex to match the {{}} and replace it with a span
const regex = /\{\{([^}]+)\}\}/g const regex = /\{\{([^}]+)\}\}/g
...@@ -25,7 +25,7 @@ export type IWelcomeProps = { ...@@ -25,7 +25,7 @@ export type IWelcomeProps = {
canEidtInpus: boolean canEidtInpus: boolean
savedInputs: Record<string, any> savedInputs: Record<string, any>
onInputsChange: (inputs: Record<string, any>) => void onInputsChange: (inputs: Record<string, any>) => void
plan: string plan?: string
} }
const Welcome: FC<IWelcomeProps> = ({ const Welcome: FC<IWelcomeProps> = ({
...@@ -38,15 +38,15 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -38,15 +38,15 @@ const Welcome: FC<IWelcomeProps> = ({
onStartChat, onStartChat,
canEidtInpus, canEidtInpus,
savedInputs, savedInputs,
onInputsChange onInputsChange,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const hasVar = promptConfig.prompt_variables.length > 0 const hasVar = promptConfig.prompt_variables.length > 0
const [isFold, setIsFold] = useState<boolean>(true) const [isFold, setIsFold] = useState<boolean>(true)
const [inputs, setInputs] = useState<Record<string, any>>((() => { const [inputs, setInputs] = useState<Record<string, any>>((() => {
if (hasSetInputs) { if (hasSetInputs)
return savedInputs return savedInputs
}
const res: Record<string, any> = {} const res: Record<string, any> = {}
if (promptConfig) { if (promptConfig) {
promptConfig.prompt_variables.forEach((item) => { promptConfig.prompt_variables.forEach((item) => {
...@@ -65,7 +65,8 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -65,7 +65,8 @@ const Welcome: FC<IWelcomeProps> = ({
}) })
} }
setInputs(res) setInputs(res)
} else { }
else {
setInputs(savedInputs) setInputs(savedInputs)
} }
}, [savedInputs]) }, [savedInputs])
...@@ -98,24 +99,26 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -98,24 +99,26 @@ const Welcome: FC<IWelcomeProps> = ({
{promptConfig.prompt_variables.map(item => ( {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}> <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> <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' ? ( {item.type === 'select'
<Select ? (
className='w-full' <Select
defaultValue={inputs?.[item.key]} className='w-full'
onSelect={(i) => { setInputs({ ...inputs, [item.key]: i.value }) }} defaultValue={inputs?.[item.key]}
items={(item.options || []).map(i => ({ name: i, value: i }))} onSelect={(i) => { setInputs({ ...inputs, [item.key]: i.value }) }}
allowSearch={false} items={(item.options || []).map(i => ({ name: i, value: i }))}
bgClassName='bg-gray-50' allowSearch={false}
/> bgClassName='bg-gray-50'
) : ( />
<input )
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`} : (
value={inputs?.[item.key] || ''} <input
onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }} placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
className={`w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50`} value={inputs?.[item.key] || ''}
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN} 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>
))} ))}
</div> </div>
...@@ -124,21 +127,20 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -124,21 +127,20 @@ const Welcome: FC<IWelcomeProps> = ({
const canChat = () => { const canChat = () => {
const prompt_variables = promptConfig?.prompt_variables const prompt_variables = promptConfig?.prompt_variables
if (!inputs || !prompt_variables || prompt_variables?.length === 0) { if (!inputs || !prompt_variables || prompt_variables?.length === 0)
return true return true
}
let hasEmptyInput = false let hasEmptyInput = false
const requiredVars = prompt_variables?.filter(({ key, name, required }) => { const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res return res
}) || [] // compatible with old version }) || [] // compatible with old version
requiredVars.forEach(({ key }) => { requiredVars.forEach(({ key }) => {
if (hasEmptyInput) { if (hasEmptyInput)
return return
}
if (!inputs?.[key]) { if (!inputs?.[key])
hasEmptyInput = true hasEmptyInput = true
}
}) })
if (hasEmptyInput) { if (hasEmptyInput) {
...@@ -149,9 +151,9 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -149,9 +151,9 @@ const Welcome: FC<IWelcomeProps> = ({
} }
const handleChat = () => { const handleChat = () => {
if (!canChat()) { if (!canChat())
return return
}
onStartChat(inputs) onStartChat(inputs)
} }
...@@ -211,9 +213,9 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -211,9 +213,9 @@ const Welcome: FC<IWelcomeProps> = ({
return ( return (
<VarOpBtnGroup <VarOpBtnGroup
onConfirm={() => { onConfirm={() => {
if (!canChat()) { if (!canChat())
return return
}
onInputsChange(inputs) onInputsChange(inputs)
setIsFold(true) setIsFold(true)
}} }}
...@@ -269,9 +271,9 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -269,9 +271,9 @@ const Welcome: FC<IWelcomeProps> = ({
} }
const renderHasSetInputsPrivate = () => { const renderHasSetInputsPrivate = () => {
if (!canEidtInpus || !hasVar) { if (!canEidtInpus || !hasVar)
return null return null
}
return ( return (
<TemplateVarPanel <TemplateVarPanel
isFold={isFold} isFold={isFold}
...@@ -293,9 +295,9 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -293,9 +295,9 @@ const Welcome: FC<IWelcomeProps> = ({
} }
const renderHasSetInputs = () => { const renderHasSetInputs = () => {
if (!isPublicVersion && !canEidtInpus || !hasVar) { if ((!isPublicVersion && !canEidtInpus) || !hasVar)
return null return null
}
return ( return (
<div <div
className='pt-[88px] mb-5' className='pt-[88px] mb-5'
...@@ -312,11 +314,13 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -312,11 +314,13 @@ const Welcome: FC<IWelcomeProps> = ({
{ {
!hasSetInputs && ( !hasSetInputs && (
<div className='mobile:pt-[72px] tablet:pt-[128px] pc:pt-[200px]'> <div className='mobile:pt-[72px] tablet:pt-[128px] pc:pt-[200px]'>
{hasVar ? ( {hasVar
renderVarPanel() ? (
) : ( renderVarPanel()
renderNoVarPanel() )
)} : (
renderNoVarPanel()
)}
</div> </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