Skip to content

Commit 4814eca

Browse files
committed
feat: sync sessions via backend
1 parent f759645 commit 4814eca

File tree

8 files changed

+140
-40
lines changed

8 files changed

+140
-40
lines changed

src/renderer/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useAtom, useAtomValue } from 'jotai'
1717
import * as atoms from './stores/atoms'
1818
import Sidebar from './Sidebar'
1919
import * as premiumActions from './stores/premiumActions'
20+
import { useLoadSessions } from './hooks/useLoadSessions'
2021

2122
function Main() {
2223
const spellCheck = useAtomValue(atoms.spellCheckAtom)
@@ -53,6 +54,7 @@ function Main() {
5354
}
5455

5556
export default function App() {
57+
useLoadSessions()
5658
useI18nEffect()
5759
premiumActions.useAutoValidate()
5860
useSystemLanguageWhenInit()

src/renderer/components/SessionList.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export default function SessionList(props: Props) {
5252
const oldIndex = sortedSessions.findIndex(s => s.id === activeId)
5353
const newIndex = sortedSessions.findIndex(s => s.id === overId)
5454
const newReversed = arrayMove(sortedSessions, oldIndex, newIndex)
55-
setSessions(atoms.sortSessions(newReversed))
55+
setSessions({
56+
ts: Date.now(),
57+
sessions: atoms.sortSessions(newReversed),
58+
})
5659
}
5760
}
5861
return (

src/renderer/hooks/useLoadSessions.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useEffect } from 'react'
2+
import { loadSessionsDump } from '@/packages/sync-sessions'
3+
import { replaceSessionsFromBackend } from '@/stores/sessionActions'
4+
5+
export function useLoadSessions() {
6+
useEffect(() => {
7+
;(async () => {
8+
try {
9+
replaceSessionsFromBackend(await loadSessionsDump())
10+
} catch (e) {
11+
console.error(e)
12+
}
13+
})()
14+
}, [])
15+
}
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ofetch } from 'ofetch'
2+
import { SessionsDump } from 'src/shared/types'
3+
4+
export async function loadSessionsDump() {
5+
return await ofetch<SessionsDump>('http://localhost:8080/api/chats-history', {
6+
method: 'GET',
7+
retry: 3,
8+
headers: {
9+
authorization: `Bearer 634fc0dee42cf0e8bb3e85de634db34e`,
10+
},
11+
})
12+
}
13+
14+
async function saveSessionsToBackend(dump: SessionsDump) {
15+
try {
16+
await fetch('http://localhost:8080/api/chats-history', {
17+
method: 'PUT',
18+
headers: {
19+
'content-type': 'application/json',
20+
authorization: `Bearer 634fc0dee42cf0e8bb3e85de634db34e`,
21+
},
22+
body: JSON.stringify(dump),
23+
})
24+
} catch (e) {
25+
console.error(e)
26+
}
27+
}
28+
29+
let saveTimer: ReturnType<typeof setTimeout> | undefined
30+
let scheduledDumpTs = 0
31+
32+
export function scheduleSaveSessionsToBackend(dump: SessionsDump) {
33+
if (dump.ts <= scheduledDumpTs) {
34+
return
35+
}
36+
37+
scheduledDumpTs = dump.ts
38+
clearTimeout(saveTimer)
39+
saveTimer = setTimeout(() => saveSessionsToBackend(dump), 500)
40+
}

src/renderer/storage/StoreStorage.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import platform from '@/packages/platform'
44

55
export enum StorageKey {
66
ChatSessions = 'chat-sessions',
7+
ChatSessionsTs = 'chat-sessions-ts',
78
Configs = 'configs',
89
Settings = 'settings',
910
MyCopilots = 'myCopilots',

src/renderer/stores/atoms.ts

+23-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { atom, SetStateAction } from 'jotai'
2-
import { Session, Toast, Settings, CopilotDetail, Message, SettingWindowTab
3-
} from '../../shared/types'
2+
import { Session, Toast, Settings, CopilotDetail, Message, SettingWindowTab, SessionsDump } from '../../shared/types'
43
import { selectAtom, atomWithStorage } from 'jotai/utils'
54
import { focusAtom } from 'jotai-optics'
65
import * as defaults from '../../shared/defaults'
76
import storage, { StorageKey } from '../storage'
87
import platform from '../packages/platform'
8+
import { scheduleSaveSessionsToBackend } from '../packages/sync-sessions'
99

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

4444
// sessions
4545

46+
const _sessionsTsAtom = atomWithStorage<number>(StorageKey.ChatSessionsTs, 0, storage)
4647
const _sessionsAtom = atomWithStorage<Session[]>(StorageKey.ChatSessions, [], storage)
4748
export const sessionsAtom = atom(
4849
(get) => {
4950
let sessions = get(_sessionsAtom)
5051
if (sessions.length === 0) {
5152
sessions = defaults.sessions()
5253
}
53-
return sessions
54+
return {
55+
ts: get(_sessionsTsAtom),
56+
sessions,
57+
}
5458
},
55-
(get, set, update: SetStateAction<Session[]>) => {
56-
const sessions = get(_sessionsAtom)
57-
let newSessions = typeof update === 'function' ? update(sessions) : update
58-
if (newSessions.length === 0) {
59-
newSessions = defaults.sessions()
59+
(get, set, update: SetStateAction<SessionsDump>) => {
60+
let newDump =
61+
typeof update === 'function' ? update({ ts: get(_sessionsTsAtom), sessions: get(_sessionsAtom) }) : update
62+
63+
if (newDump.sessions.length === 0) {
64+
newDump = {
65+
ts: Date.now(),
66+
sessions: defaults.sessions(),
67+
}
6068
}
61-
set(_sessionsAtom, newSessions)
69+
70+
set(_sessionsTsAtom, newDump.ts)
71+
set(_sessionsAtom, newDump.sessions)
72+
73+
scheduleSaveSessionsToBackend(newDump)
6274
}
6375
)
6476
export const sortedSessionsAtom = atom((get) => {
65-
return sortSessions(get(sessionsAtom))
77+
return sortSessions(get(sessionsAtom).sessions)
6678
})
6779

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

8799
export const currentSessionAtom = atom((get) => {
88100
const id = get(currentSessionIdAtom)
89-
const sessions = get(sessionsAtom)
101+
const { sessions } = get(sessionsAtom)
90102
let current = sessions.find((session) => session.id === id)
91103
if (!current) {
92104
return sessions[sessions.length - 1] // fallback to the last session

src/renderer/stores/sessionActions.ts

+50-28
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { getDefaultStore } from 'jotai'
2-
import {
3-
Settings,
4-
createMessage,
5-
Message,
6-
Session,
7-
} from '../../shared/types'
2+
import { Settings, createMessage, Message, Session, SessionsDump } from '../../shared/types'
83
import * as atoms from './atoms'
94
import * as promptFormat from '../packages/prompts'
105
import * as Sentry from '@sentry/react'
@@ -18,35 +13,41 @@ import { throttle } from 'lodash'
1813
import { countWord } from '@/packages/word-count'
1914
import { estimateTokensFromMessages } from '@/packages/token'
2015
import * as settingActions from './settingActions'
16+
import { scheduleSaveSessionsToBackend } from '@/packages/sync-sessions'
2117

2218
export function create(newSession: Session) {
2319
const store = getDefaultStore()
24-
store.set(atoms.sessionsAtom, (sessions) => [...sessions, newSession])
20+
store.set(atoms.sessionsAtom, ({ sessions }) => ({
21+
ts: Date.now(),
22+
sessions: [...sessions, newSession],
23+
}))
2524
switchCurrentSession(newSession.id)
2625
}
2726

2827
export function modify(update: Session) {
2928
const store = getDefaultStore()
30-
store.set(atoms.sessionsAtom, (sessions) =>
31-
sessions.map((s) => {
29+
store.set(atoms.sessionsAtom, ({ sessions }) => ({
30+
ts: Date.now(),
31+
sessions: sessions.map((s) => {
3232
if (s.id === update.id) {
3333
return update
3434
}
3535
return s
36-
})
37-
)
36+
}),
37+
}))
3838
}
3939

4040
export function modifyName(sessionId: string, name: string) {
4141
const store = getDefaultStore()
42-
store.set(atoms.sessionsAtom, (sessions) =>
43-
sessions.map((s) => {
42+
store.set(atoms.sessionsAtom, ({ sessions }) => ({
43+
ts: Date.now(),
44+
sessions: sessions.map((s) => {
4445
if (s.id === sessionId) {
4546
return { ...s, name, threadName: name }
4647
}
4748
return s
48-
})
49-
)
49+
}),
50+
}))
5051
}
5152

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

6768
export function remove(session: Session) {
6869
const store = getDefaultStore()
69-
store.set(atoms.sessionsAtom, (sessions) => sessions.filter((s) => s.id !== session.id))
70+
store.set(atoms.sessionsAtom, ({ sessions }) => ({
71+
ts: Date.now(),
72+
sessions: sessions.filter((s) => s.id !== session.id),
73+
}))
7074
}
7175

7276
export function clear(sessionId: string) {
@@ -84,29 +88,30 @@ export async function copy(source: Session) {
8488
const store = getDefaultStore()
8589
const newSession = { ...source }
8690
newSession.id = uuidv4()
87-
store.set(atoms.sessionsAtom, (sessions) => {
91+
store.set(atoms.sessionsAtom, ({ sessions }) => {
8892
let originIndex = sessions.findIndex((s) => s.id === source.id)
8993
if (originIndex < 0) {
9094
originIndex = 0
9195
}
9296
const newSessions = [...sessions]
9397
newSessions.splice(originIndex + 1, 0, newSession)
94-
return newSessions
98+
return { ts: Date.now(), sessions: newSessions }
9599
})
96100
}
97101

98102
export function getSession(sessionId: string) {
99103
const store = getDefaultStore()
100-
const sessions = store.get(atoms.sessionsAtom)
104+
const { sessions } = store.get(atoms.sessionsAtom)
101105
return sessions.find((s) => s.id === sessionId)
102106
}
103107

104108
export function insertMessage(sessionId: string, msg: Message) {
105109
const store = getDefaultStore()
106110
msg.wordCount = countWord(msg.content)
107111
msg.tokenCount = estimateTokensFromMessages([msg])
108-
store.set(atoms.sessionsAtom, (sessions) =>
109-
sessions.map((s) => {
112+
store.set(atoms.sessionsAtom, ({ sessions }) => ({
113+
ts: Date.now(),
114+
sessions: sessions.map((s) => {
110115
if (s.id === sessionId) {
111116
const newMessages = [...s.messages]
112117
newMessages.push(msg)
@@ -116,8 +121,8 @@ export function insertMessage(sessionId: string, msg: Message) {
116121
}
117122
}
118123
return s
119-
})
120-
)
124+
}),
125+
}))
121126
}
122127

123128
export function modifyMessage(sessionId: string, updated: Message, refreshCounting?: boolean) {
@@ -139,15 +144,16 @@ export function modifyMessage(sessionId: string, updated: Message, refreshCounti
139144
return m
140145
})
141146
}
142-
store.set(atoms.sessionsAtom, (sessions) =>
143-
sessions.map((s) => {
147+
store.set(atoms.sessionsAtom, ({ sessions }) => ({
148+
ts: Date.now(),
149+
sessions: sessions.map((s) => {
144150
if (s.id !== sessionId) {
145151
return s
146152
}
147153
s.messages = handle(s.messages)
148154
return { ...s }
149-
})
150-
)
155+
}),
156+
}))
151157
}
152158

153159
export async function submitNewUserMessage(params: {
@@ -329,11 +335,27 @@ export function initEmptyChatSession(): Session {
329335
}
330336
}
331337

332-
export function getSessions() {
338+
export function replaceSessionsFromBackend(newDump: SessionsDump) {
339+
const store = getDefaultStore()
340+
const currentDump = store.get(atoms.sessionsAtom)
341+
342+
if (newDump.ts > currentDump.ts) {
343+
store.set(atoms.sessionsAtom, newDump)
344+
} else if (newDump.ts < currentDump.ts) {
345+
scheduleSaveSessionsToBackend(currentDump)
346+
}
347+
}
348+
349+
export function getSessionsDump() {
333350
const store = getDefaultStore()
334351
return store.get(atoms.sessionsAtom)
335352
}
336353

354+
export function getSessions() {
355+
const store = getDefaultStore()
356+
return store.get(atoms.sessionsAtom).sessions
357+
}
358+
337359
export function getSortedSessions() {
338360
const store = getDefaultStore()
339361
return store.get(atoms.sortedSessionsAtom)

src/shared/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ export interface Session {
5353
copilotId?: string
5454
}
5555

56+
export interface SessionsDump {
57+
ts: number
58+
sessions: Session[]
59+
}
60+
5661
export function createMessage(role: MessageRole = MessageRoleEnum.User, content: string = ''): Message {
5762
return {
5863
id: uuidv4(),

0 commit comments

Comments
 (0)