Commit 04abd4b5 authored by Joel's avatar Joel

feat: converation pin and unpin almost done

parent 17d19612
import { useState } from 'react' import { useState } from 'react'
import type { ConversationItem } from '@/models/share'
import produce from 'immer' import produce from 'immer'
import type { ConversationItem } from '@/models/share'
const storageConversationIdKey = 'conversationIdInfo' const storageConversationIdKey = 'conversationIdInfo'
type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'> type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'>
function useConversation() { function useConversation() {
const [conversationList, setConversationList] = useState<ConversationItem[]>([]) const [conversationList, setConversationList] = useState<ConversationItem[]>([])
const [pinnedConversationList, setPinnedConversationList] = useState<ConversationItem[]>([])
const [currConversationId, doSetCurrConversationId] = useState<string>('-1') const [currConversationId, doSetCurrConversationId] = useState<string>('-1')
// when set conversation id, we do not have set appId // when set conversation id, we do not have set appId
const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => { const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => {
...@@ -29,9 +30,10 @@ function useConversation() { ...@@ -29,9 +30,10 @@ function useConversation() {
// input can be updated by user // input can be updated by user
const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null) const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null)
const resetNewConversationInputs = () => { const resetNewConversationInputs = () => {
if (!newConversationInputs) return if (!newConversationInputs)
setNewConversationInputs(produce(newConversationInputs, draft => { return
Object.keys(draft).forEach(key => { setNewConversationInputs(produce(newConversationInputs, (draft) => {
Object.keys(draft).forEach((key) => {
draft[key] = '' draft[key] = ''
}) })
})) }))
...@@ -48,6 +50,8 @@ function useConversation() { ...@@ -48,6 +50,8 @@ function useConversation() {
return { return {
conversationList, conversationList,
setConversationList, setConversationList,
pinnedConversationList,
setPinnedConversationList,
currConversationId, currConversationId,
setCurrConversationId, setCurrConversationId,
getConversationIdFromStorage, getConversationIdFromStorage,
...@@ -59,8 +63,8 @@ function useConversation() { ...@@ -59,8 +63,8 @@ function useConversation() {
setCurrInputs, setCurrInputs,
currConversationInfo, currConversationInfo,
setNewConversationInfo, setNewConversationInfo,
setExistConversationInfo setExistConversationInfo,
} }
} }
export default useConversation; export default useConversation
\ No newline at end of file
...@@ -14,7 +14,7 @@ import { ToastContext } from '@/app/components/base/toast' ...@@ -14,7 +14,7 @@ import { ToastContext } from '@/app/components/base/toast'
import Sidebar from '@/app/components/share/chat/sidebar' import Sidebar from '@/app/components/share/chat/sidebar'
import ConfigSence from '@/app/components/share/chat/config-scence' import ConfigSence from '@/app/components/share/chat/config-scence'
import Header from '@/app/components/share/header' import Header from '@/app/components/share/header'
import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, sendChatMessage, stopChatMessageResponding, updateFeedback } from '@/service/share' import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, pinConversation, sendChatMessage, stopChatMessageResponding, unpinConversation, updateFeedback } from '@/service/share'
import type { ConversationItem, SiteInfo } from '@/models/share' import type { ConversationItem, SiteInfo } from '@/models/share'
import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug' import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
import type { Feedbacktype, IChatItem } from '@/app/components/app/chat' import type { Feedbacktype, IChatItem } from '@/app/components/app/chat'
...@@ -68,6 +68,8 @@ const Main: FC<IMainProps> = ({ ...@@ -68,6 +68,8 @@ const Main: FC<IMainProps> = ({
const { const {
conversationList, conversationList,
setConversationList, setConversationList,
pinnedConversationList,
setPinnedConversationList,
currConversationId, currConversationId,
setCurrConversationId, setCurrConversationId,
getConversationIdFromStorage, getConversationIdFromStorage,
...@@ -81,11 +83,34 @@ const Main: FC<IMainProps> = ({ ...@@ -81,11 +83,34 @@ const Main: FC<IMainProps> = ({
setNewConversationInfo, setNewConversationInfo,
setExistConversationInfo, setExistConversationInfo,
} = useConversation() } = useConversation()
const [hasMore, setHasMore] = useState<boolean>(false) const [hasMore, setHasMore] = useState<boolean>(true)
const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true)
const onMoreLoaded = ({ data: conversations, has_more }: any) => { const onMoreLoaded = ({ data: conversations, has_more }: any) => {
setHasMore(has_more) setHasMore(has_more)
setConversationList([...conversationList, ...conversations]) setConversationList([...conversationList, ...conversations])
} }
const onPinnedMoreLoaded = ({ data: conversations, has_more }: any) => {
setHasPinnedMore(has_more)
setPinnedConversationList([...pinnedConversationList, ...conversations])
}
const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0)
const noticeUpdateList = () => {
setConversationList([])
setHasMore(true)
setPinnedConversationList([])
setHasPinnedMore(true)
setControlUpdateConversationList(Date.now())
}
const handlePin = async (id: string) => {
await pinConversation(isInstalledApp, installedAppInfo?.id, id)
noticeUpdateList()
}
const handleUnpin = async (id: string) => {
await unpinConversation(isInstalledApp, installedAppInfo?.id, id)
noticeUpdateList()
}
const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null) const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false) const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
...@@ -258,7 +283,7 @@ const Main: FC<IMainProps> = ({ ...@@ -258,7 +283,7 @@ const Main: FC<IMainProps> = ({
const { data: conversations, has_more } = conversationData as { data: ConversationItem[]; has_more: boolean } const { data: conversations, has_more } = conversationData as { data: ConversationItem[]; has_more: boolean }
const _conversationId = getConversationIdFromStorage(appId) const _conversationId = getConversationIdFromStorage(appId)
const isNotNewConversation = conversations.some(item => item.id === _conversationId) const isNotNewConversation = conversations.some(item => item.id === _conversationId)
setHasMore(has_more) // setHasMore(has_more)
// fetch new conversation info // fetch new conversation info
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer }: any = appParams const { user_input_form, opening_statement: introduction, suggested_questions_after_answer }: any = appParams
const prompt_variables = userInputsFormToPromptVariables(user_input_form) const prompt_variables = userInputsFormToPromptVariables(user_input_form)
...@@ -276,7 +301,7 @@ const Main: FC<IMainProps> = ({ ...@@ -276,7 +301,7 @@ const Main: FC<IMainProps> = ({
} as PromptConfig) } as PromptConfig)
setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer) setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer)
setConversationList(conversations as ConversationItem[]) // setConversationList(conversations as ConversationItem[])
if (isNotNewConversation) if (isNotNewConversation)
setCurrConversationId(_conversationId, appId, false) setCurrConversationId(_conversationId, appId, false)
...@@ -403,12 +428,11 @@ const Main: FC<IMainProps> = ({ ...@@ -403,12 +428,11 @@ const Main: FC<IMainProps> = ({
if (hasError) if (hasError)
return return
let currChatList = conversationList
if (getConversationIdChangeBecauseOfNew()) { if (getConversationIdChangeBecauseOfNew()) {
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppInfo?.id) const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppInfo?.id)
setHasMore(has_more) // setHasMore(has_more)
setConversationList(conversations as ConversationItem[]) // setConversationList(conversations as ConversationItem[])
currChatList = conversations setControlUpdateConversationList(Date.now())
} }
setConversationIdChangeBecauseOfNew(false) setConversationIdChangeBecauseOfNew(false)
resetNewConversationInputs() resetNewConversationInputs()
...@@ -451,14 +475,20 @@ const Main: FC<IMainProps> = ({ ...@@ -451,14 +475,20 @@ const Main: FC<IMainProps> = ({
return ( return (
<Sidebar <Sidebar
list={conversationList} list={conversationList}
pinnedList={pinnedConversationList}
onMoreLoaded={onMoreLoaded} onMoreLoaded={onMoreLoaded}
onPinnedMoreLoaded={onPinnedMoreLoaded}
isNoMore={!hasMore} isNoMore={!hasMore}
isPinnedNoMore={!hasPinnedMore}
onCurrentIdChange={handleConversationIdChange} onCurrentIdChange={handleConversationIdChange}
currentId={currConversationId} currentId={currConversationId}
copyRight={siteInfo.copyright || siteInfo.title} copyRight={siteInfo.copyright || siteInfo.title}
isInstalledApp={isInstalledApp} isInstalledApp={isInstalledApp}
installedAppId={installedAppInfo?.id} installedAppId={installedAppInfo?.id}
siteInfo={siteInfo} siteInfo={siteInfo}
onPin={handlePin}
onUnpin={handleUnpin}
controlUpdateList={controlUpdateConversationList}
/> />
) )
} }
......
import React, { useRef } from 'react' import React, { useEffect, useState } from 'react'
import type { FC } from 'react' import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
ChatBubbleOvalLeftEllipsisIcon,
PencilSquareIcon, PencilSquareIcon,
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid' import cn from 'classnames'
import { useInfiniteScroll } from 'ahooks'
import Button from '../../../base/button' import Button from '../../../base/button'
import List from './list'
import AppInfo from '@/app/components/share/chat/sidebar/app-info' 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'
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ')
}
export type ISidebarProps = { export type ISidebarProps = {
copyRight: string copyRight: string
currentId: string currentId: string
onCurrentIdChange: (id: string) => void onCurrentIdChange: (id: string) => void
list: ConversationItem[] list: ConversationItem[]
pinnedList: ConversationItem[]
isInstalledApp: boolean isInstalledApp: boolean
installedAppId?: string installedAppId?: string
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
isNoMore: boolean isNoMore: boolean
isPinnedNoMore: boolean
onPin: (id: string) => void
onUnpin: (id: string) => void
controlUpdateList: number
} }
const Sidebar: FC<ISidebarProps> = ({ const Sidebar: FC<ISidebarProps> = ({
...@@ -34,37 +35,39 @@ const Sidebar: FC<ISidebarProps> = ({ ...@@ -34,37 +35,39 @@ const Sidebar: FC<ISidebarProps> = ({
currentId, currentId,
onCurrentIdChange, onCurrentIdChange,
list, list,
pinnedList,
isInstalledApp, isInstalledApp,
installedAppId, installedAppId,
siteInfo, siteInfo,
onMoreLoaded, onMoreLoaded,
onPinnedMoreLoaded,
isNoMore, isNoMore,
isPinnedNoMore,
onPin,
onUnpin,
controlUpdateList,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const listRef = useRef<HTMLDivElement>(null) const [hasPinned, setHasPinned] = useState(false)
useInfiniteScroll( const checkHasPinned = async () => {
async () => { const { data }: any = await fetchConversations(isInstalledApp, installedAppId, undefined, true)
if (!isNoMore) { setHasPinned(data.length > 0)
const lastId = list[list.length - 1].id }
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId)
onMoreLoaded({ data: conversations, has_more }) useEffect(() => {
} checkHasPinned()
return { list: [] } }, [])
},
{ useEffect(() => {
target: listRef, if (controlUpdateList !== 0)
isNoMore: () => { checkHasPinned()
return isNoMore }, [controlUpdateList])
},
reloadDeps: [isNoMore],
},
)
return ( return (
<div <div
className={ className={
classNames( cn(
isInstalledApp ? 'tablet:h-[calc(100vh_-_74px)]' : 'tablet:h-[calc(100vh_-_3rem)]', isInstalledApp ? 'tablet:h-[calc(100vh_-_74px)]' : 'tablet:h-[calc(100vh_-_3rem)]',
'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',
) )
...@@ -85,40 +88,46 @@ const Sidebar: FC<ISidebarProps> = ({ ...@@ -85,40 +88,46 @@ const Sidebar: FC<ISidebarProps> = ({
<PencilSquareIcon className="mr-2 h-4 w-4" /> {t('share.chat.newChat')} <PencilSquareIcon className="mr-2 h-4 w-4" /> {t('share.chat.newChat')}
</Button> </Button>
</div> </div>
<div className='flex-grow'>
<nav {/* pinned list */}
ref={listRef} {hasPinned && (
className="mt-4 flex-1 space-y-1 bg-white p-4 !pt-0 overflow-y-auto" <div className='mt-4 px-4'>
> <div className='leading-[18px] text-xs text-gray-500 font-medium uppercase'>Pinned</div>
{list.map((item) => { <List
const isCurrent = item.id === currentId className='max-h-[40vh]'
const ItemIcon currentId={currentId}
= isCurrent ? ChatBubbleOvalLeftEllipsisSolidIcon : ChatBubbleOvalLeftEllipsisIcon onCurrentIdChange={onCurrentIdChange}
return ( list={pinnedList}
<div isInstalledApp={isInstalledApp}
onClick={() => onCurrentIdChange(item.id)} installedAppId={installedAppId}
key={item.id} onMoreLoaded={onPinnedMoreLoaded}
className={classNames( isNoMore={isPinnedNoMore}
isCurrent isPinned={true}
? 'bg-primary-50 text-primary-600' onPinChanged={id => onUnpin(id)}
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-700', controlUpdate={controlUpdateList + 1}
'group flex items-center rounded-md px-2 py-2 text-sm font-medium cursor-pointer', />
)} </div>
> )}
<ItemIcon {/* unpinned list */}
className={classNames( <div className='mt-4 px-4'>
isCurrent {hasPinned && (
? 'text-primary-600' <div className='leading-[18px] text-xs text-gray-500 font-medium uppercase'>Chats</div>
: 'text-gray-400 group-hover:text-gray-500', )}
'mr-3 h-5 w-5 flex-shrink-0', <List
)} className={cn(hasPinned ? 'max-h-[40vh]' : 'flex-grow')}
aria-hidden="true" currentId={currentId}
/> onCurrentIdChange={onCurrentIdChange}
{item.name} list={list}
</div> isInstalledApp={isInstalledApp}
) installedAppId={installedAppId}
})} onMoreLoaded={onMoreLoaded}
</nav> isNoMore={isNoMore}
isPinned={false}
onPinChanged={id => onPin(id)}
controlUpdate={controlUpdateList + 1}
/>
</div>
</div>
<div className="flex flex-shrink-0 pr-4 pb-4 pl-4"> <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 className="text-gray-400 font-normal text-xs">© {copyRight} {(new Date()).getFullYear()}</div>
</div> </div>
......
'use client'
import type { FC } from 'react'
import React, { useRef } from 'react'
import {
ChatBubbleOvalLeftEllipsisIcon,
} from '@heroicons/react/24/outline'
import { useInfiniteScroll } from 'ahooks'
import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid'
import cn from 'classnames'
import s from './style.module.css'
import type { ConversationItem } from '@/models/share'
import { fetchConversations } from '@/service/share'
import ItemOperation from '@/app/components/explore/item-operation'
export type IListProps = {
className: string
currentId: string
onCurrentIdChange: (id: string) => void
list: ConversationItem[]
isInstalledApp: boolean
installedAppId?: string
onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void
isNoMore: boolean
isPinned: boolean
onPinChanged: (id: string) => void
controlUpdate: number
}
const List: FC<IListProps> = ({
currentId,
onCurrentIdChange,
list,
isInstalledApp,
installedAppId,
onMoreLoaded,
isNoMore,
isPinned,
onPinChanged,
controlUpdate,
}) => {
const listRef = useRef<HTMLDivElement>(null)
useInfiniteScroll(
async () => {
if (!isNoMore) {
const lastId = list[list.length - 1]?.id
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned)
onMoreLoaded({ data: conversations, has_more })
}
return { list: [] }
},
{
target: listRef,
isNoMore: () => {
return isNoMore
},
reloadDeps: [isNoMore, controlUpdate],
},
)
return (
<nav
ref={listRef}
className="space-y-1 bg-white pb-[60px] overflow-y-auto"
>
{list.map((item) => {
const isCurrent = item.id === currentId
const ItemIcon
= isCurrent ? ChatBubbleOvalLeftEllipsisSolidIcon : ChatBubbleOvalLeftEllipsisIcon
return (
<div
onClick={() => onCurrentIdChange(item.id)}
key={item.id}
className={cn(s.item,
isCurrent
? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:bg-gray-200 hover:text-gray-700',
'group flex justify-between items-center rounded-md px-2 py-2 text-sm font-medium cursor-pointer',
)}
>
<div className='flex items-center w-0 grow'>
<ItemIcon
className={cn(
isCurrent
? 'text-primary-600'
: 'text-gray-400 group-hover:text-gray-500',
'mr-3 h-5 w-5 flex-shrink-0',
)}
aria-hidden="true"
/>
<span>{item.name}</span>
</div>
{
!isCurrent && (
<div className={cn(s.opBtn, 'shrink-0')} onClick={e => e.stopPropagation()}>
<ItemOperation
isPinned={isPinned}
togglePin={() => onPinChanged(item.id)}
isShowDelete={false}
onDelete={() => {}}
/>
</div>
)
}
</div>
)
})}
</nav>
)
}
export default React.memo(List)
.opBtn {
visibility: hidden;
}
.item:hover .opBtn {
visibility: visible;
}
\ No newline at end of file
import type { IOnCompleted, IOnData, IOnError } from './base' import type { IOnCompleted, IOnData, IOnError } from './base'
import { import {
del as consoleDel, get as consoleGet, post as consolePost, del as consoleDel, get as consoleGet, patch as consolePatch, post as consolePost,
delPublic as del, getPublic as get, postPublic as post, ssePost, delPublic as del, getPublic as get, patchPublic as patch, postPublic as post, ssePost,
} from './base' } from './base'
import type { Feedbacktype } from '@/app/components/app/chat' import type { Feedbacktype } from '@/app/components/app/chat'
function getAction(action: 'get' | 'post' | 'del', isInstalledApp: boolean) { function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) {
switch (action) { switch (action) {
case 'get': case 'get':
return isInstalledApp ? consoleGet : get return isInstalledApp ? consoleGet : get
case 'post': case 'post':
return isInstalledApp ? consolePost : post return isInstalledApp ? consolePost : post
case 'patch':
return isInstalledApp ? consolePatch : patch
case 'del': case 'del':
return isInstalledApp ? consoleDel : del return isInstalledApp ? consoleDel : del
} }
...@@ -55,8 +57,17 @@ export const fetchAppInfo = async () => { ...@@ -55,8 +57,17 @@ export const fetchAppInfo = async () => {
return get('/site') return get('/site')
} }
export const fetchConversations = async (isInstalledApp: boolean, installedAppId = '', last_id?: string) => { export const fetchConversations = async (isInstalledApp: boolean, installedAppId = '', last_id?: string, pinned?: boolean) => {
return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: { ...{ limit: 20 }, ...(last_id ? { last_id } : {}) } }) console.log(pinned)
return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: { ...{ limit: 20 }, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } })
}
export const pinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/pin`, isInstalledApp, installedAppId))
}
export const unpinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/unpin`, isInstalledApp, installedAppId))
} }
export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '') => { export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '') => {
......
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