mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-17 02:25:57 +00:00
feat: conversation history (#235)
* feat: create conversations storage hook * feat: save conversation hook * feat: created chat layout * feat: created chat history button * feat: setup chat history view links * chore: updated placeholder * fix: width of the chat history screen * feat: provide navigation from history page back to conversation page * fix: issue with restoring conversation id * chore: do not update history when content doesn't change * feat: mark active conversation id * fix: syncing the conversation id ref
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
import type { FC } from 'react'
|
||||
import { HashRouter, Route, Routes } from 'react-router'
|
||||
import { ChatHistory } from './history/ChatHistory'
|
||||
import { Chat } from './index/Chat'
|
||||
import { ChatLayout } from './layout/ChatLayout'
|
||||
|
||||
export const App: FC = () => {
|
||||
return (
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route index element={<Chat />} />
|
||||
<Route element={<ChatLayout />}>
|
||||
<Route index element={<Chat />} />
|
||||
<Route path="history" element={<ChatHistory />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
)
|
||||
|
||||
185
apps/agent/entrypoints/sidepanel/history/ChatHistory.tsx
Normal file
185
apps/agent/entrypoints/sidepanel/history/ChatHistory.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { MessageSquare, Trash2 } from 'lucide-react'
|
||||
import { type FC, useMemo } from 'react'
|
||||
import { Link } from 'react-router'
|
||||
import {
|
||||
type Conversation,
|
||||
useConversations,
|
||||
} from '@/lib/conversations/conversationStorage'
|
||||
import { useChatSessionContext } from '../layout/ChatSessionContext'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
type TimeGroup = 'today' | 'thisWeek' | 'thisMonth' | 'older'
|
||||
|
||||
interface GroupedConversations {
|
||||
today: Conversation[]
|
||||
thisWeek: Conversation[]
|
||||
thisMonth: Conversation[]
|
||||
older: Conversation[]
|
||||
}
|
||||
|
||||
const TIME_GROUP_LABELS: Record<TimeGroup, string> = {
|
||||
today: 'Today',
|
||||
thisWeek: 'This Week',
|
||||
thisMonth: 'This Month',
|
||||
older: 'Older',
|
||||
}
|
||||
|
||||
const getTimeGroup = (timestamp: number): TimeGroup => {
|
||||
const date = dayjs(timestamp)
|
||||
const now = dayjs()
|
||||
|
||||
if (date.isSame(now, 'day')) return 'today'
|
||||
if (date.isSame(now, 'week')) return 'thisWeek'
|
||||
if (date.isSame(now, 'month')) return 'thisMonth'
|
||||
return 'older'
|
||||
}
|
||||
|
||||
const getLastUserMessage = (conversation: Conversation): string => {
|
||||
const userMessages = conversation.messages.filter((m) => m.role === 'user')
|
||||
const lastUserMessage = userMessages[userMessages.length - 1]
|
||||
|
||||
if (!lastUserMessage) return 'New conversation'
|
||||
|
||||
const textParts = lastUserMessage.parts.filter((p) => p.type === 'text')
|
||||
const text = textParts.map((p) => p.text).join(' ')
|
||||
|
||||
return text || 'New conversation'
|
||||
}
|
||||
|
||||
const ConversationItem: FC<{
|
||||
conversation: Conversation
|
||||
onDelete: (id: string) => void
|
||||
isActive: boolean
|
||||
}> = ({ conversation, onDelete, isActive }) => {
|
||||
const label = getLastUserMessage(conversation)
|
||||
const relativeTimeAgo = dayjs(conversation.lastMessagedAt).fromNow()
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/?conversationId=${conversation.id}`}
|
||||
className={`group flex w-full items-start gap-3 rounded-lg px-3 py-2.5 transition-colors hover:bg-muted/50 ${
|
||||
isActive ? 'bg-muted/70' : ''
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`mt-0.5 shrink-0 ${isActive ? 'text-primary' : 'text-muted-foreground'}`}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
<p className="truncate font-medium text-foreground text-sm">{label}</p>
|
||||
<p className="text-muted-foreground text-xs">{relativeTimeAgo}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onDelete(conversation.id)
|
||||
}}
|
||||
className="shrink-0 rounded p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100"
|
||||
title="Delete conversation"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const ConversationGroup: FC<{
|
||||
label: string
|
||||
conversations: Conversation[]
|
||||
onDelete: (id: string) => void
|
||||
activeConversationId: string
|
||||
}> = ({ label, conversations, onDelete, activeConversationId }) => {
|
||||
if (conversations.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 px-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider">
|
||||
{label}
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{conversations.map((conversation) => (
|
||||
<ConversationItem
|
||||
key={conversation.id}
|
||||
conversation={conversation}
|
||||
onDelete={onDelete}
|
||||
isActive={conversation.id === activeConversationId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChatHistory: FC = () => {
|
||||
const { conversations, removeConversation } = useConversations()
|
||||
const { conversationId: activeConversationId } = useChatSessionContext()
|
||||
|
||||
const groupedConversations = useMemo<GroupedConversations>(() => {
|
||||
const groups: GroupedConversations = {
|
||||
today: [],
|
||||
thisWeek: [],
|
||||
thisMonth: [],
|
||||
older: [],
|
||||
}
|
||||
|
||||
for (const conversation of conversations) {
|
||||
const group = getTimeGroup(conversation.lastMessagedAt)
|
||||
groups[group].push(conversation)
|
||||
}
|
||||
|
||||
return groups
|
||||
}, [conversations])
|
||||
|
||||
const hasConversations = conversations.length > 0
|
||||
|
||||
return (
|
||||
<main className="mt-4 flex h-full flex-1 flex-col space-y-4 overflow-y-auto">
|
||||
<div className="w-full p-3">
|
||||
{!hasConversations ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<MessageSquare className="mb-3 h-10 w-10 text-muted-foreground/50" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
No conversations yet
|
||||
</p>
|
||||
<Link to="/" className="mt-2 text-primary text-sm hover:underline">
|
||||
Start a new chat
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ConversationGroup
|
||||
label={TIME_GROUP_LABELS.today}
|
||||
conversations={groupedConversations.today}
|
||||
onDelete={removeConversation}
|
||||
activeConversationId={activeConversationId}
|
||||
/>
|
||||
<ConversationGroup
|
||||
label={TIME_GROUP_LABELS.thisWeek}
|
||||
conversations={groupedConversations.thisWeek}
|
||||
onDelete={removeConversation}
|
||||
activeConversationId={activeConversationId}
|
||||
/>
|
||||
<ConversationGroup
|
||||
label={TIME_GROUP_LABELS.thisMonth}
|
||||
conversations={groupedConversations.thisMonth}
|
||||
onDelete={removeConversation}
|
||||
activeConversationId={activeConversationId}
|
||||
/>
|
||||
<ConversationGroup
|
||||
label={TIME_GROUP_LABELS.older}
|
||||
conversations={groupedConversations.older}
|
||||
onDelete={removeConversation}
|
||||
activeConversationId={activeConversationId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -3,12 +3,11 @@ import { createBrowserOSAction } from '@/lib/chat-actions/types'
|
||||
import { SIDEPANEL_AI_TRIGGERED_EVENT } from '@/lib/constants/analyticsEvents'
|
||||
import { useJtbdPopup } from '@/lib/jtbd-popup/useJtbdPopup'
|
||||
import { track } from '@/lib/metrics/track'
|
||||
import { useChatSessionContext } from '../layout/ChatSessionContext'
|
||||
import { ChatEmptyState } from './ChatEmptyState'
|
||||
import { ChatError } from './ChatError'
|
||||
import { ChatFooter } from './ChatFooter'
|
||||
import { ChatHeader } from './ChatHeader'
|
||||
import { ChatMessages } from './ChatMessages'
|
||||
import { useChatSession } from './useChatSession'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -21,19 +20,14 @@ export const Chat = () => {
|
||||
sendMessage,
|
||||
status,
|
||||
stop,
|
||||
providers,
|
||||
selectedProvider,
|
||||
isLoading,
|
||||
agentUrlError,
|
||||
chatError,
|
||||
handleSelectProvider,
|
||||
getActionForMessage,
|
||||
resetConversation,
|
||||
liked,
|
||||
onClickLike,
|
||||
disliked,
|
||||
onClickDislike,
|
||||
} = useChatSession()
|
||||
} = useChatSessionContext()
|
||||
|
||||
const {
|
||||
popupVisible,
|
||||
@@ -137,24 +131,8 @@ export const Chat = () => {
|
||||
executeMessage(suggestion)
|
||||
}
|
||||
|
||||
if (isLoading || !selectedProvider) {
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-background">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-screen w-screen flex-col bg-background text-foreground">
|
||||
<ChatHeader
|
||||
selectedProvider={selectedProvider}
|
||||
onSelectProvider={handleSelectProvider}
|
||||
providers={providers}
|
||||
onNewConversation={resetConversation}
|
||||
hasMessages={messages.length > 0}
|
||||
/>
|
||||
|
||||
<>
|
||||
<main className="mt-4 flex h-full flex-1 flex-col space-y-4 overflow-y-auto">
|
||||
{messages.length === 0 ? (
|
||||
<ChatEmptyState
|
||||
@@ -193,6 +171,6 @@ export const Chat = () => {
|
||||
onToggleTab={toggleTabSelection}
|
||||
onRemoveTab={removeTab}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Github, Plus, SettingsIcon } from 'lucide-react'
|
||||
import { Github, History, Plus, SettingsIcon } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { Link, useLocation, useNavigate } from 'react-router'
|
||||
import { ChatProviderSelector } from '@/components/chat/ChatProviderSelector'
|
||||
import type { Provider } from '@/components/chat/chatComponentTypes'
|
||||
import { ThemeToggle } from '@/components/elements/theme-toggle'
|
||||
@@ -22,6 +23,15 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
|
||||
onNewConversation,
|
||||
hasMessages,
|
||||
}) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const isHistoryPage = location.pathname === '/history'
|
||||
|
||||
const handleNewConversationFromHistory = () => {
|
||||
onNewConversation()
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between border-border/40 border-b bg-background/80 px-3 py-2.5 backdrop-blur-md">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -52,7 +62,7 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{hasMessages && (
|
||||
{!isHistoryPage && hasMessages && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewConversation}
|
||||
@@ -63,6 +73,25 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isHistoryPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNewConversationFromHistory}
|
||||
className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
||||
title="New conversation"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
to="/history"
|
||||
className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
||||
title="Chat history"
|
||||
>
|
||||
<History className="h-4 w-4" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<a
|
||||
href={productRepositoryUrl}
|
||||
target="_blank"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useChat } from '@ai-sdk/react'
|
||||
import { DefaultChatTransport, type UIMessage } from 'ai'
|
||||
import { compact } from 'es-toolkit/array'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router'
|
||||
import useDeepCompareEffect from 'use-deep-compare-effect'
|
||||
import type { Provider } from '@/components/chat/chatComponentTypes'
|
||||
import { useAgentServerUrl } from '@/lib/browseros/useBrowserOSProviders'
|
||||
@@ -13,6 +14,10 @@ import {
|
||||
MESSAGE_SENT_EVENT,
|
||||
PROVIDER_SELECTED_EVENT,
|
||||
} from '@/lib/constants/analyticsEvents'
|
||||
import {
|
||||
conversationStorage,
|
||||
useConversations,
|
||||
} from '@/lib/conversations/conversationStorage'
|
||||
import { useLlmProviders } from '@/lib/llm-providers/useLlmProviders'
|
||||
import { track } from '@/lib/metrics/track'
|
||||
import { searchActionsStorage } from '@/lib/search-actions/searchActionsStorage'
|
||||
@@ -69,6 +74,10 @@ export const useChatSession = () => {
|
||||
error: agentUrlError,
|
||||
} = useAgentServerUrl()
|
||||
|
||||
const { saveConversation } = useConversations()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const conversationIdParam = searchParams.get('conversationId')
|
||||
|
||||
const agentUrlRef = useRef(agentServerUrl)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -87,7 +96,12 @@ export const useChatSession = () => {
|
||||
)
|
||||
const [liked, setLiked] = useState<Record<string, boolean>>({})
|
||||
const [disliked, setDisliked] = useState<Record<string, boolean>>({})
|
||||
const conversationIdRef = useRef(crypto.randomUUID())
|
||||
const [conversationId, setConversationId] = useState(crypto.randomUUID())
|
||||
const conversationIdRef = useRef(conversationId)
|
||||
|
||||
useEffect(() => {
|
||||
conversationIdRef.current = conversationId
|
||||
}, [conversationId])
|
||||
|
||||
const onClickLike = (messageId: string) => {
|
||||
const { responseText, queryText } = getResponseAndQueryFromMessageId(
|
||||
@@ -251,6 +265,35 @@ export const useChatSession = () => {
|
||||
conversationId: conversationIdRef.current,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!conversationIdParam) return
|
||||
|
||||
const restoreConversation = async () => {
|
||||
const conversations = await conversationStorage.getValue()
|
||||
const conversation = conversations?.find(
|
||||
(c) => c.id === conversationIdParam,
|
||||
)
|
||||
|
||||
if (conversation) {
|
||||
setConversationId(
|
||||
conversation.id as ReturnType<typeof crypto.randomUUID>,
|
||||
)
|
||||
setMessages(conversation.messages)
|
||||
}
|
||||
|
||||
setSearchParams({}, { replace: true })
|
||||
}
|
||||
|
||||
restoreConversation()
|
||||
}, [conversationIdParam, setMessages, setSearchParams])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: only need to run when messages change
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
saveConversation(conversationIdRef.current, messages)
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
const sendMessage = (params: { text: string; action?: ChatAction }) => {
|
||||
track(MESSAGE_SENT_EVENT, {
|
||||
mode,
|
||||
@@ -300,7 +343,7 @@ export const useChatSession = () => {
|
||||
track(CONVERSATION_RESET_EVENT, { message_count: messages.length })
|
||||
stop()
|
||||
const oldConversationId = conversationIdRef.current
|
||||
conversationIdRef.current = crypto.randomUUID()
|
||||
setConversationId(crypto.randomUUID())
|
||||
setMessages([])
|
||||
setTextToAction(new Map())
|
||||
setLiked({})
|
||||
@@ -332,5 +375,6 @@ export const useChatSession = () => {
|
||||
onClickLike,
|
||||
disliked,
|
||||
onClickDislike,
|
||||
conversationId,
|
||||
}
|
||||
}
|
||||
|
||||
49
apps/agent/entrypoints/sidepanel/layout/ChatLayout.tsx
Normal file
49
apps/agent/entrypoints/sidepanel/layout/ChatLayout.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { FC } from 'react'
|
||||
import { Outlet } from 'react-router'
|
||||
import { ChatHeader } from '../index/ChatHeader'
|
||||
import {
|
||||
ChatSessionProvider,
|
||||
useChatSessionContext,
|
||||
} from './ChatSessionContext'
|
||||
|
||||
const ChatLayoutContent: FC = () => {
|
||||
const {
|
||||
providers,
|
||||
selectedProvider,
|
||||
handleSelectProvider,
|
||||
resetConversation,
|
||||
messages,
|
||||
isLoading,
|
||||
} = useChatSessionContext()
|
||||
|
||||
if (isLoading || !selectedProvider) {
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-background">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-screen w-screen flex-col overflow-hidden bg-background text-foreground">
|
||||
<ChatHeader
|
||||
selectedProvider={selectedProvider}
|
||||
onSelectProvider={handleSelectProvider}
|
||||
providers={providers}
|
||||
onNewConversation={resetConversation}
|
||||
hasMessages={messages.length > 0}
|
||||
/>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChatLayout: FC = () => {
|
||||
return (
|
||||
<ChatSessionProvider>
|
||||
<ChatLayoutContent />
|
||||
</ChatSessionProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createContext, type FC, type ReactNode, useContext } from 'react'
|
||||
import { useChatSession } from '../index/useChatSession'
|
||||
|
||||
type ChatSessionContextValue = ReturnType<typeof useChatSession>
|
||||
|
||||
const ChatSessionContext = createContext<ChatSessionContextValue | null>(null)
|
||||
|
||||
export const ChatSessionProvider: FC<{ children: ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const session = useChatSession()
|
||||
return (
|
||||
<ChatSessionContext.Provider value={session}>
|
||||
{children}
|
||||
</ChatSessionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useChatSessionContext = () => {
|
||||
const context = useContext(ChatSessionContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useChatSessionContext must be used within a ChatSessionProvider',
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
78
apps/agent/lib/conversations/conversationStorage.ts
Normal file
78
apps/agent/lib/conversations/conversationStorage.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { storage } from '@wxt-dev/storage'
|
||||
import type { UIMessage } from 'ai'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const MAX_CONVERSATIONS = 50
|
||||
|
||||
export interface Conversation {
|
||||
id: string
|
||||
messages: UIMessage[]
|
||||
lastMessagedAt: number
|
||||
}
|
||||
|
||||
export const conversationStorage = storage.defineItem<Conversation[]>(
|
||||
'local:conversations',
|
||||
{
|
||||
fallback: [],
|
||||
},
|
||||
)
|
||||
|
||||
export function useConversations() {
|
||||
const [conversations, setConversations] = useState<Conversation[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
conversationStorage.getValue().then(setConversations)
|
||||
const unwatch = conversationStorage.watch((newValue) => {
|
||||
setConversations(newValue ?? [])
|
||||
})
|
||||
return unwatch
|
||||
}, [])
|
||||
|
||||
const removeConversation = async (id: string) => {
|
||||
const current = (await conversationStorage.getValue()) ?? []
|
||||
await conversationStorage.setValue(current.filter((c) => c.id !== id))
|
||||
}
|
||||
|
||||
const saveConversation = async (id: string, messages: UIMessage[]) => {
|
||||
const current = (await conversationStorage.getValue()) ?? []
|
||||
const existingIndex = current.findIndex((c) => c.id === id)
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const existing = current[existingIndex]
|
||||
const hasContentChanged =
|
||||
existing.messages.length !== messages.length ||
|
||||
JSON.stringify(existing.messages) !== JSON.stringify(messages)
|
||||
|
||||
if (!hasContentChanged) return
|
||||
|
||||
current[existingIndex] = {
|
||||
...existing,
|
||||
messages,
|
||||
lastMessagedAt: Date.now(),
|
||||
}
|
||||
await conversationStorage.setValue(current)
|
||||
} else {
|
||||
const newConversation: Conversation = {
|
||||
id,
|
||||
messages,
|
||||
lastMessagedAt: Date.now(),
|
||||
}
|
||||
let updated = [newConversation, ...current]
|
||||
if (updated.length > MAX_CONVERSATIONS) {
|
||||
updated = updated.slice(0, MAX_CONVERSATIONS)
|
||||
}
|
||||
await conversationStorage.setValue(updated)
|
||||
}
|
||||
}
|
||||
|
||||
const getConversation = (id: string) => {
|
||||
return conversations.find((c) => c.id === id)
|
||||
}
|
||||
|
||||
return {
|
||||
conversations,
|
||||
removeConversation,
|
||||
saveConversation,
|
||||
getConversation,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user