Skip to content

Commit

Permalink
Dingoes!
Browse files Browse the repository at this point in the history
  • Loading branch information
acharneski committed Nov 28, 2024
1 parent f35cbac commit f5721d1
Show file tree
Hide file tree
Showing 14 changed files with 184 additions and 293 deletions.
2 changes: 1 addition & 1 deletion webapp/chat-app/src/components/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
debugLog('Processing HTML message');
const newMessage = {
id: `${Date.now()}`,
content: data.data,
content: data.data || '',
type: 'response' as const,
timestamp: data.timestamp,
isHtml: true,
Expand Down
149 changes: 88 additions & 61 deletions webapp/chat-app/src/components/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useCallback} from 'react';
import styled from 'styled-components';
import {useSelector} from 'react-redux';
import {RootState} from '../store';
Expand All @@ -7,6 +7,17 @@ import {Message} from '../types';
import {updateTabs} from '../utils/tabHandling';
import {handleMessageAction} from '../utils/messageHandling';

const expandMessageReferences = (content: string, messages: Message[]): string => {
if (!content) return '';
return content.replace(/\{([^}]+)}/g, (match, messageId) => {
const referencedMessage = messages.find(m => m.id === messageId);
if (referencedMessage) {
return expandMessageReferences(referencedMessage.content, messages);
}
return match;
});
};

const MessageListContainer = styled.div`
flex: 1;
overflow-y: auto;
Expand All @@ -25,14 +36,14 @@ const MessageContent = styled.div`
}
}
`;
const extractMessageAction = (target: HTMLElement): {messageId: string | undefined, action: string | undefined} => {
const extractMessageAction = (target: HTMLElement): { messageId: string | undefined, action: string | undefined } => {
// Check for data attributes
const messageId = target.getAttribute('data-message-id') ??
target.getAttribute('data-id') ??
undefined;
let action = target.getAttribute('data-message-action') ??
target.getAttribute('data-action') ??
undefined;
const messageId = target.getAttribute('data-message-id') ??
target.getAttribute('data-id') ??
undefined;
let action = target.getAttribute('data-message-action') ??
target.getAttribute('data-action') ??
undefined;
// Check element classes
if (!action) {
if (target.classList.contains('href-link')) action = 'link';
Expand All @@ -58,7 +69,7 @@ const MessageItem = styled.div<{ type: 'user' | 'system' | 'response' }>`
default:
return '#f8f9fa';
}
}};
}};
color: ${({type}) => type === 'user' || type === 'system' ? '#fff' : '#212529'};
`;
const handleClick = (e: React.MouseEvent) => {
Expand All @@ -73,65 +84,81 @@ const handleClick = (e: React.MouseEvent) => {
};

interface MessageListProps {
messages?: Message[];
}
messages?: Message[];
}

const MessageList: React.FC<MessageListProps> = ({messages: propMessages}) => {
logger.component('MessageList', 'Rendering component', {hasPropMessages: !!propMessages});
const MessageList: React.FC<MessageListProps> = ({messages: propMessages}) => {
logger.component('MessageList', 'Rendering component', {hasPropMessages: !!propMessages});

// Log when component is mounted/unmounted
React.useEffect(() => {
logger.component('MessageList', 'Component mounted', {timestamp: new Date().toISOString()});
return () => {
logger.component('MessageList', 'Component unmounted', {timestamp: new Date().toISOString()});
};
}, []);
// Log when component is mounted/unmounted
React.useEffect(() => {
logger.component('MessageList', 'Component mounted', {timestamp: new Date().toISOString()});
return () => {
logger.component('MessageList', 'Component unmounted', {timestamp: new Date().toISOString()});
};
}, []);

const storeMessages = useSelector((state: RootState) => state.messages.messages);
// Ensure messages is always an array
const messages = Array.isArray(propMessages) ? propMessages :
Array.isArray(storeMessages) ? storeMessages : [];
const storeMessages = useSelector((state: RootState) => state.messages.messages);
// Ensure messages is always an array
const messages = Array.isArray(propMessages) ? propMessages :
Array.isArray(storeMessages) ? storeMessages : [];
const processMessageContent = useCallback((content: string) => {
return expandMessageReferences(content, messages);
}, [messages]);

React.useEffect(() => {
logger.debug('MessageList - Messages updated', {
messageCount: messages.length,
messages: messages,
source: propMessages ? 'props' : 'store'
});
// Process tabs after messages update
requestAnimationFrame(() => {
updateTabs();
React.useEffect(() => {
logger.debug('MessageList - Messages updated', {
messageCount: messages.length,
messages: messages,
source: propMessages ? 'props' : 'store'
});
// Process tabs after messages update
requestAnimationFrame(() => {
updateTabs();
// Process message references in visible tab content
document.querySelectorAll('.tab-content.active').forEach(tab => {
const content = tab.innerHTML;
const expandedContent = processMessageContent(content);
if (content !== expandedContent) {
tab.innerHTML = expandedContent;
}
});
}, [messages]);
});
}, [messages]);

return (
<MessageListContainer>
{messages.map((message) => {
logger.debug('MessageList - Rendering message', {
id: message.id,
type: message.type,
timestamp: message.timestamp,
contentLength: message.content?.length || 0
});
// Log message render before returning JSX
logger.debug('MessageList - Message rendered', {
id: message.id,
type: message.type
});
return (
<MessageListContainer>

return (
<MessageItem key={`${message.id}-${message.timestamp}-${Math.random()}`} type={message.type}>
<MessageContent
className="message-body"
onClick={handleClick}
dangerouslySetInnerHTML={{__html: message.content}}
/>
</MessageItem>
);
})}
</MessageListContainer>
);
};
{messages.map((message) => {
logger.debug('MessageList - Rendering message', {
id: message.id,
type: message.type,
timestamp: message.timestamp,
contentLength: message.content?.length || 0
});
// Log message render before returning JSX
logger.debug('MessageList - Message rendered', {
id: message.id,
type: message.type
});
return (
<MessageItem
key={`${message.id}-${message.timestamp}`}
type={message.type}
>
<MessageContent
className="message-body"
onClick={handleClick}
dangerouslySetInnerHTML={{
__html: processMessageContent(message.content)
}}
/>
</MessageItem>
);
})}
</MessageListContainer>
);
};


export default MessageList;
58 changes: 16 additions & 42 deletions webapp/chat-app/src/services/appConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,22 @@ import {logger} from '../utils/logger';
import {setAppInfo} from '../store/slices/configSlice';

export const fetchAppConfig = async (sessionId: string) => {
try {
logger.info('Fetching app config for session:', sessionId);
const response = await fetch(`appInfo?session=${sessionId}`);

if (!response.ok) {
throw new Error('Network response was not ok');
}

const data = await response.json();
logger.info('Received app config:', data);

store.dispatch(setAppInfo(data));

return data;
} catch (error) {
logger.error('Failed to fetch app config:', error);
throw error;
}
};
try {
logger.info('Fetching app config for session:', sessionId);
const response = await fetch(`appInfo?session=${sessionId}`);

export const applyMenubarConfig = (showMenubar: boolean) => {
if (showMenubar === false) {
const elements = {
menubar: document.getElementById('toolbar'),
namebar: document.getElementById('namebar'),
mainInput: document.getElementById('main-input'),
session: document.getElementById('session')
};
if (!response.ok) {
throw new Error('Network response was not ok');
}

if (elements.menubar) elements.menubar.style.display = 'none';
if (elements.namebar) elements.namebar.style.display = 'none';

if (elements.mainInput) {
elements.mainInput.style.top = '0px';
}

if (elements.session) {
elements.session.style.top = '0px';
elements.session.style.width = '100%';
elements.session.style.position = 'absolute';
const data = await response.json();
logger.info('Received app config:', data);

store.dispatch(setAppInfo(data));

return data;
} catch (error) {
logger.error('Failed to fetch app config:', error);
throw error;
}

logger.info('Applied menubar config: hidden');
}
};
};
68 changes: 28 additions & 40 deletions webapp/chat-app/src/services/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,12 @@ export class WebSocketService {
private isReconnecting = false;
private connectionTimeout: NodeJS.Timeout | null = null;
private messageQueue: string[] = [];
private readonly HEARTBEAT_INTERVAL = 30000;

queueMessage(message: string) {
console.debug('[WebSocket] Queuing message:', message.substring(0, 100) + (message.length > 100 ? '...' : ''));
this.messageQueue.push(message);
this.processMessageQueue();
}

public getSessionId(): string {
console.debug('[WebSocket] Getting session ID:', this.sessionId);
return this.sessionId;
}

public getUrl(): string {
const config = this.getConfig();
if (!config) return 'not available';
const path = this.getWebSocketPath();
return `${config.protocol}//${config.url}:${config.port}${path}ws`;
}

public addErrorHandler(handler: (error: Error) => void): void {
this.errorHandlers.push(handler);
console.log('[WebSocket] Error handler added');
Expand Down Expand Up @@ -65,25 +51,10 @@ export class WebSocketService {
console.log('[WebSocket] Connection handler removed');
}

private debugLog(message: string, ...args: any[]) {
if (this.DEBUG) {
console.debug(`[WebSocket] ${message}`, ...args);
}
}

public isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}

private stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
console.log('[WebSocket] Stopped heartbeat monitoring');
}
}


connect(sessionId: string): void {
try {
if (!sessionId) {
Expand All @@ -104,7 +75,14 @@ export class WebSocketService {
const path = this.getWebSocketPath();
// Only create new connection if not already connected or reconnecting
if (!this.isConnected() && !this.isReconnecting) {
const wsUrl = `${config.protocol}//${config.url}:${config.port}${path}ws?sessionId=${sessionId}`;
// Construct URL with proper handling of default ports
let wsUrl = `${config.protocol}//${config.url}`;
// Only add port if it's non-standard
if ((config.protocol === 'ws:' && config.port !== '80') ||
(config.protocol === 'wss:' && config.port !== '443')) {
wsUrl += `:${config.port}`;
}
wsUrl += `${path}ws?sessionId=${sessionId}`;
console.log(`[WebSocket] Connecting to: ${wsUrl}`);
this.ws = new WebSocket(wsUrl);
this.setupEventHandlers();
Expand All @@ -115,7 +93,7 @@ export class WebSocketService {
this.ws?.close();
this.attemptReconnect();
}
}, 5000);
}, 10000); // Increase timeout to 10 seconds
}
} catch (error) {
console.error('[WebSocket] Connection error:', error);
Expand Down Expand Up @@ -147,13 +125,17 @@ export class WebSocketService {
}
}

private processMessageQueue() {
if (this.ws?.readyState === WebSocket.OPEN) {
console.debug(`[WebSocket] Processing message queue (${this.messageQueue.length} messages)`);
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
if (message) this.send(message);
}
private debugLog(message: string, ...args: any[]) {
if (this.DEBUG) {
console.debug(`[WebSocket] ${message}`, ...args);
}
}

private stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
console.log('[WebSocket] Stopped heartbeat monitoring');
}
}

Expand All @@ -176,9 +158,10 @@ export class WebSocketService {
}
}
console.debug('[WebSocket] Using default config');
const defaultPort = window.location.protocol === 'https:' ? '443' : '8083';
return {
url: window.location.hostname,
port: state.config.websocket.port || window.location.port || '8083',
port: state.config?.websocket?.port || window.location.port || defaultPort,
protocol: window.location.protocol === 'https:' ? 'wss:' : 'ws:'
};
}
Expand All @@ -187,9 +170,14 @@ export class WebSocketService {
const path = window.location.pathname;
const strings = path.split('/');
let wsPath = '/';
if (strings.length >= 2 && strings[1] !== '' && strings[1] !== 'index.html') {
// Simplify path handling to avoid potential issues
if (strings.length >= 2 && strings[1]) {
wsPath = '/' + strings[1] + '/';
}
// Ensure path ends with trailing slash
if (!wsPath.endsWith('/')) {
wsPath += '/';
}
console.debug(`[WebSocket] Calculated WebSocket path: ${wsPath}`);
return wsPath;
}
Expand Down
Loading

0 comments on commit f5721d1

Please sign in to comment.