Skip to content

Commit

Permalink
Electron UI Dark Mode (#552)
Browse files Browse the repository at this point in the history
Adds a dark mode toggle to the electron UI
  • Loading branch information
hlucco authored Jan 14, 2025
1 parent c44d484 commit e0ddb7a
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 10 deletions.
27 changes: 27 additions & 0 deletions ts/packages/shell/src/main/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,32 @@ class ShellSetTopMostCommandHandler implements CommandHandlerNoParams {
}
}

function getThemeCommandHandlers(): CommandHandlerTable {
return {
description: "Set the theme",
commands: {
light: {
description: "Set the theme to light",
run: async (context: ActionContext<ShellContext>) => {
context.sessionContext.agentContext.settings.set(
"darkMode",
false,
);
},
},
dark: {
description: "Set the theme to dark",
run: async (context: ActionContext<ShellContext>) => {
context.sessionContext.agentContext.settings.set(
"darkMode",
true,
);
},
},
},
};
}

class ShellOpenWebContentView implements CommandHandler {
public readonly description = "Show a new Web Content view";
public readonly parameters = {
Expand Down Expand Up @@ -216,6 +242,7 @@ const handlers: CommandHandlerTable = {
open: new ShellOpenWebContentView(),
close: new ShellCloseWebContentView(),
localWhisper: getLocalWhisperCommandHandlers(),
theme: getThemeCommandHandlers(),
},
};

Expand Down
2 changes: 2 additions & 0 deletions ts/packages/shell/src/main/shellSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class ShellSettings
public onToggleTopMost: EmptyFunction | null;
public onOpenInlineBrowser: ((targetUrl: URL) => void) | null;
public onCloseInlineBrowser: EmptyFunction | null;
public darkMode: boolean;

public get width(): number {
return this.size[0] ?? defaultSettings.size[0];
Expand Down Expand Up @@ -89,6 +90,7 @@ export class ShellSettings
this.onToggleTopMost = null;
this.onOpenInlineBrowser = null;
this.onCloseInlineBrowser = null;
this.darkMode = settings.darkMode;
}

public static get filePath(): string {
Expand Down
2 changes: 2 additions & 0 deletions ts/packages/shell/src/main/shellSettingsType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type ShellSettingsType = {
devUI: boolean;
partialCompletion: boolean;
disallowedDisplayType: DisplayType[];
darkMode: boolean;
};

export const defaultSettings: ShellSettingsType = {
Expand All @@ -36,4 +37,5 @@ export const defaultSettings: ShellSettingsType = {
devUI: false,
partialCompletion: true,
disallowedDisplayType: [],
darkMode: false,
};
11 changes: 11 additions & 0 deletions ts/packages/shell/src/renderer/assets/settingsStyles.less
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,17 @@
font-size: 12px;
}

.settings-button {
margin-left: 24px;
border: none;
background-color: #fff;
border-radius: 5px;

svg {
margin-right: 5px;
}
}

.settings-container {
margin: 20px;
}
42 changes: 42 additions & 0 deletions ts/packages/shell/src/renderer/assets/styles.less
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,48 @@ body {
font-size: 14px;
}

.dark-mode {
background-color: #333;

.action-container {
background-color: #222;
}

.chat-container {
background-color: #222;

.scroll_enabled {
background-color: #222;
}
}

.replacement-container {
background-color: #222;
}

.chat-input {
background-color: #444;
border: 1px solid #444;

.user-textarea {
color: white;
}

.chat-input-button {
color: white;
}

label {
color: white;
}

.autocomplete-container {
background-color: #444;
color: white;
}
}
}

.wrapper {
height: 100%;
width: 100%;
Expand Down
30 changes: 20 additions & 10 deletions ts/packages/shell/src/renderer/src/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,16 @@ export function iconChevronRight() {
return createSVGElement(path);
}

export function iconMicrophone() {
export function iconMicrophone(fillColor: string = "currentColor") {
const path = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>Start speech recognition (Alt+M).</title>
<path d="M8.5 10.5C9.16304 10.5 9.79893 10.2366 10.2678 9.76777C10.7366 9.29893 11 8.66304 11 8V3.5C11 2.83696 10.7366 2.20107 10.2678 1.73223C9.79893 1.26339 9.16304 1 8.5 1C7.83696 1 7.20107 1.26339 6.73223 1.73223C6.26339 2.20107 6 2.83696 6 3.5V8C6 8.66304 6.26339 9.29893 6.73223 9.76777C7.20107 10.2366 7.83696 10.5 8.5 10.5ZM7 3.5C7 3.10218 7.15804 2.72064 7.43934 2.43934C7.72064 2.15804 8.10218 2 8.5 2C8.89782 2 9.27936 2.15804 9.56066 2.43934C9.84196 2.72064 10 3.10218 10 3.5V8C10 8.39782 9.84196 8.77936 9.56066 9.06066C9.27936 9.34196 8.89782 9.5 8.5 9.5C8.10218 9.5 7.72064 9.34196 7.43934 9.06066C7.15804 8.77936 7 8.39782 7 8V3.5ZM9 12.472V14H11V15H6V14H8V12.472C6.89998 12.349 5.88387 11.8249 5.14594 10.9999C4.40801 10.1749 4.00003 9.10688 4 8H5C5 8.92826 5.36875 9.8185 6.02513 10.4749C6.6815 11.1313 7.57174 11.5 8.5 11.5C9.42826 11.5 10.3185 11.1313 10.9749 10.4749C11.6313 9.8185 12 8.92826 12 8H13C13 9.10688 12.592 10.1749 11.8541 10.9999C11.1161 11.8249 10.1 12.349 9 12.472V12.472Z" fill="#1F1F1F" />
<path d="M8.5 10.5C9.16304 10.5 9.79893 10.2366 10.2678 9.76777C10.7366 9.29893 11 8.66304 11 8V3.5C11 2.83696 10.7366 2.20107 10.2678 1.73223C9.79893 1.26339 9.16304 1 8.5 1C7.83696 1 7.20107 1.26339 6.73223 1.73223C6.26339 2.20107 6 2.83696 6 3.5V8C6 8.66304 6.26339 9.29893 6.73223 9.76777C7.20107 10.2366 7.83696 10.5 8.5 10.5ZM7 3.5C7 3.10218 7.15804 2.72064 7.43934 2.43934C7.72064 2.15804 8.10218 2 8.5 2C8.89782 2 9.27936 2.15804 9.56066 2.43934C9.84196 2.72064 10 3.10218 10 3.5V8C10 8.39782 9.84196 8.77936 9.56066 9.06066C9.27936 9.34196 8.89782 9.5 8.5 9.5C8.10218 9.5 7.72064 9.34196 7.43934 9.06066C7.15804 8.77936 7 8.39782 7 8V3.5ZM9 12.472V14H11V15H6V14H8V12.472C6.89998 12.349 5.88387 11.8249 5.14594 10.9999C4.40801 10.1749 4.00003 9.10688 4 8H5C5 8.92826 5.36875 9.8185 6.02513 10.4749C6.6815 11.1313 7.57174 11.5 8.5 11.5C9.42826 11.5 10.3185 11.1313 10.9749 10.4749C11.6313 9.8185 12 8.92826 12 8H13C13 9.10688 12.592 10.1749 11.8541 10.9999C11.1161 11.8249 10.1 12.349 9 12.472V12.472Z" fill=${fillColor} />
</svg>`;
return createSVGElement(path);
}

export function iconMicrophoneDisabled() {
const path = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
export function iconMicrophoneDisabled(fillColor: string = "currentColor") {
const path = `<svg viewBox="0 0 16 16" fill=${fillColor} xmlns="http://www.w3.org/2000/svg">
<title>Speech recogintion disabled. Have you configured speech API access?</title>
<g clip-path="url(#clip0_820_90505)">
<path opacity="0.1" d="M12 8.5C12.6922 8.5 13.3689 8.70527 13.9445 9.08986C14.5201 9.47444 14.9687 10.0211 15.2336 10.6606C15.4985 11.3001 15.5678 12.0039 15.4327 12.6828C15.2977 13.3618 14.9644 13.9854 14.4749 14.4749C13.9854 14.9644 13.3618 15.2977 12.6828 15.4327C12.0039 15.5678 11.3001 15.4985 10.6606 15.2336C10.0211 14.9687 9.47444 14.5201 9.08986 13.9445C8.70527 13.3689 8.5 12.6922 8.5 12C8.5 11.0717 8.86875 10.1815 9.52513 9.52513C10.1815 8.86875 11.0717 8.5 12 8.5V8.5Z" fill="#C50F1F" />
Expand All @@ -85,13 +85,13 @@ export function iconMicrophoneDisabled() {
return createSVGElement(path);
}

export function iconMicrophoneListening() {
const path = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
export function iconMicrophoneListening(fillColor: string = "currentColor") {
const path = `<svg viewBox="0 0 16 16" fill=${fillColor} xmlns="http://www.w3.org/2000/svg">
<title>Speech recognition in progress.</title>
<g opacity="0.75">
<path d="M12.411 2.663C12.7958 3.23817 13.0008 3.91487 12.9997 4.60691C12.9986 5.29896 12.7916 5.97502 12.405 6.549L11.68 5.8C11.8931 5.42924 12.0036 5.00841 12 4.58079C11.9965 4.15317 11.8792 3.73421 11.66 3.367L12.411 2.663ZM3.859 1.977L3.122 1.285C2.38337 2.25157 1.98855 3.43706 2.00014 4.6535C2.01173 5.86993 2.42908 7.04768 3.186 8L3.9 7.268C3.32316 6.50858 3.00747 5.58294 3.00008 4.62931C2.99269 3.67569 3.294 2.74526 3.859 1.977V1.977ZM4.589 2.667C4.20605 3.24199 4.00264 3.91783 4.0046 4.60867C4.00656 5.29951 4.2138 5.97419 4.6 6.547L5.32 5.8C5.10691 5.42924 4.99644 5.00841 4.99995 4.58079C5.00347 4.15317 5.12084 3.73421 5.34 3.367L4.589 2.667ZM13.878 1.285H13.873L13.136 1.977C13.7004 2.74517 14.0016 3.67509 13.9948 4.62831C13.988 5.58152 13.6734 6.50702 13.098 7.267L13.814 8C14.5709 7.04768 14.9883 5.86993 14.9999 4.6535C15.0115 3.43706 14.6166 2.25157 13.878 1.285V1.285Z" fill="#1F1F1F" />
<path d="M12.411 2.663C12.7958 3.23817 13.0008 3.91487 12.9997 4.60691C12.9986 5.29896 12.7916 5.97502 12.405 6.549L11.68 5.8C11.8931 5.42924 12.0036 5.00841 12 4.58079C11.9965 4.15317 11.8792 3.73421 11.66 3.367L12.411 2.663ZM3.859 1.977L3.122 1.285C2.38337 2.25157 1.98855 3.43706 2.00014 4.6535C2.01173 5.86993 2.42908 7.04768 3.186 8L3.9 7.268C3.32316 6.50858 3.00747 5.58294 3.00008 4.62931C2.99269 3.67569 3.294 2.74526 3.859 1.977V1.977ZM4.589 2.667C4.20605 3.24199 4.00264 3.91783 4.0046 4.60867C4.00656 5.29951 4.2138 5.97419 4.6 6.547L5.32 5.8C5.10691 5.42924 4.99644 5.00841 4.99995 4.58079C5.00347 4.15317 5.12084 3.73421 5.34 3.367L4.589 2.667ZM13.878 1.285H13.873L13.136 1.977C13.7004 2.74517 14.0016 3.67509 13.9948 4.62831C13.988 5.58152 13.6734 6.50702 13.098 7.267L13.814 8C14.5709 7.04768 14.9883 5.86993 14.9999 4.6535C15.0115 3.43706 14.6166 2.25157 13.878 1.285V1.285Z" fill=${fillColor} />
</g>
<path d="M8.5 10.5C9.16304 10.5 9.79893 10.2366 10.2678 9.76777C10.7366 9.29893 11 8.66304 11 8V3.5C11 2.83696 10.7366 2.20107 10.2678 1.73223C9.79893 1.26339 9.16304 1 8.5 1C7.83696 1 7.20107 1.26339 6.73223 1.73223C6.26339 2.20107 6 2.83696 6 3.5V8C6 8.66304 6.26339 9.29893 6.73223 9.76777C7.20107 10.2366 7.83696 10.5 8.5 10.5ZM7 3.5C7 3.10218 7.15804 2.72064 7.43934 2.43934C7.72064 2.15804 8.10218 2 8.5 2C8.89782 2 9.27936 2.15804 9.56066 2.43934C9.84196 2.72064 10 3.10218 10 3.5V8C10 8.39782 9.84196 8.77936 9.56066 9.06066C9.27936 9.34196 8.89782 9.5 8.5 9.5C8.10218 9.5 7.72064 9.34196 7.43934 9.06066C7.15804 8.77936 7 8.39782 7 8V3.5ZM9 12.472V14H11V15H6V14H8V12.472C6.89998 12.349 5.88387 11.8249 5.14594 10.9999C4.40801 10.1749 4.00003 9.10688 4 8H5C5 8.92826 5.36875 9.8185 6.02513 10.4749C6.6815 11.1313 7.57174 11.5 8.5 11.5C9.42826 11.5 10.3185 11.1313 10.9749 10.4749C11.6313 9.8185 12 8.92826 12 8H13C13 9.10688 12.592 10.1749 11.8541 10.9999C11.1161 11.8249 10.1 12.349 9 12.472V12.472Z" fill="#1F1F1F" />
<path d="M8.5 10.5C9.16304 10.5 9.79893 10.2366 10.2678 9.76777C10.7366 9.29893 11 8.66304 11 8V3.5C11 2.83696 10.7366 2.20107 10.2678 1.73223C9.79893 1.26339 9.16304 1 8.5 1C7.83696 1 7.20107 1.26339 6.73223 1.73223C6.26339 2.20107 6 2.83696 6 3.5V8C6 8.66304 6.26339 9.29893 6.73223 9.76777C7.20107 10.2366 7.83696 10.5 8.5 10.5ZM7 3.5C7 3.10218 7.15804 2.72064 7.43934 2.43934C7.72064 2.15804 8.10218 2 8.5 2C8.89782 2 9.27936 2.15804 9.56066 2.43934C9.84196 2.72064 10 3.10218 10 3.5V8C10 8.39782 9.84196 8.77936 9.56066 9.06066C9.27936 9.34196 8.89782 9.5 8.5 9.5C8.10218 9.5 7.72064 9.34196 7.43934 9.06066C7.15804 8.77936 7 8.39782 7 8V3.5ZM9 12.472V14H11V15H6V14H8V12.472C6.89998 12.349 5.88387 11.8249 5.14594 10.9999C4.40801 10.1749 4.00003 9.10688 4 8H5C5 8.92826 5.36875 9.8185 6.02513 10.4749C6.6815 11.1313 7.57174 11.5 8.5 11.5C9.42826 11.5 10.3185 11.1313 10.9749 10.4749C11.6313 9.8185 12 8.92826 12 8H13C13 9.10688 12.592 10.1749 11.8541 10.9999C11.1161 11.8249 10.1 12.349 9 12.472V12.472Z" fill=${fillColor} />
</svg>`;
return createSVGElement(path);
}
Expand Down Expand Up @@ -215,7 +215,7 @@ export function iconCancel(fillColor: string = "currentColor") {
return createSVGElement(path);
}

export function iconAttach(fillColor: string = "#1F1F1F") {
export function iconAttach(fillColor: string = "currentColor") {
const path = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>Attach a file</title>
<path d="M7.75 15H7.25C6.35568 14.9646 5.51177 14.5765 4.90275 13.9207C4.29374 13.2648 3.96917 12.3945 4 11.5V3.682C3.97784 2.99536 4.22834 2.32785 4.69677 1.82531C5.1652 1.32278 5.81349 1.02607 6.5 1C7.18651 1.02607 7.8348 1.32278 8.30323 1.82531C8.77166 2.32785 9.02216 2.99536 9 3.682V10.849C9.01105 11.2586 8.85945 11.6559 8.57836 11.9541C8.29727 12.2522 7.90955 12.4269 7.5 12.44C7.09434 12.4275 6.70983 12.2561 6.42927 11.9628C6.14872 11.6695 5.99456 11.2778 6 10.872V7H7V10.849C6.98916 10.9934 7.0355 11.1363 7.12904 11.2468C7.22257 11.3574 7.3558 11.4268 7.5 11.44C7.6442 11.4268 7.77743 11.3574 7.87096 11.2468C7.9645 11.1363 8.01084 10.9934 8 10.849V3.682C8.02242 3.26046 7.87735 2.84714 7.59639 2.53209C7.31543 2.21704 6.92135 2.02579 6.5 2C6.07865 2.02579 5.68457 2.21704 5.40361 2.53209C5.12265 2.84714 4.97758 3.26046 5 3.682V11.5C4.96944 12.1292 5.18877 12.7451 5.6102 13.2133C6.03164 13.6816 6.62103 13.9643 7.25 14H7.75C8.37897 13.9643 8.96836 13.6816 9.3898 13.2133C9.81123 12.7451 10.0306 12.1292 10 11.5V5H11V11.5C11.0308 12.3945 10.7063 13.2648 10.0972 13.9207C9.48823 14.5765 8.64432 14.9646 7.75 15V15Z" fill="${fillColor}" />
Expand All @@ -224,10 +224,20 @@ export function iconAttach(fillColor: string = "#1F1F1F") {
return createSVGElement(path);
}

export function iconSend(fillColor: string = "#1F1F1F") {
export function iconSend(fillColor: string = "currentColor") {
const path = `<svg xmlns="http://www.w3.org/2000/svg" fill="${fillColor}" viewBox="0 0 2048 2048">
<path d="M2048 960q0 19-10 34t-27 24L91 1914q-12 6-27 6-28 0-46-18t-18-47v-9q0-4 2-8l251-878L2 82q-2-4-2-8t0-9q0-28 18-46T64 0q15 0 27 6l1920 896q37 17 37 58zM164 1739l1669-779L164 181l205 715h847q26 0 45 19t19 45q0 26-19 45t-45 19H369l-205 715z" />
</svg>`;

return createSVGElement(path);
}

export function iconMoon(fillColor: string = "#1f1f1f") {
const path = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill=${fillColor} stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-moon"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>`;
return createSVGElement(path);
}

export function iconSun(fillColor: string = "#1f1f1f") {
const path = `<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 24 24" fill=${fillColor} stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`;
return createSVGElement(path);
}
49 changes: 49 additions & 0 deletions ts/packages/shell/src/renderer/src/settingsView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { ChatView } from "./chatView.js";
import { getTTS, getTTSProviders, getTTSVoices } from "./tts/tts.js";
import { DisplayType } from "../../preload/electronTypes";
import { iconMoon, iconSun } from "./icon.js";

function addOption(
select: HTMLSelectElement,
Expand Down Expand Up @@ -86,6 +87,7 @@ export class SettingsView {
private ttsVoice: HTMLSelectElement;
private agentGreetingCheckBox: HTMLInputElement;
private intellisenseCheckBox: HTMLInputElement;
private darkModeToggle: HTMLButtonElement;
private _shellSettings: ShellSettingsType = defaultSettings;
private updateFromSettings: () => Promise<void>;
public get shellSettings(): Readonly<ShellSettingsType> {
Expand Down Expand Up @@ -172,6 +174,24 @@ export class SettingsView {
chatView.setMetricsVisible(this.shellSettings.devUI);
};

const updateTheme = () => {
const labelElement = document.createElement("span");
labelElement.innerText = this._shellSettings.darkMode
? "Light mode"
: "Dark mode";
if (this._shellSettings.darkMode) {
this.darkModeToggle.innerHTML = "";
this.darkModeToggle.appendChild(iconSun());
this.darkModeToggle.appendChild(labelElement);
document.body.classList.add("dark-mode");
} else {
this.darkModeToggle.innerHTML = "";
this.darkModeToggle.appendChild(iconMoon());
this.darkModeToggle.appendChild(labelElement);
document.body.classList.remove("dark-mode");
}
};

const updateInputs = () => {
if (this.shellSettings.multiModalContent) {
chatView.chatInput.camButton.classList.remove(
Expand Down Expand Up @@ -240,6 +260,7 @@ export class SettingsView {
updateTTSSelections();

this.updateFromSettings = async () => {
updateTheme();
updateChatView();
await updateTTSSelections();
updateInputs();
Expand Down Expand Up @@ -268,12 +289,40 @@ export class SettingsView {
this._shellSettings.devUI = !this.agentGreetingCheckBox.checked;
chatView.setMetricsVisible(!this.devUICheckBox.checked);
});

this.darkModeToggle = this.addButton(
this._shellSettings.darkMode ? iconSun() : iconMoon(),
() => {
this._shellSettings.darkMode = !this._shellSettings.darkMode;
this.saveSettings();
this.updateFromSettings();
},
this._shellSettings.darkMode ? "Light mode" : "Dark mode",
);
}

getContainer() {
return this.mainContainer;
}

private addButton(
innerContent: HTMLElement,
onclick: () => void,
label?: string,
) {
const button = document.createElement("button");
button.innerHTML = innerContent.innerHTML;
button.onclick = onclick;
button.classList.add("settings-button");
if (label) {
const labelElement = document.createElement("span");
labelElement.innerText = label;
button.appendChild(labelElement);
}
this.mainContainer.appendChild(button);
return button;
}

private addSelect(labelText: string, id: string, onchange: () => void) {
// microphone selection
const div: HTMLDivElement = document.createElement("div");
Expand Down

0 comments on commit e0ddb7a

Please sign in to comment.