feat: Add streaming chat + scroll persistence; improve markdown & links
Backend
- /chat: support streaming via StreamingResponse; save full reply after stream ends. Non-stream path unchanged.
- ChatRequest: add stream flag (default false).
- GenerateTitleRequest: add model and use it instead of hardcoded llama3.
- ollama_client.chat_stream(): new async generator parsing Ollama streaming JSON (both formats).
- Remove response_model from /chat to allow streaming; non-stream still returns { reply }.
Electron
- Open external links in system browser (setWindowOpenHandler, shell.openExternal).
- New IPC: update-settings, open-external-link.
- Set minimum window size; preload exposes updateSettings and openExternalLink.
Frontend (React)
- Streaming UI with live chunking; sticky-bottom only when user at bottom.
- Per-session scroll persistence and robust restore.
- New message tip to jump to latest reply when scrolled up.
- Disable Send while sending; spinner.
- General Settings: stream output toggle; propagate model/stream changes.
- Apply color scheme at boot; extract colorSchemes helper.
- Sidebar UX tweaks and unread badges.
Markdown/rendering
- Code blocks: language title bar and wrapper.
- Tables: GitHub-style parsing, per-cell borders, rounded wrapper, spacing, alignment.
- Headings: remove blank line after h1-h4.
- <hr>: handle after tables; strip following whitespace.
- Links: target=_blank with icon and URL tooltip.
Styles
- Add styles for code/table wrappers, new-message tip, toggle, spinner; hover/active vars; narrower sidebar.
API notes / breaking changes
- /chat accepts stream=true and returns text/plain streamed chunks.
- generate-title now requires a model.
- Non-stream /chat response shape unchanged.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
|
||||
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
|
||||
const { app, BrowserWindow, Menu, ipcMain, shell } = require('electron')
|
||||
const path = require('path')
|
||||
const { is } = require('@electron-toolkit/utils')
|
||||
const fs = require('fs')
|
||||
@@ -44,6 +44,8 @@ async function createMainWindow () {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 720,
|
||||
minWidth: 680,
|
||||
minHeight: 300,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.cjs'),
|
||||
@@ -62,6 +64,11 @@ async function createMainWindow () {
|
||||
} else {
|
||||
await mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
|
||||
}
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
}
|
||||
|
||||
async function createSettingsWindow () {
|
||||
@@ -166,6 +173,16 @@ ipcMain.handle('set-setting', (event, key, value) => {
|
||||
return true
|
||||
})
|
||||
|
||||
ipcMain.handle('update-settings', (event, settings) => {
|
||||
appSettings = { ...appSettings, ...settings }
|
||||
saveSettings()
|
||||
return true
|
||||
})
|
||||
|
||||
ipcMain.on('open-external-link', (event, url) => {
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
|
||||
@@ -4,5 +4,11 @@ const { contextBridge, ipcRenderer } = require('electron')
|
||||
// Expose a secure API to the renderer process
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getSettings: () => ipcRenderer.invoke('get-settings'),
|
||||
setSetting: (key, value) => ipcRenderer.invoke('set-setting', key, value)
|
||||
setSetting: (key, value) => ipcRenderer.invoke('set-setting', key, value),
|
||||
updateSettings: (settings) => ipcRenderer.invoke('update-settings', settings),
|
||||
openExternalLink: (event) => {
|
||||
event.preventDefault();
|
||||
const url = event.currentTarget.href;
|
||||
ipcRenderer.send('open-external-link', url);
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user