memory in web app

This commit is contained in:
JzoNg 2025-10-12 13:12:50 +08:00
parent 0e545d7be5
commit 61e4bc6b17
20 changed files with 420 additions and 449 deletions

View File

@ -7,6 +7,7 @@ import type {
ChatConfig,
ChatItemInTree,
Feedback,
Memory,
} from '../types'
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
import type {
@ -62,6 +63,12 @@ export type ChatWithHistoryContextValue = {
}
showChatMemory?: boolean
setShowChatMemory: (state: boolean) => void
memoryList: Memory[]
clearAllMemory: () => void
updateMemory: (memory: Memory, content: string) => void
resetDefault: (memory: Memory) => void
clearAllUpdateVersion: (memory: Memory) => void
switchMemoryVersion: (memory: Memory, version: string) => void
}
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
@ -99,5 +106,11 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
initUserVariables: {},
showChatMemory: false,
setShowChatMemory: noop,
memoryList: [],
clearAllMemory: noop,
updateMemory: noop,
resetDefault: noop,
clearAllUpdateVersion: noop,
switchMemoryVersion: noop,
})
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)

View File

@ -21,8 +21,11 @@ import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import {
delConversation,
deleteMemory,
editMemory,
fetchChatList,
fetchConversations,
fetchMemories,
generationConversationName,
pinConversation,
renameConversation,
@ -41,6 +44,9 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { noop } from 'lodash-es'
import { useWebAppStore } from '@/context/web-app-context'
import type { Memory } from '@/app/components/base/chat/types'
import { mockMemoryList } from '@/app/components/base/chat/chat-with-history/memory/mock'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@ -527,6 +533,59 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}, [isInstalledApp, appId, t, notify])
const [showChatMemory, setShowChatMemory] = useState(false)
const [memoryList, setMemoryList] = useState<Memory[]>(mockMemoryList)
const getMemoryList = useCallback(async (currentConversationId: string) => {
const memories = await fetchMemories(currentConversationId, '', '', isInstalledApp, appId)
setMemoryList(memories)
}, [isInstalledApp, appId])
const clearAllMemory = useCallback(async () => {
await deleteMemory('', isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.success') })
getMemoryList(currentConversationId)
}, [currentConversationId, getMemoryList])
const resetDefault = useCallback(async (memory: Memory) => {
try {
await editMemory(memory.spec.id, memory.spec.template, isInstalledApp, appId)
getMemoryList(currentConversationId)
}
catch (error) {
console.error('Failed to reset memory:', error)
}
}, [currentConversationId, getMemoryList, isInstalledApp, appId])
const clearAllUpdateVersion = useCallback(async (memory: Memory) => {
await deleteMemory(memory.spec.id, isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.success') })
getMemoryList(currentConversationId)
}, [currentConversationId, getMemoryList])
const switchMemoryVersion = useCallback(async (memory: Memory, version: string) => {
const memories = await fetchMemories(currentConversationId, memory.spec.id, version, isInstalledApp, appId)
const newMemory = memories[0]
const newList = produce(memoryList, (draft) => {
const index = draft.findIndex(item => item.spec.id === memory.spec.id)
if (index !== -1)
draft[index] = newMemory
})
setMemoryList(newList)
}, [memoryList, currentConversationId, isInstalledApp, appId])
const updateMemory = useCallback(async (memory: Memory, content: string) => {
try {
await editMemory(memory.spec.id, content, isInstalledApp, appId)
getMemoryList(currentConversationId)
}
catch (error) {
console.error('Failed to reset memory:', error)
}
}, [getMemoryList, currentConversationId, isInstalledApp, appId])
useEffect(() => {
getMemoryList(currentConversationId)
}, [currentConversationId, getMemoryList])
return {
isInstalledApp,
@ -576,5 +635,11 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
initUserVariables,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}
}

View File

@ -39,6 +39,12 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
sidebarCollapseState,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
} = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState
const customConfig = appData?.custom_config
@ -101,14 +107,34 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
)}
</div>
{!isMobile && (
<MemoryPanel showChatMemory={showChatMemory} />
<MemoryPanel
isMobile={isMobile}
showChatMemory={showChatMemory}
setShowChatMemory={setShowChatMemory}
memoryList={memoryList}
clearAllMemory={clearAllMemory}
updateMemory={updateMemory}
resetDefault={resetDefault}
clearAllUpdateVersion={clearAllUpdateVersion}
switchMemoryVersion={switchMemoryVersion}
/>
)}
{isMobile && showChatMemory && (
<div className='fixed inset-0 z-50 flex flex-row-reverse bg-background-overlay p-1 backdrop-blur-sm'
onClick={() => setShowChatMemory(false)}
>
<div className='flex h-full w-[360px] rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
<MemoryPanel showChatMemory={showChatMemory} />
<MemoryPanel
isMobile={isMobile}
showChatMemory={showChatMemory}
setShowChatMemory={setShowChatMemory}
memoryList={memoryList}
clearAllMemory={clearAllMemory}
updateMemory={updateMemory}
resetDefault={resetDefault}
clearAllUpdateVersion={clearAllUpdateVersion}
switchMemoryVersion={switchMemoryVersion}
/>
</div>
</div>
)}
@ -169,6 +195,12 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
initUserVariables,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
} = useChatWithHistory(installedAppInfo)
return (
@ -214,6 +246,12 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
initUserVariables,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}}>
<ChatWithHistory className={className} />
</ChatWithHistoryContext.Provider>

View File

@ -1,5 +1,5 @@
'use client'
import React from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
@ -10,14 +10,14 @@ import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider'
import Toast from '@/app/components/base/toast'
import type { MemoryItem } from '../type'
import type { Memory as MemoryItem } from '@/app/components/base/chat/types'
import { noop } from 'lodash-es'
import cn from '@/utils/classnames'
type Props = {
memory: MemoryItem
show: boolean
onConfirm: (info: MemoryItem) => Promise<void>
onConfirm: (info: MemoryItem, content: string) => Promise<void>
onHide: () => void
isMobile?: boolean
}
@ -30,14 +30,25 @@ const MemoryEditModal = ({
isMobile,
}: Props) => {
const { t } = useTranslation()
const [content, setContent] = React.useState(memory.content)
const [content, setContent] = React.useState(memory.value)
const versionTag = useMemo(() => {
const res = `${t('share.chat.memory.updateVersion.update')} ${memory.version}`
if (memory.edited_by_user)
return `${res} · ${t('share.chat.memory.updateVersion.edited')}`
return res
}, [memory.version, t])
const reset = () => {
setContent(memory.value)
}
const submit = () => {
if (!content.trim()) {
Toast.notify({ type: 'error', message: 'content is required' })
return
}
onConfirm({ ...memory, content })
onConfirm(memory, content)
onHide()
}
@ -56,8 +67,8 @@ const MemoryEditModal = ({
<div className='title-2xl-semi-bold mb-2 text-text-primary'>{t('share.chat.memory.editTitle')}</div>
<div className='flex items-center gap-1 pb-1 pt-2'>
<Memory className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-sm-semibold truncate text-text-primary'>{memory.name}</div>
<Badge text={`${t('share.chat.memory.updateVersion.update')} 2`} />
<div className='system-sm-semibold truncate text-text-primary'>{memory.spec.name}</div>
{memory.version > 1 && <Badge text={versionTag} className='!h-4' />}
</div>
</div>
<div className='grow px-4'>
@ -71,7 +82,7 @@ const MemoryEditModal = ({
<Button className='ml-2' variant='primary' onClick={submit}>{t('share.chat.memory.operations.save')}</Button>
<Button className='ml-3' onClick={onHide}>{t('share.chat.memory.operations.cancel')}</Button>
<Divider type='vertical' className='!mx-0 !h-4' />
<Button className='mr-3' onClick={onHide}>{t('share.chat.memory.operations.reset')}</Button>
<Button className='mr-3' onClick={reset}>{t('share.chat.memory.operations.reset')}</Button>
</div>
</div>
</div>
@ -93,8 +104,8 @@ const MemoryEditModal = ({
<div className='title-2xl-semi-bold mb-2 text-text-primary'>{t('share.chat.memory.editTitle')}</div>
<div className='flex items-center gap-1 pb-1 pt-2'>
<Memory className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-sm-semibold truncate text-text-primary'>{memory.name}</div>
<Badge text={`${t('share.chat.memory.updateVersion.update')} 2`} />
<div className='system-sm-semibold truncate text-text-primary'>{memory.spec.name}</div>
{memory.version > 1 && <Badge text={versionTag} />}
</div>
</div>
<div className='px-6'>
@ -108,7 +119,7 @@ const MemoryEditModal = ({
<Button className='ml-2' variant='primary' onClick={submit}>{t('share.chat.memory.operations.save')}</Button>
<Button className='ml-3' onClick={onHide}>{t('share.chat.memory.operations.cancel')}</Button>
<Divider type='vertical' className='!mx-0 !h-4' />
<Button className='mr-3' onClick={onHide}>{t('share.chat.memory.operations.reset')}</Button>
<Button className='mr-3' onClick={reset}>{t('share.chat.memory.operations.reset')}</Button>
</div>
</Modal>
)

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
@ -10,53 +10,100 @@ import Badge from '@/app/components/base/badge'
import Indicator from '@/app/components/header/indicator'
import Operation from './operation'
import MemoryEditModal from './edit-modal'
import type { MemoryItem } from '../type'
import type { Memory as MemoryItem } from '@/app/components/base/chat/types'
import cn from '@/utils/classnames'
type Props = {
memory: MemoryItem
isMobile?: boolean
memory: MemoryItem
updateMemory: (memory: MemoryItem, content: string) => void
resetDefault: (memory: MemoryItem) => void
clearAllUpdateVersion: (memory: MemoryItem) => void
switchMemoryVersion: (memory: MemoryItem, version: string) => void
}
const MemoryCard: React.FC<Props> = ({ memory, isMobile }) => {
const MemoryCard: React.FC<Props> = ({
isMobile,
memory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}) => {
const { t } = useTranslation()
const [isHovering, setIsHovering] = React.useState(false)
const [showEditModal, setShowEditModal] = React.useState(false)
const versionTag = useMemo(() => {
const res = `${t('share.chat.memory.updateVersion.update')} ${memory.version}`
if (memory.edited_by_user)
return `${res} · ${t('share.chat.memory.updateVersion.edited')}`
return res
}, [memory.version, t])
const isLatest = useMemo(() => {
if (memory.conversation_metadata)
return memory.conversation_metadata.visible_count === memory.spec.preserved_turns
return true
}, [memory])
const waitMergeCount = useMemo(() => {
if (memory.conversation_metadata)
return memory.conversation_metadata.visible_count - memory.spec.preserved_turns
return 0
}, [memory])
const prevVersion = () => {
if (memory.version > 1)
switchMemoryVersion(memory, (memory.version - 1).toString())
}
const nextVersion = () => {
switchMemoryVersion(memory, (memory.version + 1).toString())
}
return (
<>
<div
className={cn('group mb-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-md', !memory.status && 'pb-2')}
className={cn('group mb-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-md')}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<div className='flex items-end justify-between pb-1 pl-4 pr-2 pt-2'>
<div className='relative flex items-end justify-between pb-1 pl-4 pr-2 pt-2'>
<div className='flex items-center gap-1 pb-1 pt-2'>
<Memory className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-sm-semibold truncate text-text-primary'>{memory.name}</div>
<Badge text={`${t('share.chat.memory.updateVersion.update')} 2`} />
<div className='system-sm-semibold truncate text-text-primary'>{memory.spec.name}</div>
{memory.version > 1 && <Badge text={versionTag} className='!h-4' />}
</div>
{isHovering && (
<div className='hover:bg-components-actionbar-bg-hover flex items-center gap-0.5 rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md'>
<ActionButton><RiArrowUpSLine className='h-4 w-4' /></ActionButton>
<ActionButton><RiArrowDownSLine className='h-4 w-4' /></ActionButton>
<Operation onEdit={() => {
setShowEditModal(true)
setIsHovering(false)
}} />
<div className='hover:bg-components-actionbar-bg-hover absolute bottom-0 right-2 flex items-center gap-0.5 rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md'>
<ActionButton onClick={prevVersion}><RiArrowUpSLine className='h-4 w-4' /></ActionButton>
<ActionButton onClick={nextVersion}><RiArrowDownSLine className='h-4 w-4' /></ActionButton>
<Operation
memory={memory}
onEdit={() => {
setShowEditModal(true)
setIsHovering(false)
}}
resetDefault={resetDefault}
clearAllUpdateVersion={clearAllUpdateVersion}
switchMemoryVersion={switchMemoryVersion}
/>
</div>
)}
</div>
<div className='system-xs-regular line-clamp-[12] px-4 pb-2 pt-1 text-text-tertiary'>{memory.content}</div>
{memory.status === 'latest' && (
<div className='system-xs-regular line-clamp-[12] px-4 pb-2 pt-1 text-text-tertiary'>{memory.value}</div>
{isLatest && (
<div className='flex items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle bg-background-default-subtle px-4 py-3 group-hover:bg-components-panel-on-panel-item-bg-hover'>
<div className='system-xs-regular text-text-tertiary'>{t('share.chat.memory.latestVersion')}</div>
<Indicator color='green' />
</div>
)}
{memory.status === 'needUpdate' && (
{!isLatest && (
<div className='flex items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle bg-background-default-subtle px-4 py-3 group-hover:bg-components-panel-on-panel-item-bg-hover'>
<div className='system-xs-regular text-text-tertiary'>{t('share.chat.memory.notLatestVersion', { num: memory.mergeCount })}</div>
<div className='system-xs-regular text-text-tertiary'>{t('share.chat.memory.notLatestVersion', { num: waitMergeCount })}</div>
<Indicator color='orange' />
</div>
)}
@ -66,9 +113,8 @@ const MemoryCard: React.FC<Props> = ({ memory, isMobile }) => {
isMobile={isMobile}
show={showEditModal}
memory={memory}
onConfirm={async (info) => {
// Handle confirm logic here
console.log('Memory updated:', info)
onConfirm={async (info, content) => {
await updateMemory(info, content)
setShowEditModal(false)
}}
onHide={() => setShowEditModal(false)}

View File

@ -10,14 +10,23 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Divider from '@/app/components/base/divider'
import type { Memory } from '@/app/components/base/chat/types'
import cn from '@/utils/classnames'
type Props = {
memory: Memory
onEdit: () => void
resetDefault: (memory: Memory) => void
clearAllUpdateVersion: (memory: Memory) => void
switchMemoryVersion: (memory: Memory, version: string) => void
}
const OperationDropdown: FC<Props> = ({
memory,
onEdit,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
@ -52,8 +61,8 @@ const OperationDropdown: FC<Props> = ({
<div className='w-[220px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
<div className='p-1'>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={onEdit}>{t('share.chat.memory.operations.edit')}</div>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('share.chat.memory.operations.reset')}</div>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-destructive-hover hover:text-text-destructive'>{t('share.chat.memory.operations.clear')}</div>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={() => resetDefault(memory)}>{t('share.chat.memory.operations.reset')}</div>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-destructive-hover hover:text-text-destructive' onClick={() => clearAllUpdateVersion(memory)}>{t('share.chat.memory.operations.clear')}</div>
</div>
<Divider className='!my-0 !h-px bg-divider-subtle' />
<div className='px-1 py-2'>

View File

@ -6,29 +6,40 @@ import {
} from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import {
useChatWithHistoryContext,
} from '../context'
import MemoryCard from './card'
import cn from '@/utils/classnames'
import { mockMemoryList } from './mock'
import type { Memory } from '@/app/components/base/chat/types'
type Props = {
isMobile?: boolean
showChatMemory?: boolean
setShowChatMemory: (show: boolean) => void
memoryList: Memory[]
clearAllMemory: () => void
updateMemory: (memory: Memory, content: string) => void
resetDefault: (memory: Memory) => void
clearAllUpdateVersion: (memory: Memory) => void
switchMemoryVersion: (memory: Memory, version: string) => void
}
const MemoryPanel: React.FC<Props> = ({ showChatMemory }) => {
const MemoryPanel: React.FC<Props> = ({
isMobile,
showChatMemory,
setShowChatMemory,
memoryList,
clearAllMemory,
updateMemory,
resetDefault,
clearAllUpdateVersion,
switchMemoryVersion,
}) => {
const { t } = useTranslation()
const {
isMobile,
setShowChatMemory,
} = useChatWithHistoryContext()
return (
<div className={cn(
'flex h-full w-[360px] shrink-0 flex-col rounded-2xl border-[0.5px] border-components-panel-border-subtle bg-chatbot-bg transition-all ease-in-out',
showChatMemory ? 'w-[360px]' : 'w-0',
showChatMemory ? 'w-[360px]' : 'w-0 opacity-0',
)}>
<div className='flex shrink-0 items-center border-b-[0.5px] border-components-panel-border-subtle pl-4 pr-3.5 pt-2'>
<div className='system-md-semibold-uppercase grow py-3 text-text-primary'>{t('share.chat.memory.title')}</div>
@ -37,15 +48,30 @@ const MemoryPanel: React.FC<Props> = ({ showChatMemory }) => {
</ActionButton>
</div>
<div className='h-0 grow overflow-y-auto px-3 pt-2'>
{mockMemoryList.map(memory => (
<MemoryCard key={memory.name} memory={memory} isMobile={isMobile} />
{memoryList.map(memory => (
<MemoryCard
key={memory.spec.id}
isMobile={isMobile}
memory={memory}
updateMemory={updateMemory}
resetDefault={resetDefault}
clearAllUpdateVersion={clearAllUpdateVersion}
switchMemoryVersion={switchMemoryVersion}
/>
))}
<div className='flex items-center justify-center'>
<Button variant='ghost'>
<RiDeleteBinLine className='mr-1 h-3.5 w-3.5' />
{t('share.chat.memory.clearAll')}
</Button>
</div>
{memoryList.length > 0 && (
<div className='flex items-center justify-center'>
<Button variant='ghost' onClick={clearAllMemory}>
<RiDeleteBinLine className='mr-1 h-3.5 w-3.5' />
{t('share.chat.memory.clearAll')}
</Button>
</div>
)}
{memoryList.length === 0 && (
<div className='system-xs-regular flex items-center justify-center py-2 text-text-tertiary'>
{t('share.chat.memory.empty')}
</div>
)}
</div>
</div>
)

View File

@ -1,29 +1,96 @@
import type { MemoryItem } from './type'
import type { Memory as MemoryItem } from '@/app/components/base/chat/types'
export const mockMemoryList: MemoryItem[] = [
{
name: 'learning_companion',
content: `Learning Goal: [What you\'re studying]
tenant_id: 'user-tenant-id',
value: `Learning Goal: [What you\'re studying]
Current Level: [Beginner/Intermediate/Advanced]
Learning Style: [Visual, hands-on, theoretical, etc.]
Progress: [Topics mastered, current focus]
Preferred Pace: [Fast/moderate/slow explanations]
Background: [Relevant experience or education]
Time Constraints: [Available study time]`,
app_id: 'user-app-id',
conversation_id: '',
version: 1,
edited_by_user: false,
conversation_metadata: {
type: 'mutable_visible_window',
visible_count: 5,
},
spec: {
id: 'learning_companion',
name: 'Learning Companion',
description: 'A companion to help with learning goals',
template: 'no zuo no die why you try', // default value
instruction: 'enjoy yourself',
scope: 'app', // app or node
term: 'session', // session or persistent
strategy: 'on_turns',
update_turns: 3,
preserved_turns: 5,
schedule_mode: 'sync', // sync or async
end_user_visible: true,
end_user_editable: true,
},
},
{
name: 'research_partner',
content: `Research Topic: [Your research topic]
tenant_id: 'user-tenant-id',
value: `Research Topic: [Your research topic]
Current Progress: [Literature review, experiments, etc.]
Challenges: [What you\'re struggling with]
Goals: [Short-term and long-term research goals]`,
status: 'latest',
app_id: 'user-app-id',
conversation_id: '',
version: 1,
edited_by_user: false,
conversation_metadata: {
type: 'mutable_visible_window',
visible_count: 5,
},
spec: {
id: 'research_partner',
name: 'research_partner',
description: 'A companion to help with research goals',
template: 'no zuo no die why you try', // default value
instruction: 'enjoy yourself',
scope: 'app', // app or node
term: 'session', // session or persistent
strategy: 'on_turns',
update_turns: 3,
preserved_turns: 3,
schedule_mode: 'sync', // sync or async
end_user_visible: true,
end_user_editable: false,
},
},
{
name: 'code_partner',
content: `Code Context: [Brief description of the codebase]
tenant_id: 'user-tenant-id',
value: `Code Context: [Brief description of the codebase]
Current Issues: [Bugs, technical debt, etc.]
Goals: [Features to implement, improvements to make]`,
status: 'needUpdate',
mergeCount: 5,
app_id: 'user-app-id',
conversation_id: '',
version: 3,
edited_by_user: true,
conversation_metadata: {
type: 'mutable_visible_window',
visible_count: 5,
},
spec: {
id: 'code_partner',
name: 'code_partner',
description: 'A companion to help with code-related tasks',
template: 'no zuo no die why you try', // default value
instruction: 'enjoy yourself',
scope: 'app', // app or node
term: 'session', // session or persistent
strategy: 'on_turns',
update_turns: 3,
preserved_turns: 5,
schedule_mode: 'sync', // sync or async
end_user_visible: true,
end_user_editable: true,
},
},
]

View File

@ -1,6 +0,0 @@
export type MemoryItem = {
name: string;
content: string;
status?: 'latest' | 'needUpdate';
mergeCount?: number;
}

View File

@ -16,7 +16,7 @@ import Loading from '@/app/components/base/loading'
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
import Header from '@/app/components/base/chat/embedded-chatbot/header'
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
import MemoryPanel from './memory'
import MemoryPanel from '@/app/components/base/chat/chat-with-history/memory'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
@ -94,14 +94,18 @@ const Chatbot = () => {
</div>
)}
{showChatMemory && (
<div className='fixed inset-0 z-50 flex flex-row-reverse bg-background-overlay p-1 backdrop-blur-sm'
onClick={() => setShowChatMemory(false)}
>
<div className='flex h-full w-[360px] rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
<MemoryPanel showChatMemory={showChatMemory} />
</div>
<div className='fixed inset-0 z-50 flex flex-row-reverse bg-background-overlay p-1 backdrop-blur-sm'
onClick={() => setShowChatMemory(false)}
>
<div className='flex h-full w-[360px] rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
<MemoryPanel
showChatMemory={showChatMemory}
isMobile={isMobile}
setShowChatMemory={setShowChatMemory}
/>
</div>
)}
</div>
)}
</div>
)
}

View File

@ -1,117 +0,0 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
import Modal from '@/app/components/base/modal'
import ActionButton from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider'
import Toast from '@/app/components/base/toast'
import type { MemoryItem } from '../type'
import { noop } from 'lodash-es'
import cn from '@/utils/classnames'
type Props = {
memory: MemoryItem
show: boolean
onConfirm: (info: MemoryItem) => Promise<void>
onHide: () => void
isMobile?: boolean
}
const MemoryEditModal = ({
memory,
show = false,
onConfirm,
onHide,
isMobile,
}: Props) => {
const { t } = useTranslation()
const [content, setContent] = React.useState(memory.content)
const submit = () => {
if (!content.trim()) {
Toast.notify({ type: 'error', message: 'content is required' })
return
}
onConfirm({ ...memory, content })
onHide()
}
if (isMobile) {
return (
<div className='fixed inset-0 z-50 flex flex-col bg-background-overlay pt-3 backdrop-blur-sm'
onClick={onHide}
>
<div className='relative flex w-full grow flex-col rounded-t-xl bg-components-panel-bg shadow-xl' onClick={e => e.stopPropagation()}>
<div className='absolute right-4 top-4 cursor-pointer p-2'>
<ActionButton onClick={onHide}>
<RiCloseLine className='h-5 w-5' />
</ActionButton>
</div>
<div className='p-4 pb-3'>
<div className='title-2xl-semi-bold mb-2 text-text-primary'>{t('share.chat.memory.editTitle')}</div>
<div className='flex items-center gap-1 pb-1 pt-2'>
<Memory className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-sm-semibold truncate text-text-primary'>{memory.name}</div>
<Badge text={`${t('share.chat.memory.updateVersion.update')} 2`} />
</div>
</div>
<div className='grow px-4'>
<Textarea
className='h-full'
value={content}
onChange={e => setContent(e.target.value)}
/>
</div>
<div className='flex flex-row-reverse items-center p-4'>
<Button className='ml-2' variant='primary' onClick={submit}>{t('share.chat.memory.operations.save')}</Button>
<Button className='ml-3' onClick={onHide}>{t('share.chat.memory.operations.cancel')}</Button>
<Divider type='vertical' className='!mx-0 !h-4' />
<Button className='mr-3' onClick={onHide}>{t('share.chat.memory.operations.reset')}</Button>
</div>
</div>
</div>
)
}
return (
<Modal
isShow={show}
onClose={noop}
className={cn('relative !max-w-[800px]', 'p-0')}
>
<div className='absolute right-5 top-5 cursor-pointer p-2'>
<ActionButton onClick={onHide}>
<RiCloseLine className='h-5 w-5' />
</ActionButton>
</div>
<div className='p-6 pb-3'>
<div className='title-2xl-semi-bold mb-2 text-text-primary'>{t('share.chat.memory.editTitle')}</div>
<div className='flex items-center gap-1 pb-1 pt-2'>
<Memory className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-sm-semibold truncate text-text-primary'>{memory.name}</div>
<Badge text={`${t('share.chat.memory.updateVersion.update')} 2`} />
</div>
</div>
<div className='px-6'>
<Textarea
className='h-[562px]'
value={content}
onChange={e => setContent(e.target.value)}
/>
</div>
<div className='flex flex-row-reverse items-center p-6 pt-5'>
<Button className='ml-2' variant='primary' onClick={submit}>{t('share.chat.memory.operations.save')}</Button>
<Button className='ml-3' onClick={onHide}>{t('share.chat.memory.operations.cancel')}</Button>
<Divider type='vertical' className='!mx-0 !h-4' />
<Button className='mr-3' onClick={onHide}>{t('share.chat.memory.operations.reset')}</Button>
</div>
</Modal>
)
}
export default MemoryEditModal

View File

@ -1,81 +0,0 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
RiArrowUpSLine,
} from '@remixicon/react'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
import ActionButton from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
import Indicator from '@/app/components/header/indicator'
import Operation from './operation'
import MemoryEditModal from './edit-modal'
import type { MemoryItem } from '../type'
import cn from '@/utils/classnames'
type Props = {
memory: MemoryItem
isMobile?: boolean
}
const MemoryCard: React.FC<Props> = ({ memory, isMobile }) => {
const { t } = useTranslation()
const [isHovering, setIsHovering] = React.useState(false)
const [showEditModal, setShowEditModal] = React.useState(false)
return (
<>
<div
className={cn('group mb-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-md', !memory.status && 'pb-2')}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<div className='flex items-end justify-between pb-1 pl-4 pr-2 pt-2'>
<div className='flex items-center gap-1 pb-1 pt-2'>
<Memory className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-sm-semibold truncate text-text-primary'>{memory.name}</div>
<Badge text={`${t('share.chat.memory.updateVersion.update')} 2`} />
</div>
{isHovering && (
<div className='hover:bg-components-actionbar-bg-hover flex items-center gap-0.5 rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md'>
<ActionButton><RiArrowUpSLine className='h-4 w-4' /></ActionButton>
<ActionButton><RiArrowDownSLine className='h-4 w-4' /></ActionButton>
<Operation onEdit={() => {
setShowEditModal(true)
setIsHovering(false)
}} />
</div>
)}
</div>
<div className='system-xs-regular line-clamp-[12] px-4 pb-2 pt-1 text-text-tertiary'>{memory.content}</div>
{memory.status === 'latest' && (
<div className='flex items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle bg-background-default-subtle px-4 py-3 group-hover:bg-components-panel-on-panel-item-bg-hover'>
<div className='system-xs-regular text-text-tertiary'>{t('share.chat.memory.latestVersion')}</div>
<Indicator color='green' />
</div>
)}
{memory.status === 'needUpdate' && (
<div className='flex items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle bg-background-default-subtle px-4 py-3 group-hover:bg-components-panel-on-panel-item-bg-hover'>
<div className='system-xs-regular text-text-tertiary'>{t('share.chat.memory.notLatestVersion', { num: memory.mergeCount })}</div>
<Indicator color='orange' />
</div>
)}
</div>
{showEditModal && (
<MemoryEditModal
isMobile={isMobile}
show={showEditModal}
memory={memory}
onConfirm={async (info) => {
// Handle confirm logic here
console.log('Memory updated:', info)
setShowEditModal(false)
}}
onHide={() => setShowEditModal(false)}
/>
)}
</>
)
}
export default MemoryCard

View File

@ -1,79 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCheckLine, RiMoreFill } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames'
type Props = {
onEdit: () => void
}
const OperationDropdown: FC<Props> = ({
onEdit,
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
const setOpen = useCallback((v: boolean) => {
doSetOpen(v)
openRef.current = v
}, [doSetOpen])
const handleTrigger = useCallback(() => {
setOpen(!openRef.current)
}, [setOpen])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div>
<ActionButton className={cn(open && 'bg-state-base-hover')}>
<RiMoreFill className='h-4 w-4' />
</ActionButton>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[220px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
<div className='p-1'>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={onEdit}>{t('share.chat.memory.operations.edit')}</div>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('share.chat.memory.operations.reset')}</div>
<div className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-destructive-hover hover:text-text-destructive'>{t('share.chat.memory.operations.clear')}</div>
</div>
<Divider className='!my-0 !h-px bg-divider-subtle' />
<div className='px-1 py-2'>
<div className='system-xs-medium-uppercase px-3 pb-0.5 pt-1 text-text-tertiary'>{t('share.chat.memory.updateVersion.title')}</div>
<div className='system-md-regular flex cursor-pointer items-center gap-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>
{t('share.chat.memory.operations.edit')}
<RiCheckLine className='h-4 w-4 text-text-accent' />
</div>
<div className='system-md-regular flex cursor-pointer items-center gap-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>
{t('share.chat.memory.operations.edit')}
<RiCheckLine className='h-4 w-4 text-text-accent' />
</div>
<div className='system-md-regular flex cursor-pointer items-center gap-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>
{t('share.chat.memory.operations.edit')}
<RiCheckLine className='h-4 w-4 text-text-accent' />
</div>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(OperationDropdown)

View File

@ -1,54 +0,0 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
RiCloseLine,
RiDeleteBinLine,
} from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import {
useEmbeddedChatbotContext,
} from '../context'
import MemoryCard from './card'
import cn from '@/utils/classnames'
import { mockMemoryList } from './mock'
type Props = {
showChatMemory?: boolean
}
const MemoryPanel: React.FC<Props> = ({ showChatMemory }) => {
const { t } = useTranslation()
const {
isMobile,
setShowChatMemory,
} = useEmbeddedChatbotContext()
return (
<div className={cn(
'flex h-full w-[360px] shrink-0 flex-col rounded-2xl border-[0.5px] border-components-panel-border-subtle bg-chatbot-bg transition-all ease-in-out',
showChatMemory ? 'w-[360px]' : 'w-0',
)}>
<div className='flex shrink-0 items-center border-b-[0.5px] border-components-panel-border-subtle pl-4 pr-3.5 pt-2'>
<div className='system-md-semibold-uppercase grow py-3 text-text-primary'>{t('share.chat.memory.title')}</div>
<ActionButton size='l' onClick={() => setShowChatMemory(false)}>
<RiCloseLine className='h-[18px] w-[18px]' />
</ActionButton>
</div>
<div className='h-0 grow overflow-y-auto px-3 pt-2'>
{mockMemoryList.map(memory => (
<MemoryCard key={memory.name} memory={memory} isMobile={isMobile} />
))}
<div className='flex items-center justify-center'>
<Button variant='ghost'>
<RiDeleteBinLine className='mr-1 h-3.5 w-3.5' />
{t('share.chat.memory.clearAll')}
</Button>
</div>
</div>
</div>
)
}
export default MemoryPanel

View File

@ -1,29 +0,0 @@
import type { MemoryItem } from './type'
export const mockMemoryList: MemoryItem[] = [
{
name: 'learning_companion',
content: `Learning Goal: [What you\'re studying]
Current Level: [Beginner/Intermediate/Advanced]
Learning Style: [Visual, hands-on, theoretical, etc.]
Progress: [Topics mastered, current focus]
Preferred Pace: [Fast/moderate/slow explanations]
Background: [Relevant experience or education]
Time Constraints: [Available study time]`,
},
{
name: 'research_partner',
content: `Research Topic: [Your research topic]
Current Progress: [Literature review, experiments, etc.]
Challenges: [What you\'re struggling with]
Goals: [Short-term and long-term research goals]`,
status: 'latest',
},
{
name: 'code_partner',
content: `Code Context: [Brief description of the codebase]
Current Issues: [Bugs, technical debt, etc.]
Goals: [Features to implement, improvements to make]`,
status: 'needUpdate',
mergeCount: 5,
},
]

View File

@ -1,6 +0,0 @@
export type MemoryItem = {
name: string;
content: string;
status?: 'latest' | 'needUpdate';
mergeCount?: number;
}

View File

@ -95,3 +95,36 @@ export type Feedback = {
rating: 'like' | 'dislike' | null
content?: string | null
}
export type MemorySpec = {
id: string
name: string
description: string
template: string // default value
instruction: string
scope: string // app or node
term: string // session or persistent
strategy: string
update_turns: number
preserved_turns: number
schedule_mode: string // sync or async
end_user_visible: boolean
end_user_editable: boolean
}
export type ConversationMetaData = {
type: string // mutable_visible_window
visible_count: number // visible_count - preserved_turns = N messages waiting merged
}
export type Memory = {
tenant_id: string
value: string
app_id: string
conversation_id?: string
node_id?: string
version: number
edited_by_user: boolean
conversation_metadata?: ConversationMetaData
spec: MemorySpec
}

View File

@ -57,6 +57,7 @@ const translation = {
cancel: 'Cancel',
save: 'Save & Apply',
},
empty: 'No memory yet.',
},
},
generation: {

View File

@ -53,6 +53,7 @@ const translation = {
cancel: '取消',
save: '保存并应用',
},
empty: '暂无记忆。',
},
},
generation: {

View File

@ -22,8 +22,8 @@ import type {
IOnWorkflowStarted,
} from './base'
import {
del as consoleDel, get as consoleGet, patch as consolePatch, post as consolePost,
delPublic as del, getPublic as get, patchPublic as patch, postPublic as post, ssePost,
del as consoleDel, get as consoleGet, patch as consolePatch, post as consolePost, put as consolePut,
delPublic as del, getPublic as get, patchPublic as patch, postPublic as post, putPublic as put, ssePost,
} from './base'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import type {
@ -32,10 +32,10 @@ import type {
AppMeta,
ConversationItem,
} from '@/models/share'
import type { ChatConfig } from '@/app/components/base/chat/types'
import type { ChatConfig, Memory } from '@/app/components/base/chat/types'
import type { AccessMode } from '@/models/access-control'
function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) {
function getAction(action: 'get' | 'post' | 'del' | 'patch' | 'put', isInstalledApp: boolean) {
switch (action) {
case 'get':
return isInstalledApp ? consoleGet : get
@ -45,6 +45,8 @@ function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boo
return isInstalledApp ? consolePatch : patch
case 'del':
return isInstalledApp ? consoleDel : del
case 'put':
return isInstalledApp ? consolePut : put
}
}
@ -308,3 +310,30 @@ export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => {
export const getAppAccessModeByAppCode = (appCode: string) => {
return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appCode=${appCode}`)
}
export const fetchMemories = async (
conversation_id = '',
memory_id = '',
version = '',
isInstalledApp: boolean,
installedAppId = '',
) => {
return (getAction('get', isInstalledApp))(getUrl('/memories', isInstalledApp, installedAppId), { params: { conversation_id, memory_id, version } }) as Promise<Memory[]>
}
export const deleteMemory = (
memoryId = '',
isInstalledApp: boolean,
installedAppId = '',
) => {
return (getAction('del', isInstalledApp))(getUrl('/memories', isInstalledApp, installedAppId), { params: { id: memoryId } })
}
export const editMemory = (
memoryId: string,
value: string,
isInstalledApp: boolean,
installedAppId = '',
) => {
return (getAction('put', isInstalledApp))(getUrl('memory-edit', isInstalledApp, installedAppId), { body: { id: memoryId, update: value } })
}