Skip to content

Commit acdcfb9

Browse files
l3utterflyCopilot
andcommitted
- added alert dialog
- improved scroll logs handling - allow empty server and model path if user wants to use another llm engine Co-authored-by: Copilot <copilot@github.com>
1 parent 87ac3b6 commit acdcfb9

7 files changed

Lines changed: 123 additions & 106 deletions

File tree

index.html

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/main.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,13 @@ ipcMain.handle("server:stop", async (): Promise<string> => {
156156
} catch (err) {
157157
return `Failed to stop server: ${err}`;
158158
}
159+
});
160+
161+
ipcMain.handle('dialog:alert', (_event, title: string, message: string) => {
162+
return dialog.showMessageBox({
163+
type: 'info',
164+
title: title,
165+
message,
166+
buttons: ['OK'],
167+
});
159168
});

src/preload.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,9 @@ contextBridge.exposeInMainWorld("electronBridge", {
4747
* @returns the path of the selected model file, or null if the dialog was cancelled
4848
*/
4949
openFileDialog: () => ipcRenderer.invoke("dialog:openFile"),
50+
51+
/**
52+
* Show an alert dialog with a message and optional title.
53+
*/
54+
showAlert: (title: string, message: string) => ipcRenderer.invoke('dialog:alert', title, message),
5055
});

src/renderer.tsx

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,43 @@
1-
import React from 'react';
2-
import { createRoot } from 'react-dom/client';
3-
import LlmServerPanel from './screens/LLMServerPanel';
4-
import SettingsPage from './screens/SettingsPage';
1+
import React from "react";
2+
import { createRoot } from "react-dom/client";
3+
import LlmServerPanel from "./screens/LLMServerPanel";
4+
import SettingsPage from "./screens/SettingsPage";
55

66
function App() {
7-
const [page, setPage] = React.useState<"main" | "settings">("main");
8-
9-
const goToSettings = () => setPage("settings");
10-
const goBack = () => setPage("main");
11-
12-
if (page === "settings") {
13-
return <SettingsPage onBack={goBack} />;
14-
}
15-
16-
return <LlmServerPanel goToSettings={goToSettings} />;
7+
const [showSettings, setShowSettings] = React.useState(false);
8+
9+
// Increment settingsRefreshCounter to trigger a refresh in LlmServerPanel when coming back from settings page
10+
const [settingsRefreshCounter, setSettingsRefreshCounter] = React.useState(0);
11+
12+
return (
13+
<>
14+
<LlmServerPanel
15+
goToSettings={() => setShowSettings(true)}
16+
settingsRefreshCounter={settingsRefreshCounter}
17+
/>
18+
{showSettings && (
19+
<div
20+
style={{
21+
position: "fixed",
22+
inset: 0,
23+
zIndex: 100,
24+
backgroundColor: "rgba(0,0,0,0.85)",
25+
backdropFilter: "blur(4px)",
26+
}}
27+
>
28+
<SettingsPage
29+
onBack={() => {
30+
setShowSettings(false);
31+
setSettingsRefreshCounter((prev) => prev + 1);
32+
}}
33+
/>
34+
</div>
35+
)}
36+
</>
37+
);
1738
}
1839

19-
const container = document.getElementById('root');
40+
const container = document.getElementById("root");
2041
if (container) {
2142
createRoot(container).render(<App />);
22-
}
43+
}

src/screens/LLMServerPanel.tsx

Lines changed: 52 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const C = {
2525
successGlow: "rgba(52, 211, 153, 0.20)",
2626
};
2727

28-
export const LAYLA_SIGNALLING_URL = "https://layla-signalling-production.up.railway.app";
28+
const LAYLA_SIGNALLING_URL =
29+
"https://layla-signalling-production.up.railway.app";
2930
const WEBRTC_DATA_CHANNEL_LABEL = "layla-datachannel";
3031
const CHUNK_SIZE = 16_000;
3132
const MAX_SERVER_LOGS_TO_DISPLAY = 500;
@@ -46,27 +47,6 @@ interface LogEntry {
4647
msg: string;
4748
}
4849

49-
// ─── IPC Bridge (Electron main process) ─────────────────────────────────────────
50-
// This assumes you expose these via contextBridge/preload.
51-
// Adjust to match your actual preload API.
52-
53-
interface ElectronBridge {
54-
startServer: (
55-
serverPath: string,
56-
modelPath: string,
57-
visionModelPath: string,
58-
additionalArgs: string,
59-
) => Promise<void>;
60-
stopServer: () => Promise<void>;
61-
getDeviceName: () => Promise<string>;
62-
onServerLog: (callback: (log: string) => void) => () => void;
63-
}
64-
65-
function getElectronBridge(): ElectronBridge {
66-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
67-
return (window as any).electronBridge as ElectronBridge;
68-
}
69-
7050
// ─── Helpers ────────────────────────────────────────────────────────────────────
7151

7252
const generateTimestamp = (offsetMs = 0): string => {
@@ -249,12 +229,18 @@ const LogViewer: React.FC<{ logs: LogEntry[] }> = ({ logs }) => {
249229

250230
useEffect(() => {
251231
if (expanded && scrollRef.current) {
252-
setTimeout(() => {
253-
scrollRef.current?.scrollTo({
254-
top: scrollRef.current.scrollHeight,
255-
behavior: "smooth",
256-
});
257-
}, 100);
232+
const el = scrollRef.current;
233+
const isNearBottom =
234+
el.scrollHeight - el.scrollTop - el.clientHeight < 100;
235+
236+
if (isNearBottom) {
237+
setTimeout(() => {
238+
scrollRef.current?.scrollTo({
239+
top: scrollRef.current.scrollHeight,
240+
behavior: "smooth",
241+
});
242+
}, 100);
243+
}
258244
}
259245
}, [logs, expanded]);
260246

@@ -329,9 +315,10 @@ const QrModal: React.FC<{
329315

330316
// ─── Main Component ─────────────────────────────────────────────────────────────
331317

332-
const LlmServerPanel: React.FC<{ goToSettings: () => void }> = ({
333-
goToSettings,
334-
}) => {
318+
const LlmServerPanel: React.FC<{
319+
goToSettings: () => void;
320+
settingsRefreshCounter: number; // used to trigger settings reload when coming back from settings page
321+
}> = ({ goToSettings, settingsRefreshCounter }) => {
335322
// ── State ──
336323
const [running, setRunning] = useState(false);
337324
const [status, setStatus] = useState("Idle");
@@ -379,7 +366,10 @@ const LlmServerPanel: React.FC<{ goToSettings: () => void }> = ({
379366

380367
const addLog = (level: LogType, msg: string) => {
381368
const ts = generateTimestamp();
382-
setLogs((prev) => [...prev.slice(-(MAX_SERVER_LOGS_TO_DISPLAY - 1)), { ts, type: level, msg }]);
369+
setLogs((prev) => [
370+
...prev.slice(-(MAX_SERVER_LOGS_TO_DISPLAY - 1)),
371+
{ ts, type: level, msg },
372+
]);
383373
};
384374

385375
// ── WebRTC (browser native) ──
@@ -711,34 +701,29 @@ const LlmServerPanel: React.FC<{ goToSettings: () => void }> = ({
711701
// it is important to update the running state immediately to avoid async function calls trying to use the old RTC connection after we've initiated shutdown
712702
runningRef.current = false;
713703

714-
await getElectronBridge().stopServer();
704+
await window.electronBridge.stopServer();
715705
};
716706

717707
const startServer = async () => {
718-
if (!modelPathRef.current) {
719-
throw new Error(
720-
"Model path is not set. Please set the model path in settings.",
708+
if (modelPathRef.current && localServerPathRef.current) {
709+
setStatus("Starting llama.cpp server...");
710+
addLog("INFO", "Starting llama.cpp server…");
711+
712+
await window.electronBridge.startServer(
713+
localServerPathRef.current,
714+
modelPathRef.current,
715+
visionModelPathRef.current || "",
716+
additionalArgsRef.current || "",
721717
);
722-
}
723-
if (!localServerPathRef.current) {
724-
throw new Error(
725-
"Local server path is not set. Please set it in settings.",
718+
} else {
719+
addLog(
720+
"WARN",
721+
"Model or server path is not set. Skipping LLM server start (assume another server is running).",
726722
);
727723
}
728724

729-
setStatus("Starting llama.cpp server...");
730-
addLog("INFO", "Starting llama.cpp server…");
731-
732-
await getElectronBridge().startServer(
733-
localServerPathRef.current,
734-
modelPathRef.current,
735-
visionModelPathRef.current || "",
736-
additionalArgsRef.current || "",
737-
);
738-
739725
runningRef.current = true;
740726

741-
// give it a second for the animation to finish to feel more polished
742727
setTimeout(() => {
743728
setShowQrCode(true);
744729
}, 1000);
@@ -769,8 +754,7 @@ const LlmServerPanel: React.FC<{ goToSettings: () => void }> = ({
769754
// ── Load settings ──
770755

771756
const loadSettings = async () => {
772-
const bridge = getElectronBridge();
773-
const computerName = await bridge.getDeviceName();
757+
const computerName = await window.electronBridge.getDeviceName();
774758

775759
const settings = await UserSettingsService.getMultipleSettings([
776760
UserSettingKey.MODEL_PATH,
@@ -827,6 +811,21 @@ const LlmServerPanel: React.FC<{ goToSettings: () => void }> = ({
827811
};
828812
}, []);
829813

814+
useEffect(() => {
815+
// this logic skips the initial load when app starts
816+
if (settingsRefreshCounter > 0) {
817+
// reload settings (they won't be applied if the server is already running, but will be picked up on restart)
818+
loadSettings()
819+
.then(() => {
820+
window.electronBridge.showAlert(
821+
"Settings updated",
822+
"Your changes have been saved. If the server is currently running, please restart it to apply the new settings.",
823+
);
824+
})
825+
.catch((e) => addLog("ERROR", `Failed to load settings: ${e.message}`));
826+
}
827+
}, [settingsRefreshCounter]);
828+
830829
// ── Init ──
831830

832831
useEffect(() => {

src/screens/SettingsPage.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ const SETTING_SECTIONS: {
6969
{
7070
key: UserSettingKey.SERVER_SECRET_KEY,
7171
label: "Server Secret Key",
72-
description:
73-
"Secret key used to authenticate with the server.",
72+
description: "Secret key used to authenticate with the server.",
7473
type: "text",
7574
placeholder: "Enter your server secret key",
7675
tag: "SECRET",
@@ -601,11 +600,17 @@ const SettingRow: React.FC<SettingRowProps> = ({
601600
/>
602601
) : (
603602
<div className="sp-file-row">
604-
<div
603+
<input
605604
className={`sp-file-display ${value ? "sp-file-display--filled" : "sp-file-display--empty"}`}
606-
>
607-
{value || "No file selected"}
608-
</div>
605+
value={value}
606+
onChange={(e) => onChange(meta.key, e.target.value)}
607+
placeholder={meta.placeholder ?? ""}
608+
onFocus={() => setFocused(true)}
609+
onBlur={() => setFocused(false)}
610+
autoComplete="off"
611+
autoCorrect="off"
612+
spellCheck={false}
613+
/>
609614
<Btn label="Browse" onPress={pickFile} variant="outline" />
610615
</div>
611616
)}
@@ -635,11 +640,7 @@ const Toast: React.FC<{ message: string }> = ({ message }) => {
635640
? "sp-toast--exiting"
636641
: "";
637642

638-
return (
639-
<div className={`sp-toast ${animClass}`}>
640-
{message}
641-
</div>
642-
);
643+
return <div className={`sp-toast ${animClass}`}>{message}</div>;
643644
};
644645

645646
// ─── Main page ──────────────────────────────────────────────────────────────────
@@ -648,7 +649,7 @@ interface SettingsPageProps {
648649
}
649650

650651
const SettingsPage: React.FC<SettingsPageProps> = ({ onBack }) => {
651-
const [version, setVersion] = useState<string>('1.0.0');
652+
const [version, setVersion] = useState<string>("1.0.0");
652653
const [settings, setSettings] = useState<Record<
653654
UserSettingKey,
654655
string
@@ -716,7 +717,7 @@ const SettingsPage: React.FC<SettingsPageProps> = ({ onBack }) => {
716717
try {
717718
for (const k of Object.values(UserSettingKey)) {
718719
const val = settings[k];
719-
if (val === "" || val == null) {
720+
if (val == null) {
720721
await UserSettingsService.removeSetting(k);
721722
} else {
722723
await UserSettingsService.saveSetting(k, val as any);
@@ -761,7 +762,9 @@ const SettingsPage: React.FC<SettingsPageProps> = ({ onBack }) => {
761762
Back
762763
</button>
763764
<div>
764-
<h1 className="sp-heading">Settings&nbsp;<span style={{fontSize: 12}}>(v{version})</span></h1>
765+
<h1 className="sp-heading">
766+
Settings&nbsp;<span style={{ fontSize: 12 }}>(v{version})</span>
767+
</h1>
765768
<div className="sp-heading-sub">
766769
Manage server configuration and model preferences
767770
</div>
@@ -833,4 +836,4 @@ const SettingsPage: React.FC<SettingsPageProps> = ({ onBack }) => {
833836
);
834837
};
835838

836-
export default SettingsPage;
839+
export default SettingsPage;

src/types/types.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ export interface ElectronAPI {
22
getAppVersion: () => Promise<string>;
33
openExternal: (url: string) => void;
44
openFileDialog: () => Promise<string | null>;
5-
startServer: (serverPath: string, modelPath: string, additionalArgs: string) => Promise<void>;
5+
startServer: (serverPath: string, modelPath: string, visionModelPath: string, additionalArgs: string) => Promise<void>;
66
stopServer: () => Promise<void>;
77
getDeviceName: () => Promise<string>;
88
onServerStdout: (callback: (data: string) => void) => () => void; // returns a cleanup function
99
onServerStderr: (callback: (data: string) => void) => () => void; // returns a cleanup function
10+
showAlert: (title: string, message: string) => Promise<void>;
1011
}
1112

1213
declare global {

0 commit comments

Comments
 (0)