Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sync sessions via backend #1804

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useAtom, useAtomValue } from 'jotai'
import * as atoms from './stores/atoms'
import Sidebar from './Sidebar'
import * as premiumActions from './stores/premiumActions'
import { useLoadSessions } from './hooks/useLoadSessions'

function Main() {
const spellCheck = useAtomValue(atoms.spellCheckAtom)
Expand Down Expand Up @@ -53,6 +54,7 @@ function Main() {
}

export default function App() {
useLoadSessions()
useI18nEffect()
premiumActions.useAutoValidate()
useSystemLanguageWhenInit()
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/components/SessionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ export default function SessionList(props: Props) {
const oldIndex = sortedSessions.findIndex(s => s.id === activeId)
const newIndex = sortedSessions.findIndex(s => s.id === overId)
const newReversed = arrayMove(sortedSessions, oldIndex, newIndex)
setSessions(atoms.sortSessions(newReversed))
setSessions({
ts: Date.now(),
sessions: atoms.sortSessions(newReversed),
})
}
}
return (
Expand Down
15 changes: 15 additions & 0 deletions src/renderer/hooks/useLoadSessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useEffect } from 'react'
import { loadSessionsDump } from '@/packages/sync-sessions'
import { replaceSessionsFromBackend } from '@/stores/sessionActions'

export function useLoadSessions() {
useEffect(() => {
;(async () => {
try {
replaceSessionsFromBackend(await loadSessionsDump())
} catch (e) {
console.error(e)
}
})()
}, [])
}
40 changes: 40 additions & 0 deletions src/renderer/packages/sync-sessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ofetch } from 'ofetch'
import { SessionsDump } from 'src/shared/types'

export async function loadSessionsDump() {
return await ofetch<SessionsDump>('http://localhost:8080/api/chats-history', {
method: 'GET',
retry: 3,
headers: {
authorization: `Bearer 634fc0dee42cf0e8bb3e85de634db34e`,
},
})
}

async function saveSessionsToBackend(dump: SessionsDump) {
try {
await fetch('http://localhost:8080/api/chats-history', {
method: 'PUT',
headers: {
'content-type': 'application/json',
authorization: `Bearer 634fc0dee42cf0e8bb3e85de634db34e`,
},
body: JSON.stringify(dump),
})
} catch (e) {
console.error(e)
}
}

let saveTimer: ReturnType<typeof setTimeout> | undefined
let scheduledDumpTs = 0

export function scheduleSaveSessionsToBackend(dump: SessionsDump) {
if (dump.ts <= scheduledDumpTs) {
return
}

scheduledDumpTs = dump.ts
clearTimeout(saveTimer)
saveTimer = setTimeout(() => saveSessionsToBackend(dump), 500)
}
1 change: 1 addition & 0 deletions src/renderer/storage/StoreStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import platform from '@/packages/platform'

export enum StorageKey {
ChatSessions = 'chat-sessions',
ChatSessionsTs = 'chat-sessions-ts',
Configs = 'configs',
Settings = 'settings',
MyCopilots = 'myCopilots',
Expand Down
34 changes: 23 additions & 11 deletions src/renderer/stores/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { atom, SetStateAction } from 'jotai'
import { Session, Toast, Settings, CopilotDetail, Message, SettingWindowTab
} from '../../shared/types'
import { Session, Toast, Settings, CopilotDetail, Message, SettingWindowTab, SessionsDump } from '../../shared/types'
import { selectAtom, atomWithStorage } from 'jotai/utils'
import { focusAtom } from 'jotai-optics'
import * as defaults from '../../shared/defaults'
import storage, { StorageKey } from '../storage'
import platform from '../packages/platform'
import { scheduleSaveSessionsToBackend } from '../packages/sync-sessions'

const _settingsAtom = atomWithStorage<Settings>(StorageKey.Settings, defaults.settings(), storage)
export const settingsAtom = atom(
Expand Down Expand Up @@ -43,26 +43,38 @@ export const myCopilotsAtom = atomWithStorage<CopilotDetail[]>(StorageKey.MyCopi

// sessions

const _sessionsTsAtom = atomWithStorage<number>(StorageKey.ChatSessionsTs, 0, storage)
const _sessionsAtom = atomWithStorage<Session[]>(StorageKey.ChatSessions, [], storage)
export const sessionsAtom = atom(
(get) => {
let sessions = get(_sessionsAtom)
if (sessions.length === 0) {
sessions = defaults.sessions()
}
return sessions
return {
ts: get(_sessionsTsAtom),
sessions,
}
},
(get, set, update: SetStateAction<Session[]>) => {
const sessions = get(_sessionsAtom)
let newSessions = typeof update === 'function' ? update(sessions) : update
if (newSessions.length === 0) {
newSessions = defaults.sessions()
(get, set, update: SetStateAction<SessionsDump>) => {
let newDump =
typeof update === 'function' ? update({ ts: get(_sessionsTsAtom), sessions: get(_sessionsAtom) }) : update

if (newDump.sessions.length === 0) {
newDump = {
ts: Date.now(),
sessions: defaults.sessions(),
}
}
set(_sessionsAtom, newSessions)

set(_sessionsTsAtom, newDump.ts)
set(_sessionsAtom, newDump.sessions)

scheduleSaveSessionsToBackend(newDump)
}
)
export const sortedSessionsAtom = atom((get) => {
return sortSessions(get(sessionsAtom))
return sortSessions(get(sessionsAtom).sessions)
})

export function sortSessions(sessions: Session[]): Session[] {
Expand All @@ -86,7 +98,7 @@ export const currentSessionIdAtom = atom(

export const currentSessionAtom = atom((get) => {
const id = get(currentSessionIdAtom)
const sessions = get(sessionsAtom)
const { sessions } = get(sessionsAtom)
let current = sessions.find((session) => session.id === id)
if (!current) {
return sessions[sessions.length - 1] // fallback to the last session
Expand Down
78 changes: 50 additions & 28 deletions src/renderer/stores/sessionActions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { getDefaultStore } from 'jotai'
import {
Settings,
createMessage,
Message,
Session,
} from '../../shared/types'
import { Settings, createMessage, Message, Session, SessionsDump } from '../../shared/types'
import * as atoms from './atoms'
import * as promptFormat from '../packages/prompts'
import * as Sentry from '@sentry/react'
Expand All @@ -18,35 +13,41 @@ import { throttle } from 'lodash'
import { countWord } from '@/packages/word-count'
import { estimateTokensFromMessages } from '@/packages/token'
import * as settingActions from './settingActions'
import { scheduleSaveSessionsToBackend } from '@/packages/sync-sessions'

export function create(newSession: Session) {
const store = getDefaultStore()
store.set(atoms.sessionsAtom, (sessions) => [...sessions, newSession])
store.set(atoms.sessionsAtom, ({ sessions }) => ({
ts: Date.now(),
sessions: [...sessions, newSession],
}))
switchCurrentSession(newSession.id)
}

export function modify(update: Session) {
const store = getDefaultStore()
store.set(atoms.sessionsAtom, (sessions) =>
sessions.map((s) => {
store.set(atoms.sessionsAtom, ({ sessions }) => ({
ts: Date.now(),
sessions: sessions.map((s) => {
if (s.id === update.id) {
return update
}
return s
})
)
}),
}))
}

export function modifyName(sessionId: string, name: string) {
const store = getDefaultStore()
store.set(atoms.sessionsAtom, (sessions) =>
sessions.map((s) => {
store.set(atoms.sessionsAtom, ({ sessions }) => ({
ts: Date.now(),
sessions: sessions.map((s) => {
if (s.id === sessionId) {
return { ...s, name, threadName: name }
}
return s
})
)
}),
}))
}

export function createEmpty(type: 'chat') {
Expand All @@ -66,7 +67,10 @@ export function switchCurrentSession(sessionId: string) {

export function remove(session: Session) {
const store = getDefaultStore()
store.set(atoms.sessionsAtom, (sessions) => sessions.filter((s) => s.id !== session.id))
store.set(atoms.sessionsAtom, ({ sessions }) => ({
ts: Date.now(),
sessions: sessions.filter((s) => s.id !== session.id),
}))
}

export function clear(sessionId: string) {
Expand All @@ -84,29 +88,30 @@ export async function copy(source: Session) {
const store = getDefaultStore()
const newSession = { ...source }
newSession.id = uuidv4()
store.set(atoms.sessionsAtom, (sessions) => {
store.set(atoms.sessionsAtom, ({ sessions }) => {
let originIndex = sessions.findIndex((s) => s.id === source.id)
if (originIndex < 0) {
originIndex = 0
}
const newSessions = [...sessions]
newSessions.splice(originIndex + 1, 0, newSession)
return newSessions
return { ts: Date.now(), sessions: newSessions }
})
}

export function getSession(sessionId: string) {
const store = getDefaultStore()
const sessions = store.get(atoms.sessionsAtom)
const { sessions } = store.get(atoms.sessionsAtom)
return sessions.find((s) => s.id === sessionId)
}

export function insertMessage(sessionId: string, msg: Message) {
const store = getDefaultStore()
msg.wordCount = countWord(msg.content)
msg.tokenCount = estimateTokensFromMessages([msg])
store.set(atoms.sessionsAtom, (sessions) =>
sessions.map((s) => {
store.set(atoms.sessionsAtom, ({ sessions }) => ({
ts: Date.now(),
sessions: sessions.map((s) => {
if (s.id === sessionId) {
const newMessages = [...s.messages]
newMessages.push(msg)
Expand All @@ -116,8 +121,8 @@ export function insertMessage(sessionId: string, msg: Message) {
}
}
return s
})
)
}),
}))
}

export function modifyMessage(sessionId: string, updated: Message, refreshCounting?: boolean) {
Expand All @@ -139,15 +144,16 @@ export function modifyMessage(sessionId: string, updated: Message, refreshCounti
return m
})
}
store.set(atoms.sessionsAtom, (sessions) =>
sessions.map((s) => {
store.set(atoms.sessionsAtom, ({ sessions }) => ({
ts: Date.now(),
sessions: sessions.map((s) => {
if (s.id !== sessionId) {
return s
}
s.messages = handle(s.messages)
return { ...s }
})
)
}),
}))
}

export async function submitNewUserMessage(params: {
Expand Down Expand Up @@ -329,11 +335,27 @@ export function initEmptyChatSession(): Session {
}
}

export function getSessions() {
export function replaceSessionsFromBackend(newDump: SessionsDump) {
const store = getDefaultStore()
const currentDump = store.get(atoms.sessionsAtom)

if (newDump.ts > currentDump.ts) {
store.set(atoms.sessionsAtom, newDump)
} else if (newDump.ts < currentDump.ts) {
scheduleSaveSessionsToBackend(currentDump)
}
}

export function getSessionsDump() {
const store = getDefaultStore()
return store.get(atoms.sessionsAtom)
}

export function getSessions() {
const store = getDefaultStore()
return store.get(atoms.sessionsAtom).sessions
}

export function getSortedSessions() {
const store = getDefaultStore()
return store.get(atoms.sortedSessionsAtom)
Expand Down
5 changes: 5 additions & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export interface Session {
copilotId?: string
}

export interface SessionsDump {
ts: number
sessions: Session[]
}

export function createMessage(role: MessageRole = MessageRoleEnum.User, content: string = ''): Message {
return {
id: uuidv4(),
Expand Down