Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
acharneski committed Dec 7, 2024
1 parent 6128bc8 commit 88d8dee
Show file tree
Hide file tree
Showing 29 changed files with 6,295 additions and 68 deletions.
16 changes: 16 additions & 0 deletions webapp/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,20 @@
var(--color-gradient-start),
var(--color-gradient-end)
);
}
/* Add loading and error container styles */
.loading-container,
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
padding: 2rem;
text-align: center;
background: var(--background-color, #fff);
color: var(--text-color, #333);
}
.error-container {
color: var(--error-color, #dc3545);
}
55 changes: 29 additions & 26 deletions webapp/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import {Provider} from 'react-redux';
import {store} from './store';
import {Provider, useSelector} from 'react-redux';
import {store, RootState} from './store';
import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary';
import ErrorFallback from './components/ErrorBoundary/ErrorFallback';
import './App.css';
Expand Down Expand Up @@ -49,12 +49,20 @@ const LOG_PREFIX = '[App]';
Prism.manual = true;


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

const sessionId = websocket.getSessionId();
const isConnected = websocket.isConnected();
React.useEffect(() => {
if (appConfig.applicationName) {
document.title = appConfig.applicationName;
console.log(`${LOG_PREFIX} Updated page title to:`, appConfig.applicationName);
}
}, [appConfig.applicationName]);
console.log('WebSocket state:', {
sessionId,
isConnected
Expand All @@ -81,36 +89,31 @@ const App: React.FC = () => {
};
}, []);

return (
<ThemeProvider>
<div className={`App`}>
<Menu/>
<ChatInterface
sessionId={sessionId}
websocket={websocket}
isConnected={isConnected}
/>
<Modal/>
</div>
</ThemeProvider>
);
};
// Create the main App component that provides the Redux store
const App: React.FC = () => {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Provider store={store}>
{(() => {
console.debug(`${LOG_PREFIX} Rendering Provider with store`);
return (
<ThemeProvider>
{(() => {
console.debug(`${LOG_PREFIX} Rendering ThemeProvider with theme`);
return (
<>
<div className={`App`}>
<Menu/>
<ChatInterface
sessionId={sessionId}
websocket={websocket}
isConnected={isConnected}
/>
<Modal/>
</div>
</>
);
})()}
</ThemeProvider>
);
})()}
<AppContent />
</Provider>
</ErrorBoundary>
);
};

console.groupEnd();
console.log(`${LOG_PREFIX} v${APP_VERSION} loaded successfully`);

Expand Down
1 change: 1 addition & 0 deletions webapp/src/components/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
const loadAppConfig = async () => {
if (!sessionId) return;
try {
console.info('Fetching app config');
const config = await fetchAppConfig(sessionId);
if (mounted && config) {
console.info('App config loaded successfully');
Expand Down
13 changes: 9 additions & 4 deletions webapp/src/components/InputArea.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {memo, useCallback, useState} from 'react';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import {useSelector} from 'react-redux';
import {RootState} from '../store';

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

const InputContainer = styled.div`
interface InputContainerProps {
$hide?: boolean;
}
const InputContainer = styled.div<InputContainerProps>`
padding: 1.5rem;
background-color: ${(props) => props.theme.colors.surface};
border-top: 1px solid ${(props) => props.theme.colors.border};
display: ${({theme}) => theme.config?.singleInput ? 'none' : 'block'};
display: ${({theme, $hide}) => $hide ? 'none' : 'block'};
max-height: 10vh;
position: sticky;
bottom: 0;
Expand Down Expand Up @@ -116,8 +119,10 @@ const InputArea = memo(function InputArea({onSendMessage}: InputAreaProps) {
log('Initializing component');
const [message, setMessage] = useState('');
const config = useSelector((state: RootState) => state.config);
const messages = useSelector((state: RootState) => state.messages.messages);
const [isSubmitting, setIsSubmitting] = useState(false);
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
const shouldHideInput = config.singleInput && messages.length > 0;

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


return (
<InputContainer>
<InputContainer $hide={shouldHideInput}>
<StyledForm onSubmit={handleSubmit}>
<TextArea
ref={textAreaRef}
Expand Down
9 changes: 7 additions & 2 deletions webapp/src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import React from 'react';
import styled from 'styled-components';
import {useDispatch, useSelector} from 'react-redux';
import {useModal} from '../../hooks/useModal';
interface MenuContainerProps {
$hidden?: boolean;
}
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCog, faHome, faSignInAlt, faSignOutAlt} from '@fortawesome/free-solid-svg-icons';
import {ThemeMenu} from "./ThemeMenu";
Expand Down Expand Up @@ -38,11 +41,12 @@ function newGlobalID(): string {
return (`G-${yyyyMMdd}-${id2()}`);
}

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

export const Menu: React.FC = () => {
useSelector((state: RootState) => state.config.websocket);
const showMenubar = useSelector((state: RootState) => state.config.showMenubar);
const {openModal} = useModal();
const dispatch = useDispatch();
const verboseMode = useSelector((state: RootState) => state.ui.verboseMode);
Expand All @@ -319,7 +324,7 @@ export const Menu: React.FC = () => {


return (
<MenuContainer>
<MenuContainer $hidden={!showMenubar}>
<ToolbarLeft>
<DropButton as="a" href="/" onClick={() => console.log('[Menu] Navigating to home')}>
<FontAwesomeIcon icon={faHome}/> Home
Expand Down
41 changes: 40 additions & 1 deletion webapp/src/components/Menu/ThemeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';
import styled from 'styled-components';
import {useTheme} from '../../hooks/useTheme';
import {themes} from '../../themes/themes';
import {useDispatch} from 'react-redux';
import {showModal, setModalContent} from '../../store/slices/uiSlice';

const LOG_PREFIX = '[ThemeMenu Component]';
const logWithPrefix = (message: string, ...args: any[]) => {
Expand Down Expand Up @@ -161,6 +163,7 @@ export const ThemeMenu: React.FC = () => {
const [isLoading, setIsLoading] = React.useState(false);
const menuRef = React.useRef<HTMLDivElement>(null);
const firstOptionRef = React.useRef<HTMLButtonElement>(null);
const dispatch = useDispatch();
// Focus first option when menu opens
React.useEffect(() => {
if (isOpen && firstOptionRef.current) {
Expand All @@ -180,7 +183,43 @@ export const ThemeMenu: React.FC = () => {
return () => {
document.removeEventListener('keydown', handleEscapeKey);
};
}, [isOpen]);
}, [isOpen]);
// Add keyboard shortcut handler
React.useEffect(() => {
const handleKeyboardShortcut = (event: KeyboardEvent) => {
if (event.altKey && event.key.toLowerCase() === 't') {
event.preventDefault();
const themeContent = `
<div>
${Object.keys(themes).map(themeName => `
<button
onclick="window.dispatchEvent(new CustomEvent('themeChange', {detail: '${themeName}'}))"
style="display: block; width: 100%; margin: 8px 0; padding: 8px; text-align: left; ${themeName === currentTheme ? 'background: #eee;' : ''}"
>
${themeName}
</button>
`).join('')}
</div>
`;
dispatch(showModal('Theme Selection'));
dispatch(setModalContent(themeContent));
logDebug('Theme modal opened via keyboard shortcut (Alt+T)');
}
};
document.addEventListener('keydown', handleKeyboardShortcut);
return () => {
document.removeEventListener('keydown', handleKeyboardShortcut);
};
}, [currentTheme, dispatch]);
React.useEffect(() => {
const handleThemeChangeEvent = (event: CustomEvent<string>) => {
handleThemeChange(event.detail as keyof typeof themes);
};
window.addEventListener('themeChange', handleThemeChangeEvent as EventListener);
return () => {
window.removeEventListener('themeChange', handleThemeChangeEvent as EventListener);
};
}, []);
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
Expand Down
17 changes: 16 additions & 1 deletion webapp/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,27 @@ const ModalOverlay = styled.div`
const ModalContent = styled.div`
background-color: ${({theme}) => theme.colors.surface};
padding: ${({theme}) => theme.sizing.spacing.lg};
border-radius: 4px;
border-radius: ${({theme}) => theme.sizing.borderRadius.md};
min-width: 300px;
max-width: 80vw;
max-height: 80vh;
min-height: 200px;
overflow: auto;
box-shadow: 0 4px 16px ${({theme}) => `${theme.colors.primary}20`};
h2 {
margin-bottom: ${({theme}) => theme.sizing.spacing.md};
color: ${({theme}) => theme.colors.text.primary};
font-weight: ${({theme}) => theme.typography.fontWeight.bold};
}
button {
border: 1px solid ${({theme}) => theme.colors.border};
border-radius: ${({theme}) => theme.sizing.borderRadius.sm};
cursor: pointer;
&:hover {
background: ${({theme}) => theme.colors.primary};
color: ${({theme}) => theme.colors.background};
}
}
`;
const LOG_PREFIX = '[Modal]';

Expand Down
31 changes: 7 additions & 24 deletions webapp/src/services/appConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import {ThemeName} from '../types';
const LOG_PREFIX = '[AppConfig]';

const BASE_API_URL = process.env.REACT_APP_API_URL || (window.location.origin + window.location.pathname);
// Add loading state tracking
let isLoadingConfig = false;

let loadConfigPromise: Promise<any> | null = null;
const STORAGE_KEYS = {
THEME: 'theme',
Expand Down Expand Up @@ -65,29 +64,17 @@ export const themeStorage = {
}
};

export interface AppConfig {
applicationName: string;
singleInput: boolean;
showMenubar: boolean;
}

// Add config cache
let cachedConfig: any = null;
const CONFIG_CACHE_KEY = 'app_config_cache';
const CONFIG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
// Try to load cached config from localStorage
try {
const cached = localStorage.getItem(CONFIG_CACHE_KEY);
if (cached) {
const {config, timestamp} = JSON.parse(cached);
if (Date.now() - timestamp < CONFIG_CACHE_TTL) {
cachedConfig = config;
console.info(`${LOG_PREFIX} Loaded valid config from cache`);
} else {
localStorage.removeItem(CONFIG_CACHE_KEY);
console.info(`${LOG_PREFIX} Removed expired config cache`);
}
}
} catch (error) {
console.warn(`${LOG_PREFIX} Error loading cached config:`, error);
}

export const fetchAppConfig = async (sessionId: string) => {
export const fetchAppConfig: (sessionId: string) => Promise<AppConfig> = async (sessionId: string) => {
try {
// Return cached config if available
if (cachedConfig) {
Expand All @@ -99,8 +86,6 @@ export const fetchAppConfig = async (sessionId: string) => {
console.info(`${LOG_PREFIX} Config fetch already in progress, reusing promise`);
return loadConfigPromise;
}
// Set loading state
isLoadingConfig = true;
loadConfigPromise = (async () => {

console.info(`${LOG_PREFIX} Fetching app config:`, {
Expand Down Expand Up @@ -188,7 +173,6 @@ export const fetchAppConfig = async (sessionId: string) => {
return data;
})();
const result = await loadConfigPromise;
isLoadingConfig = false;
loadConfigPromise = null;
return result;

Expand All @@ -199,7 +183,6 @@ export const fetchAppConfig = async (sessionId: string) => {
url: BASE_API_URL ? `${BASE_API_URL}/appInfo` : '/appInfo',
env: process.env.NODE_ENV
});
isLoadingConfig = false;
loadConfigPromise = null;
throw error;
}
Expand Down
1 change: 0 additions & 1 deletion webapp/src/store/slices/uiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ export const uiSlice = createSlice({
});
state.modalOpen = true;
state.modalType = action.payload;
state.modalContent = 'Loading...';
},
hideModal: (state) => {
logStateChange('Hiding modal', null, {
Expand Down
Loading

0 comments on commit 88d8dee

Please sign in to comment.