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:
parent
752d455312
commit
ee4c0dcda1
4 changed files with 529 additions and 0 deletions
100
gbapp/MESSAGE_TYPES.md
Normal file
100
gbapp/MESSAGE_TYPES.md
Normal 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
|
||||
82
src/core/shared/message_types.rs
Normal file
82
src/core/shared/message_types.rs
Normal 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
247
tests/websocket_test.html
Normal 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
100
ui/shared/messageTypes.js
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue