Add message type constants and documentation

Introduce a shared enum-based system for categorizing message types
across the Rust backend and JavaScript frontend. This replaces magic
numbers with named constants for improved type safety, readability, and
maintainability.

The implementation includes:
- Rust MessageType enum with serialization support
- JavaScript constants matching the Rust enum values
- Helper
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-29 11:17:01 -03:00
parent 752d455312
commit ee4c0dcda1
4 changed files with 529 additions and 0 deletions

100
gbapp/MESSAGE_TYPES.md Normal file
View file

@ -0,0 +1,100 @@
# Message Types Documentation
## Overview
The botserver uses a simple enum-based system for categorizing different types of messages flowing through the system. This document describes each message type and its usage.
## Message Type Enum
The `MessageType` enum is defined in both Rust (backend) and JavaScript (frontend) to ensure consistency across the entire application.
### Backend (Rust)
Location: `src/core/shared/message_types.rs`
### Frontend (JavaScript)
Location: `ui/shared/messageTypes.js`
## Message Types
| Value | Name | Description | Usage |
|-------|------|-------------|-------|
| 0 | `EXTERNAL` | Messages from external systems | WhatsApp, Instagram, Teams, and other external channel integrations |
| 1 | `USER` | User messages from web interface | Regular user input from the web chat interface |
| 2 | `BOT_RESPONSE` | Bot responses | Can contain either regular text content or JSON-encoded events (theme changes, thinking indicators, etc.) |
| 3 | `CONTINUE` | Continue interrupted response | Used when resuming a bot response that was interrupted |
| 4 | `SUGGESTION` | Suggestion or command message | Used for contextual suggestions and command messages |
| 5 | `CONTEXT_CHANGE` | Context change notification | Signals when the conversation context has changed |
## Special Handling for BOT_RESPONSE (Type 2)
The `BOT_RESPONSE` type requires special handling in the frontend because it can contain two different types of content:
### 1. Regular Text Content
Standard bot responses containing plain text or markdown that should be displayed directly to the user.
### 2. Event Messages
JSON-encoded objects with the following structure:
```json
{
"event": "event_type",
"data": {
// Event-specific data
}
}
```
#### Supported Events:
- `thinking_start` - Bot is processing/thinking
- `thinking_end` - Bot finished processing
- `warn` - Warning message to display
- `context_usage` - Context usage update
- `change_theme` - Theme customization data
## Frontend Detection Logic
The frontend uses the following logic to differentiate between regular content and event messages:
1. Check if `message_type === 2` (BOT_RESPONSE)
2. Check if content starts with `{` or `[` (potential JSON)
3. Attempt to parse as JSON
4. If successful and has `event` and `data` properties, handle as event
5. Otherwise, process as regular message content
## Usage Examples
### Rust Backend
```rust
use crate::shared::message_types::MessageType;
let response = BotResponse {
// ... other fields
message_type: MessageType::BOT_RESPONSE,
// ...
};
```
### JavaScript Frontend
```javascript
if (message.message_type === MessageType.BOT_RESPONSE) {
// Handle bot response
}
if (isUserMessage(message)) {
// Handle user message
}
```
## Migration Notes
When migrating from magic numbers to the MessageType enum:
1. Replace all hardcoded message type numbers with the appropriate constant
2. Import the MessageType module/script where needed
3. Use the helper functions for type checking when available
## Benefits
1. **Type Safety**: Reduces errors from using wrong message type numbers
2. **Readability**: Code is self-documenting with named constants
3. **Maintainability**: Easy to add new message types or modify existing ones
4. **Consistency**: Same values used across frontend and backend

View file

@ -0,0 +1,82 @@
use serde::{Deserialize, Serialize};
/// Enum representing different types of messages in the bot system
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct MessageType(pub i32);
impl MessageType {
/// Regular message from external systems (WhatsApp, Instagram, etc.)
pub const EXTERNAL: MessageType = MessageType(0);
/// User message from web interface
pub const USER: MessageType = MessageType(1);
/// Bot response (can be regular content or event)
pub const BOT_RESPONSE: MessageType = MessageType(2);
/// Continue interrupted response
pub const CONTINUE: MessageType = MessageType(3);
/// Suggestion or command message
pub const SUGGESTION: MessageType = MessageType(4);
/// Context change notification
pub const CONTEXT_CHANGE: MessageType = MessageType(5);
}
impl From<i32> for MessageType {
fn from(value: i32) -> Self {
MessageType(value)
}
}
impl From<MessageType> for i32 {
fn from(value: MessageType) -> Self {
value.0
}
}
impl Default for MessageType {
fn default() -> Self {
MessageType::USER
}
}
impl std::fmt::Display for MessageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self.0 {
0 => "EXTERNAL",
1 => "USER",
2 => "BOT_RESPONSE",
3 => "CONTINUE",
4 => "SUGGESTION",
5 => "CONTEXT_CHANGE",
_ => "UNKNOWN",
};
write!(f, "{}", name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_type_conversion() {
assert_eq!(i32::from(MessageType::USER), 1);
assert_eq!(MessageType::from(2), MessageType::BOT_RESPONSE);
}
#[test]
fn test_message_type_display() {
assert_eq!(MessageType::USER.to_string(), "USER");
assert_eq!(MessageType::BOT_RESPONSE.to_string(), "BOT_RESPONSE");
}
#[test]
fn test_message_type_equality() {
assert_eq!(MessageType::USER, MessageType(1));
assert_ne!(MessageType::USER, MessageType::BOT_RESPONSE);
}
}

247
tests/websocket_test.html Normal file
View file

@ -0,0 +1,247 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Message Handler Test</title>
<style>
body {
font-family: monospace;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.test-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.test-case {
margin: 10px 0;
padding: 10px;
background: #f5f5f5;
}
.success {
color: green;
font-weight: bold;
}
.error {
color: red;
font-weight: bold;
}
.info {
color: blue;
}
button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
}
#output {
background: #000;
color: #0f0;
padding: 10px;
height: 300px;
overflow-y: auto;
margin-top: 20px;
}
</style>
</head>
<body>
<h1>WebSocket Message Handler Test Suite</h1>
<div class="test-section">
<h2>Test Cases</h2>
<button onclick="runAllTests()">Run All Tests</button>
<button onclick="clearOutput()">Clear Output</button>
</div>
<div class="test-section">
<h2>Individual Tests</h2>
<button onclick="testEmptyMessage()">Test Empty Message</button>
<button onclick="testValidEventMessage()">Test Valid Event Message</button>
<button onclick="testRegularTextMessage()">Test Regular Text Message</button>
<button onclick="testMalformedJSON()">Test Malformed JSON</button>
<button onclick="testIncompleteJSON()">Test Incomplete JSON</button>
<button onclick="testContextMessage()">Test Context Message</button>
</div>
<div id="output"></div>
<script>
// Simulate the message handler from the actual implementation
function handleEvent(eventType, eventData) {
log(`Event handled: ${eventType}`, 'info');
log(`Event data: ${JSON.stringify(eventData)}`, 'info');
}
function processMessageContent(message) {
log(`Processing message content: ${message.content}`, 'info');
}
function handleWebSocketMessage(rawData) {
try {
if (!rawData || rawData.trim() === "") {
log("Empty WebSocket message received", 'error');
return { success: false, reason: "Empty message" };
}
const r = JSON.parse(rawData);
if (r.type === "connected") {
log("WebSocket welcome message", 'info');
return { success: true, type: "connected" };
}
if (r.message_type === 2) {
// Check if content looks like JSON (starts with { or [)
const contentTrimmed = r.content.trim();
if (contentTrimmed.startsWith("{") || contentTrimmed.startsWith("[")) {
try {
const d = JSON.parse(r.content);
if (d.event && d.data) {
// This is an event message
handleEvent(d.event, d.data);
return { success: true, type: "event", event: d.event };
}
} catch (parseErr) {
// Not a valid event message, treat as regular content
log("Content is not an event message, processing as regular message", 'info');
}
}
// Process as regular message content
processMessageContent(r);
return { success: true, type: "regular_message" };
}
if (r.message_type === 5) {
log("Context change message", 'info');
return { success: true, type: "context_change" };
}
processMessageContent(r);
return { success: true, type: "default_message" };
} catch (err) {
log(`WebSocket message parse error: ${err.message}`, 'error');
return { success: false, reason: err.message };
}
}
// Test cases
function testEmptyMessage() {
log("\n=== Testing Empty Message ===", 'info');
const result = handleWebSocketMessage("");
if (!result.success && result.reason === "Empty message") {
log("✓ Empty message handled correctly", 'success');
} else {
log("✗ Empty message not handled correctly", 'error');
}
}
function testValidEventMessage() {
log("\n=== Testing Valid Event Message ===", 'info');
const message = JSON.stringify({
message_type: 2,
content: JSON.stringify({
event: "thinking_start",
data: { message: "Processing..." }
})
});
const result = handleWebSocketMessage(message);
if (result.success && result.type === "event") {
log("✓ Valid event message handled correctly", 'success');
} else {
log("✗ Valid event message not handled correctly", 'error');
}
}
function testRegularTextMessage() {
log("\n=== Testing Regular Text Message (type 2) ===", 'info');
const message = JSON.stringify({
message_type: 2,
content: "This is a regular text message, not JSON"
});
const result = handleWebSocketMessage(message);
if (result.success && result.type === "regular_message") {
log("✓ Regular text message handled correctly", 'success');
} else {
log("✗ Regular text message not handled correctly", 'error');
}
}
function testMalformedJSON() {
log("\n=== Testing Malformed JSON in Content ===", 'info');
const message = JSON.stringify({
message_type: 2,
content: "{invalid json: true"
});
const result = handleWebSocketMessage(message);
if (result.success && result.type === "regular_message") {
log("✓ Malformed JSON handled gracefully", 'success');
} else {
log("✗ Malformed JSON not handled correctly", 'error');
}
}
function testIncompleteJSON() {
log("\n=== Testing Incomplete JSON ===", 'info');
const message = JSON.stringify({
message_type: 2,
content: '{"event": "test", "data":' // Incomplete JSON
});
const result = handleWebSocketMessage(message);
if (result.success && result.type === "regular_message") {
log("✓ Incomplete JSON handled gracefully", 'success');
} else {
log("✗ Incomplete JSON not handled correctly", 'error');
}
}
function testContextMessage() {
log("\n=== Testing Context Message (type 5) ===", 'info');
const message = JSON.stringify({
message_type: 5,
context_name: "test_context",
content: "Context content"
});
const result = handleWebSocketMessage(message);
if (result.success && result.type === "context_change") {
log("✓ Context message handled correctly", 'success');
} else {
log("✗ Context message not handled correctly", 'error');
}
}
function runAllTests() {
log("Starting all tests...\n", 'info');
testEmptyMessage();
testValidEventMessage();
testRegularTextMessage();
testMalformedJSON();
testIncompleteJSON();
testContextMessage();
log("\n=== All tests completed ===", 'info');
}
function log(message, type = 'info') {
const output = document.getElementById('output');
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const className = type === 'error' ? 'error' : type === 'success' ? 'success' : 'info';
output.innerHTML += `<span class="${className}">[${timestamp}] ${message}</span>\n`;
output.scrollTop = output.scrollHeight;
}
function clearOutput() {
document.getElementById('output').innerHTML = '';
}
// Run tests on page load
window.onload = function() {
log("WebSocket Message Handler Test Suite Ready", 'info');
log("Click 'Run All Tests' to begin\n", 'info');
};
</script>
</body>
</html>

100
ui/shared/messageTypes.js Normal file
View file

@ -0,0 +1,100 @@
/**
* Message Type Constants
* Defines the different types of messages in the bot system
* These values must match the server-side MessageType enum in Rust
*/
const MessageType = {
/** Regular message from external systems (WhatsApp, Instagram, etc.) */
EXTERNAL: 0,
/** User message from web interface */
USER: 1,
/** Bot response (can be regular content or event) */
BOT_RESPONSE: 2,
/** Continue interrupted response */
CONTINUE: 3,
/** Suggestion or command message */
SUGGESTION: 4,
/** Context change notification */
CONTEXT_CHANGE: 5
};
/**
* Get the name of a message type
* @param {number} type - The message type number
* @returns {string} The name of the message type
*/
function getMessageTypeName(type) {
const names = {
0: 'EXTERNAL',
1: 'USER',
2: 'BOT_RESPONSE',
3: 'CONTINUE',
4: 'SUGGESTION',
5: 'CONTEXT_CHANGE'
};
return names[type] || 'UNKNOWN';
}
/**
* Check if a message is a bot response
* @param {Object} message - The message object
* @returns {boolean} True if the message is a bot response
*/
function isBotResponse(message) {
return message && message.message_type === MessageType.BOT_RESPONSE;
}
/**
* Check if a message is a user message
* @param {Object} message - The message object
* @returns {boolean} True if the message is from a user
*/
function isUserMessage(message) {
return message && message.message_type === MessageType.USER;
}
/**
* Check if a message is a context change
* @param {Object} message - The message object
* @returns {boolean} True if the message is a context change
*/
function isContextChange(message) {
return message && message.message_type === MessageType.CONTEXT_CHANGE;
}
/**
* Check if a message is a suggestion
* @param {Object} message - The message object
* @returns {boolean} True if the message is a suggestion
*/
function isSuggestion(message) {
return message && message.message_type === MessageType.SUGGESTION;
}
// Export for use in other modules (if using modules)
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
MessageType,
getMessageTypeName,
isBotResponse,
isUserMessage,
isContextChange,
isSuggestion
};
}
// Also make available globally for non-module scripts
if (typeof window !== 'undefined') {
window.MessageType = MessageType;
window.getMessageTypeName = getMessageTypeName;
window.isBotResponse = isBotResponse;
window.isUserMessage = isUserMessage;
window.isContextChange = isContextChange;
window.isSuggestion = isSuggestion;
}