Skip to content

Commit

Permalink
feat: sync sessions via backend
Browse files Browse the repository at this point in the history
  • Loading branch information
vslinko committed Jan 29, 2025
1 parent f759645 commit ab74fbb
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 42 deletions.
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
9 changes: 6 additions & 3 deletions src/renderer/components/SessionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ export default function SessionList(props: Props) {
const activeId = event.active.id
const overId = event.over.id
if (activeId !== overId) {
const oldIndex = sortedSessions.findIndex(s => s.id === activeId)
const newIndex = sortedSessions.findIndex(s => s.id === overId)
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
80 changes: 52 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,29 @@ export function initEmptyChatSession(): Session {
}
}

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

console.log(newDump.ts, currentDump.ts)

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

0 comments on commit ab74fbb

Please sign in to comment.