Skip to content

Commit 88d8dee

Browse files
committed
wip
1 parent 6128bc8 commit 88d8dee

29 files changed

+6295
-68
lines changed

webapp/src/App.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,20 @@
251251
var(--color-gradient-start),
252252
var(--color-gradient-end)
253253
);
254+
}
255+
/* Add loading and error container styles */
256+
.loading-container,
257+
.error-container {
258+
display: flex;
259+
flex-direction: column;
260+
align-items: center;
261+
justify-content: center;
262+
height: 100vh;
263+
padding: 2rem;
264+
text-align: center;
265+
background: var(--background-color, #fff);
266+
color: var(--text-color, #333);
267+
}
268+
.error-container {
269+
color: var(--error-color, #dc3545);
254270
}

webapp/src/App.tsx

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
2-
import {Provider} from 'react-redux';
3-
import {store} from './store';
2+
import {Provider, useSelector} from 'react-redux';
3+
import {store, RootState} from './store';
44
import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary';
55
import ErrorFallback from './components/ErrorBoundary/ErrorFallback';
66
import './App.css';
@@ -49,12 +49,20 @@ const LOG_PREFIX = '[App]';
4949
Prism.manual = true;
5050

5151

52-
const App: React.FC = () => {
52+
// Create a separate component for the app content
53+
const AppContent: React.FC = () => {
5354
console.group(`${LOG_PREFIX} Initializing v${APP_VERSION}`);
5455
console.log('Starting component render');
56+
const appConfig = useSelector((state: RootState) => state.config);
5557

5658
const sessionId = websocket.getSessionId();
5759
const isConnected = websocket.isConnected();
60+
React.useEffect(() => {
61+
if (appConfig.applicationName) {
62+
document.title = appConfig.applicationName;
63+
console.log(`${LOG_PREFIX} Updated page title to:`, appConfig.applicationName);
64+
}
65+
}, [appConfig.applicationName]);
5866
console.log('WebSocket state:', {
5967
sessionId,
6068
isConnected
@@ -81,36 +89,31 @@ const App: React.FC = () => {
8189
};
8290
}, []);
8391

92+
return (
93+
<ThemeProvider>
94+
<div className={`App`}>
95+
<Menu/>
96+
<ChatInterface
97+
sessionId={sessionId}
98+
websocket={websocket}
99+
isConnected={isConnected}
100+
/>
101+
<Modal/>
102+
</div>
103+
</ThemeProvider>
104+
);
105+
};
106+
// Create the main App component that provides the Redux store
107+
const App: React.FC = () => {
84108
return (
85109
<ErrorBoundary FallbackComponent={ErrorFallback}>
86110
<Provider store={store}>
87-
{(() => {
88-
console.debug(`${LOG_PREFIX} Rendering Provider with store`);
89-
return (
90-
<ThemeProvider>
91-
{(() => {
92-
console.debug(`${LOG_PREFIX} Rendering ThemeProvider with theme`);
93-
return (
94-
<>
95-
<div className={`App`}>
96-
<Menu/>
97-
<ChatInterface
98-
sessionId={sessionId}
99-
websocket={websocket}
100-
isConnected={isConnected}
101-
/>
102-
<Modal/>
103-
</div>
104-
</>
105-
);
106-
})()}
107-
</ThemeProvider>
108-
);
109-
})()}
111+
<AppContent />
110112
</Provider>
111113
</ErrorBoundary>
112114
);
113115
};
116+
114117
console.groupEnd();
115118
console.log(`${LOG_PREFIX} v${APP_VERSION} loaded successfully`);
116119

webapp/src/components/ChatInterface.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
5757
const loadAppConfig = async () => {
5858
if (!sessionId) return;
5959
try {
60+
console.info('Fetching app config');
6061
const config = await fetchAppConfig(sessionId);
6162
if (mounted && config) {
6263
console.info('App config loaded successfully');

webapp/src/components/InputArea.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {memo, useCallback, useState} from 'react';
2-
import styled from 'styled-components';
2+
import styled, { css } from 'styled-components';
33
import {useSelector} from 'react-redux';
44
import {RootState} from '../store';
55

@@ -15,11 +15,14 @@ const log = (message: string, data?: unknown) => {
1515
}
1616
};
1717

18-
const InputContainer = styled.div`
18+
interface InputContainerProps {
19+
$hide?: boolean;
20+
}
21+
const InputContainer = styled.div<InputContainerProps>`
1922
padding: 1.5rem;
2023
background-color: ${(props) => props.theme.colors.surface};
2124
border-top: 1px solid ${(props) => props.theme.colors.border};
22-
display: ${({theme}) => theme.config?.singleInput ? 'none' : 'block'};
25+
display: ${({theme, $hide}) => $hide ? 'none' : 'block'};
2326
max-height: 10vh;
2427
position: sticky;
2528
bottom: 0;
@@ -116,8 +119,10 @@ const InputArea = memo(function InputArea({onSendMessage}: InputAreaProps) {
116119
log('Initializing component');
117120
const [message, setMessage] = useState('');
118121
const config = useSelector((state: RootState) => state.config);
122+
const messages = useSelector((state: RootState) => state.messages.messages);
119123
const [isSubmitting, setIsSubmitting] = useState(false);
120124
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
125+
const shouldHideInput = config.singleInput && messages.length > 0;
121126

122127
const handleSubmit = useCallback((e: React.FormEvent) => {
123128
e.preventDefault();
@@ -167,7 +172,7 @@ const InputArea = memo(function InputArea({onSendMessage}: InputAreaProps) {
167172

168173

169174
return (
170-
<InputContainer>
175+
<InputContainer $hide={shouldHideInput}>
171176
<StyledForm onSubmit={handleSubmit}>
172177
<TextArea
173178
ref={textAreaRef}

webapp/src/components/Menu/Menu.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import React from 'react';
22
import styled from 'styled-components';
33
import {useDispatch, useSelector} from 'react-redux';
44
import {useModal} from '../../hooks/useModal';
5+
interface MenuContainerProps {
6+
$hidden?: boolean;
7+
}
58
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
69
import {faCog, faHome, faSignInAlt, faSignOutAlt} from '@fortawesome/free-solid-svg-icons';
710
import {ThemeMenu} from "./ThemeMenu";
@@ -38,11 +41,12 @@ function newGlobalID(): string {
3841
return (`G-${yyyyMMdd}-${id2()}`);
3942
}
4043

41-
const MenuContainer = styled.div`
44+
const MenuContainer = styled.div<MenuContainerProps>`
4245
display: flex;
4346
justify-content: space-between;
4447
border-bottom: 1px solid ${({theme}) => theme.colors.border};
4548
max-height: 5vh;
49+
display: ${({$hidden}) => $hidden ? 'none' : 'flex'};
4650
box-shadow: 0 2px 8px ${({theme}) => `${theme.colors.primary}20`};
4751
position: sticky;
4852
top: 0;
@@ -295,6 +299,7 @@ const DropLink = styled.a`
295299

296300
export const Menu: React.FC = () => {
297301
useSelector((state: RootState) => state.config.websocket);
302+
const showMenubar = useSelector((state: RootState) => state.config.showMenubar);
298303
const {openModal} = useModal();
299304
const dispatch = useDispatch();
300305
const verboseMode = useSelector((state: RootState) => state.ui.verboseMode);
@@ -319,7 +324,7 @@ export const Menu: React.FC = () => {
319324

320325

321326
return (
322-
<MenuContainer>
327+
<MenuContainer $hidden={!showMenubar}>
323328
<ToolbarLeft>
324329
<DropButton as="a" href="/" onClick={() => console.log('[Menu] Navigating to home')}>
325330
<FontAwesomeIcon icon={faHome}/> Home

webapp/src/components/Menu/ThemeMenu.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React from 'react';
22
import styled from 'styled-components';
33
import {useTheme} from '../../hooks/useTheme';
44
import {themes} from '../../themes/themes';
5+
import {useDispatch} from 'react-redux';
6+
import {showModal, setModalContent} from '../../store/slices/uiSlice';
57

68
const LOG_PREFIX = '[ThemeMenu Component]';
79
const logWithPrefix = (message: string, ...args: any[]) => {
@@ -161,6 +163,7 @@ export const ThemeMenu: React.FC = () => {
161163
const [isLoading, setIsLoading] = React.useState(false);
162164
const menuRef = React.useRef<HTMLDivElement>(null);
163165
const firstOptionRef = React.useRef<HTMLButtonElement>(null);
166+
const dispatch = useDispatch();
164167
// Focus first option when menu opens
165168
React.useEffect(() => {
166169
if (isOpen && firstOptionRef.current) {
@@ -180,7 +183,43 @@ export const ThemeMenu: React.FC = () => {
180183
return () => {
181184
document.removeEventListener('keydown', handleEscapeKey);
182185
};
183-
}, [isOpen]);
186+
}, [isOpen]);
187+
// Add keyboard shortcut handler
188+
React.useEffect(() => {
189+
const handleKeyboardShortcut = (event: KeyboardEvent) => {
190+
if (event.altKey && event.key.toLowerCase() === 't') {
191+
event.preventDefault();
192+
const themeContent = `
193+
<div>
194+
${Object.keys(themes).map(themeName => `
195+
<button
196+
onclick="window.dispatchEvent(new CustomEvent('themeChange', {detail: '${themeName}'}))"
197+
style="display: block; width: 100%; margin: 8px 0; padding: 8px; text-align: left; ${themeName === currentTheme ? 'background: #eee;' : ''}"
198+
>
199+
${themeName}
200+
</button>
201+
`).join('')}
202+
</div>
203+
`;
204+
dispatch(showModal('Theme Selection'));
205+
dispatch(setModalContent(themeContent));
206+
logDebug('Theme modal opened via keyboard shortcut (Alt+T)');
207+
}
208+
};
209+
document.addEventListener('keydown', handleKeyboardShortcut);
210+
return () => {
211+
document.removeEventListener('keydown', handleKeyboardShortcut);
212+
};
213+
}, [currentTheme, dispatch]);
214+
React.useEffect(() => {
215+
const handleThemeChangeEvent = (event: CustomEvent<string>) => {
216+
handleThemeChange(event.detail as keyof typeof themes);
217+
};
218+
window.addEventListener('themeChange', handleThemeChangeEvent as EventListener);
219+
return () => {
220+
window.removeEventListener('themeChange', handleThemeChangeEvent as EventListener);
221+
};
222+
}, []);
184223
React.useEffect(() => {
185224
const handleClickOutside = (event: MouseEvent) => {
186225
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {

webapp/src/components/Modal/Modal.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,27 @@ const ModalOverlay = styled.div`
2020
const ModalContent = styled.div`
2121
background-color: ${({theme}) => theme.colors.surface};
2222
padding: ${({theme}) => theme.sizing.spacing.lg};
23-
border-radius: 4px;
23+
border-radius: ${({theme}) => theme.sizing.borderRadius.md};
2424
min-width: 300px;
2525
max-width: 80vw;
2626
max-height: 80vh;
2727
min-height: 200px;
2828
overflow: auto;
29+
box-shadow: 0 4px 16px ${({theme}) => `${theme.colors.primary}20`};
30+
h2 {
31+
margin-bottom: ${({theme}) => theme.sizing.spacing.md};
32+
color: ${({theme}) => theme.colors.text.primary};
33+
font-weight: ${({theme}) => theme.typography.fontWeight.bold};
34+
}
35+
button {
36+
border: 1px solid ${({theme}) => theme.colors.border};
37+
border-radius: ${({theme}) => theme.sizing.borderRadius.sm};
38+
cursor: pointer;
39+
&:hover {
40+
background: ${({theme}) => theme.colors.primary};
41+
color: ${({theme}) => theme.colors.background};
42+
}
43+
}
2944
`;
3045
const LOG_PREFIX = '[Modal]';
3146

webapp/src/services/appConfig.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import {ThemeName} from '../types';
66
const LOG_PREFIX = '[AppConfig]';
77

88
const BASE_API_URL = process.env.REACT_APP_API_URL || (window.location.origin + window.location.pathname);
9-
// Add loading state tracking
10-
let isLoadingConfig = false;
9+
1110
let loadConfigPromise: Promise<any> | null = null;
1211
const STORAGE_KEYS = {
1312
THEME: 'theme',
@@ -65,29 +64,17 @@ export const themeStorage = {
6564
}
6665
};
6766

67+
export interface AppConfig {
68+
applicationName: string;
69+
singleInput: boolean;
70+
showMenubar: boolean;
71+
}
6872

6973
// Add config cache
7074
let cachedConfig: any = null;
7175
const CONFIG_CACHE_KEY = 'app_config_cache';
72-
const CONFIG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
73-
// Try to load cached config from localStorage
74-
try {
75-
const cached = localStorage.getItem(CONFIG_CACHE_KEY);
76-
if (cached) {
77-
const {config, timestamp} = JSON.parse(cached);
78-
if (Date.now() - timestamp < CONFIG_CACHE_TTL) {
79-
cachedConfig = config;
80-
console.info(`${LOG_PREFIX} Loaded valid config from cache`);
81-
} else {
82-
localStorage.removeItem(CONFIG_CACHE_KEY);
83-
console.info(`${LOG_PREFIX} Removed expired config cache`);
84-
}
85-
}
86-
} catch (error) {
87-
console.warn(`${LOG_PREFIX} Error loading cached config:`, error);
88-
}
8976

90-
export const fetchAppConfig = async (sessionId: string) => {
77+
export const fetchAppConfig: (sessionId: string) => Promise<AppConfig> = async (sessionId: string) => {
9178
try {
9279
// Return cached config if available
9380
if (cachedConfig) {
@@ -99,8 +86,6 @@ export const fetchAppConfig = async (sessionId: string) => {
9986
console.info(`${LOG_PREFIX} Config fetch already in progress, reusing promise`);
10087
return loadConfigPromise;
10188
}
102-
// Set loading state
103-
isLoadingConfig = true;
10489
loadConfigPromise = (async () => {
10590

10691
console.info(`${LOG_PREFIX} Fetching app config:`, {
@@ -188,7 +173,6 @@ export const fetchAppConfig = async (sessionId: string) => {
188173
return data;
189174
})();
190175
const result = await loadConfigPromise;
191-
isLoadingConfig = false;
192176
loadConfigPromise = null;
193177
return result;
194178

@@ -199,7 +183,6 @@ export const fetchAppConfig = async (sessionId: string) => {
199183
url: BASE_API_URL ? `${BASE_API_URL}/appInfo` : '/appInfo',
200184
env: process.env.NODE_ENV
201185
});
202-
isLoadingConfig = false;
203186
loadConfigPromise = null;
204187
throw error;
205188
}

webapp/src/store/slices/uiSlice.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ export const uiSlice = createSlice({
104104
});
105105
state.modalOpen = true;
106106
state.modalType = action.payload;
107-
state.modalContent = 'Loading...';
108107
},
109108
hideModal: (state) => {
110109
logStateChange('Hiding modal', null, {

0 commit comments

Comments
 (0)