Skip to content

Commit

Permalink
Session management WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Kvadratni committed Dec 4, 2024
1 parent 6c8221b commit 805a256
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 20 deletions.
16 changes: 15 additions & 1 deletion ui/desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion ui/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"react-router-dom": "^6.28.0",
"react-syntax-highlighter": "^15.6.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3"
}
}
65 changes: 53 additions & 12 deletions ui/desktop/src/ChatWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React, { useEffect, useRef, useState } from 'react';
import { Message, useChat } from './ai-sdk-fork/useChat';
import { Route, Routes, Navigate } from 'react-router-dom';
import { getApiUrl } from './config';
import { Card } from './components/ui/card';
import { ScrollArea } from './components/ui/scroll-area';
import React, {useEffect, useRef, useState} from 'react';
import {Message,useChat} from './ai-sdk-fork/useChat';
import {Navigate, Route, Routes} from 'react-router-dom';
import {getApiUrl} from './config';
import {Card} from './components/ui/card';
import {ScrollArea} from './components/ui/scroll-area';
import Splash from './components/Splash';
import GooseMessage from './components/GooseMessage';
import UserMessage from './components/UserMessage';
import Input from './components/Input';
import MoreMenu from './components/MoreMenu';
import {Bird} from './components/ui/icons';
import LoadingGoose from './components/LoadingGoose';
import { ApiKeyWarning } from './components/ApiKeyWarning';
import {ApiKeyWarning} from './components/ApiKeyWarning';

import { askAi, getPromptTemplates } from './utils/askAI';
import WingToWing, { Working } from './components/WingToWing';

Expand Down Expand Up @@ -89,8 +91,23 @@ function ChatContent({
c.id === selectedChatId ? { ...c, messages } : c
);
setChats(updatedChats);
const currentChat = chats.find(chat => chat.id === selectedChatId);
if (currentChat) {
const sessionToSave = {
messages: currentChat.messages,
directory: window.appConfig.get("GOOSE_WORKING_DIR")
};
saveSession(sessionToSave);
}
}, [messages, selectedChatId]);

// Function to save a session
const saveSession = (session) => {
if(session.messages === undefined || session.messages.length === 0) return
window.electron.saveSession(session);
};


const initialQueryAppended = useRef(false);
useEffect(() => {
if (initialQuery && !initialQueryAppended.current) {
Expand Down Expand Up @@ -149,11 +166,11 @@ function ChatContent({
}
}),
};

const updatedMessages = [...messages.slice(0, -1), newLastMessage];
setMessages(updatedMessages);
}

};

return (
Expand Down Expand Up @@ -225,6 +242,24 @@ export default function ChatWindow() {
window.electron.createChatWindow();
};

// Function to get a session by ID
const getSession = async (sessionId) => {
try {
return window.electron.getSession(sessionId);
} catch (error) {
console.error('Failed to load session:', error);
}
};

const convertSessionToChat = (session) => {
const chat = {
id: 1,
title: session.name,
messages: session.messages,
};
return chat;
}

// Add keyboard shortcut handler
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -259,6 +294,12 @@ export default function ChatWindow() {
title: initialQuery || 'Chat 1',
messages: initialHistory.length > 0 ? initialHistory : [],
};
const sessionId = window.appConfig.get("GOOSE_SESSION_ID");
getSession(sessionId).then((session) => {
if (session) {
setChats([convertSessionToChat(session)]);
}
});
return [firstChat];
});

Expand Down Expand Up @@ -308,11 +349,11 @@ export default function ChatWindow() {
<Route path="*" element={<Navigate to="/chat/1" replace />} />
</Routes>
</div>

<WingToWing onExpand={toggleMode} progressMessage={progressMessage} working={working} />

</>
)}
</div>
);
}
}
6 changes: 5 additions & 1 deletion ui/desktop/src/LauncherWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import React, { useState, useRef } from 'react';
declare global {
interface Window {
electron: {
getConfig(): object;
getSession(arg0: string): object;
logInfo(arg0: string): object;
saveSession(arg0: { name: string; messages: Array<object>; directory: string }): object;
hideWindow: () => void;
createChatWindow: (query: string) => void;
};
Expand Down Expand Up @@ -41,4 +45,4 @@ export default function SpotlightWindow() {
</form>
</div>
);
}
}
70 changes: 66 additions & 4 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import started from "electron-squirrel-startup";
import log from './utils/logger';
import { exec } from 'child_process';
import { addRecentDir, loadRecentDirs } from './utils/recentDirs';
import { loadSessions, saveSession, clearAllSessions } from './utils/sessionManager';

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) app.quit();
Expand Down Expand Up @@ -87,7 +88,7 @@ const createLauncher = () => {
let windowCounter = 0;
const windowMap = new Map<number, BrowserWindow>();

const createChat = async (app, query?: string, dir?: string) => {
const createChat = async (app, query?: string, dir?: string, sessionId?: string) => {

const [port, working_dir] = await startGoosed(app, dir);
const mainWindow = new BrowserWindow({
Expand All @@ -102,7 +103,7 @@ const createChat = async (app, query?: string, dir?: string) => {
icon: path.join(__dirname, '../images/icon'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
additionalArguments: [JSON.stringify({ ...appConfig, GOOSE_SERVER__PORT: port, GOOSE_WORKING_DIR: working_dir })],
additionalArguments: [JSON.stringify({ ...appConfig, GOOSE_SERVER__PORT: port, GOOSE_WORKING_DIR: working_dir, GOOSE_SESSION_ID: sessionId })],
},
});

Expand Down Expand Up @@ -211,6 +212,17 @@ const buildRecentFilesMenu = () => {
}));
};

// Add Recent Sessions submenu
const buildRecentSessionsMenu = () => {
const sessions = loadSessions();
return sessions.map(session => ({
label: session.name,
click: () => {
createChat(app, undefined, session.directory, session.name);
}
}));
};

const openDirectoryDialog = async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory']
Expand Down Expand Up @@ -244,6 +256,23 @@ app.whenReady().then(async () => {
},
}));

const recentSessionsSubmenu = buildRecentSessionsMenu();
if (recentSessionsSubmenu.length > 0) {
fileMenu.submenu.append(new MenuItem({ type: 'separator' }));
fileMenu.submenu.append(new MenuItem({
label: 'Recent Sessions',
submenu: recentSessionsSubmenu
}));
}

// Add option to clear session history
fileMenu.submenu.append(new MenuItem({
label: 'Clear Session History',
click() {
clearAllSessions();
},
}));

// Add Recent Files submenu
const recentFilesSubmenu = buildRecentFilesMenu();
if (recentFilesSubmenu.length > 0) {
Expand Down Expand Up @@ -274,8 +303,41 @@ app.whenReady().then(async () => {
}
});

ipcMain.on('save-session', (_, session) => {
try {
return saveSession(session);
} catch (error) {
console.error('Failed to save session:', error);
throw error;
}
});

ipcMain.on('get-session', (_, sessionId) => {
try {
return loadSessions().find(session => session.name === sessionId);
} catch (error) {
console.error('Failed to load sessions:', error);
throw error;
}
});

ipcMain.on('create-chat-window', (_, query) => {
createChat(app, query);
createChat(app, query);
});

ipcMain.on('clear-session-history', () => {
// Clear all stored session data
try {
// We'll simulate clearing session data - implement this using your session storage logic
clearAllSessions();
console.log('All session history cleared');
// Optionally notify all open chat windows to update/reset
windowMap.forEach(win => {
win.webContents.send('session-history-cleared');
});
} catch (error) {
console.error('Failed to clear session history:', error);
}
});

ipcMain.on('directory-chooser', (_) => {
Expand Down Expand Up @@ -330,4 +392,4 @@ app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
});
2 changes: 2 additions & 0 deletions ui/desktop/src/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ contextBridge.exposeInMainWorld('electron', {
logInfo: (txt) => ipcRenderer.send('logInfo', txt),
showNotification: (data) => ipcRenderer.send('notify', data),
createWingToWingWindow: (query) => ipcRenderer.send('create-wing-to-wing-window', query),
saveSession: (session) => ipcRenderer.send('save-session', session),
getSession: (sessionId) => ipcRenderer.send('get-session', sessionId),
openInChrome: (url) => ipcRenderer.send('open-in-chrome', url),
fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url),
})
4 changes: 3 additions & 1 deletion ui/desktop/src/types/electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ interface IElectronAPI {
GOOSE_API_HOST: string;
apiCredsMissing: boolean;
};
getSession: (sessionId: string) => object;
saveSession: (session: { name: string; messages: Array<object>; directory: string }) => string;
}

declare global {
interface Window {
electron: IElectronAPI;
}
}
}
68 changes: 68 additions & 0 deletions ui/desktop/src/utils/sessionManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import fs from 'fs';
import path from 'path';
import { app } from 'electron';

const SESSIONS_PATH = path.join(app.getPath('userData'), 'sessions');
if (!fs.existsSync(SESSIONS_PATH)) {
fs.mkdirSync(SESSIONS_PATH);
}

interface Session {
name: string; // Derived from a synopsis of the conversation
messages: Array<{
id: number;
role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool';
content: string;
}>;
directory: string;
}

function generateSessionName(messages: object[]): string {
// Create a session name based on the first message or a combination of initial messages
if (messages === undefined || messages.length === 0) return 'empty_session';
return messages[0].content.split(' ').slice(0, 5).join(' ');
}

export function saveSession(session: Session): string {
try {
const sessionData = {
...session,
name: generateSessionName(session.messages)
};
const filePath = path.join(SESSIONS_PATH, `${sessionData.name}.json`);
fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2));
console.log('Session saved:', sessionData);
return sessionData.name;
} catch (error) {
console.error('Error saving session:', error);
}
}

export function loadSessions(): Session[] {
try {
console.log('Attempting to load sessions from:', SESSIONS_PATH);
const files = fs.readdirSync(SESSIONS_PATH);
if (files.length === 0) {
console.warn('No session files found in directory');
} else {
console.log('Session files found:', files);
}
return files.map(file => {
const data = fs.readFileSync(path.join(SESSIONS_PATH, file), 'utf8');
return JSON.parse(data) as Session;
});
} catch (error) {
console.error('Error loading sessions:', error);
return [];
}
}

export function clearAllSessions(): void {
try {
const files = fs.readdirSync(SESSIONS_PATH);
files.forEach(file => fs.unlinkSync(path.join(SESSIONS_PATH, file)));
console.log('All sessions cleared');
} catch (error) {
console.error('Error clearing sessions:', error);
}
}

0 comments on commit 805a256

Please sign in to comment.