Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,8 @@ For detailed examples of receiving and sending messages, see **[EMBEDDING.md](EM
```javascript
{
type: 'message',
role: 'external' | 'internal',
senderId: 100, // Integer user/patient ID
role: 'external' | 'internal' | 'system',
senderId: 100, // Integer user/patient ID (null for system messages)
channel: 'portal' | 'sms' | 'voicemail' | 'auto',
time: '2025-10-29 08:12',
text: 'Message text'
Expand Down Expand Up @@ -419,6 +419,21 @@ For detailed examples of receiving and sending messages, see **[EMBEDDING.md](EM
- `appt` - Appointments
- Additional types can be added as needed

#### System Message

```javascript
{
type: 'message',
role: 'system',
senderId: null,
channel: 'auto',
time: '2025-10-29 08:00',
text: 'New conversation initialized.'
}
```

System messages are center-aligned in a yellow text box and are used for automated messages like conversation creation notifications. They use `role: 'system'` instead of `'external'` or `'internal'`.

## Loading Custom Data

```jsx
Expand Down
8 changes: 1 addition & 7 deletions src/components/ChatComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,9 @@ const ChatComponent = ({
const setSidebarOpen = useChatStore(state => state.setSidebarOpen);
const loadConversations = useChatStore(state => state.loadConversations);
const createConversation = useChatStore(state => state.createConversation);
const setCurrentUserId = useChatStore(state => state.setCurrentUserId);
const storedCurrentUserId = useChatStore(state => state.currentUserId);
const setActiveConversation = useChatStore(state => state.setActiveConversation);
const activeConversation = useChatStore(state => state.getActiveConversation());

// Sync currentUserId prop with store
React.useEffect(() => {
setCurrentUserId(currentUserId);
}, [currentUserId, setCurrentUserId]);

// Load initial data if provided
React.useEffect(() => {
if (initialData) {
Expand Down Expand Up @@ -181,6 +174,7 @@ const ChatComponent = ({
activeConversation={activeConversation}
disableClosedConversations={disableClosedConversations}
hideDeliveryMethod={hideDeliveryMethod}
onConversationCreated={onConversationCreated}
/>
</div>
</div>
Expand Down
46 changes: 38 additions & 8 deletions src/components/ComposeArea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const ComposeArea = ({
currentUserId = null,
activeConversation,
disableClosedConversations = false,
hideDeliveryMethod = false
hideDeliveryMethod = false,
onConversationCreated = null
}) => {
const [text, setText] = useState('');
const [sendType, setSendType] = useState('auto');
Expand All @@ -15,6 +16,9 @@ const ComposeArea = ({
const fileInputRef = useRef(null);

const addMessage = useChatStore(state => state.addMessage);
const conversations = useChatStore(state => state.conversations);
const createConversation = useChatStore(state => state.createConversation);
const getActiveConversation = useChatStore(state => state.getActiveConversation);

// Fix: Use 'open' property instead of 'status', and add null check
const isClosed = disableClosedConversations && activeConversation && !activeConversation.open;
Expand Down Expand Up @@ -133,7 +137,20 @@ const ComposeArea = ({
};

const handleSend = () => {
if ((!text.trim() && images.length === 0) || isClosed || !activeConversation) return;
if ((!text.trim() && images.length === 0) || isClosed ) return;

// If no conversations exist, create a new one
let conversationToUse = activeConversation;
let newlyCreatedConversation = false;

if (!activeConversation && conversations.length === 0) {
const newConversation = createConversation('New Conversation');
conversationToUse = newConversation;
newlyCreatedConversation = true;
}

// If still no conversation (edge case), return
if (!conversationToUse) return;

const message = {
text: text.trim(),
Expand All @@ -144,12 +161,25 @@ const ComposeArea = ({

const newMessage = addMessage(message);

// Call the callback if provided
if (onMessageSent && newMessage) {
onMessageSent({
conversationId: activeConversation.id,
message: newMessage,
});
// If we created a new conversation, trigger onConversationCreated with full conversation including the message
if (newlyCreatedConversation) {
if (onConversationCreated) {
const updatedConversation = getActiveConversation();
if (updatedConversation) {
onConversationCreated({
conversationId: updatedConversation.id,
conversation: updatedConversation
});
}
}
} else {
// Only call onMessageSent if we didn't just create a conversation
if (onMessageSent && newMessage) {
onMessageSent({
conversationId: conversationToUse.id,
message: newMessage,
});
}
}

setText('');
Expand Down
18 changes: 18 additions & 0 deletions src/components/MessageThread.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ const MessageItem = ({ item, currentUserId, linkBuilder }) => {
});

if (item.type === 'message') {
// System messages are center-aligned
if (item.role === 'system') {
return (
<div className="tw-mb-5 tw-flex tw-flex-col tw-items-center">
<div
className="tw-bg-[#fff9c4] tw-border tw-border-[#fff59d] tw-rounded-xl tw-px-4 tw-py-2.5 tw-max-w-[85%] tw-text-[15px] tw-shadow-sm tw-text-center"
>
<div className="tw-text-[#666]">
<span dangerouslySetInnerHTML={{ __html: item.text.replace(/\n/g, '<br>') }} />
</div>
<div className="tw-text-xs tw-text-[#888] tw-mt-1">
{time}
</div>
</div>
</div>
);
}

// Message is on the right if current user sent it
const isCurrentUser = item.senderId === currentUserId;
const senderLabel = item.sender_name || (isCurrentUser ? 'You' : 'Other');
Expand Down
2 changes: 0 additions & 2 deletions src/sample-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,11 @@ export const sampleConversations = [
];

export const sampleActiveConversationId = 735;
export const sampleCurrentUserId = null;

// Helper function to get initial demo state
export function getDemoInitialState() {
return {
conversations: sampleConversations,
activeConversationId: sampleActiveConversationId,
currentUserId: sampleCurrentUserId,
};
}
23 changes: 4 additions & 19 deletions src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const useChatStore = create((set, get) => ({
activeConversationId: null,
searchQuery: '',
sidebarOpen: false,
currentUserId: null,
conversationsLoading: false,
conversationsHasMore: false,
threadLoadingStates: {}, // { [conversationId]: { isLoading: boolean, hasMore: boolean } }
Expand Down Expand Up @@ -131,15 +130,12 @@ const useChatStore = create((set, get) => ({
lastActivity: now,
thread: [
{
type: 'ref',
refType: 'appt',
refId: null,
title: 'Conversation Created',
role: 'internal',
type: 'message',
role: 'system',
senderId: null,
channel: 'auto',
time: now,
text: 'Conversation Created: New conversation initialized.'
text: 'New conversation initialized.'
}
]
};
Expand All @@ -166,12 +162,6 @@ const useChatStore = create((set, get) => ({
set({ sidebarOpen: open });
},

// Set current user ID
setCurrentUserId: (userId) => {
const intUserId = userId !== null && userId !== undefined ? parseInt(userId, 10) : null;
set({ currentUserId: intUserId });
},

// Replace entire state with new data
loadConversations: (data) => {
// Ensure all IDs are integers
Expand All @@ -192,14 +182,10 @@ const useChatStore = create((set, get) => ({
const activeId = data.activeConversationId
? parseInt(data.activeConversationId, 10)
: conversations[0]?.id || null;
const userId = data.currentUserId !== undefined
? parseInt(data.currentUserId, 10)
: null;

set({
conversations,
activeConversationId: activeId,
...(userId !== null && { currentUserId: userId }),
});
},

Expand Down Expand Up @@ -320,11 +306,10 @@ const useChatStore = create((set, get) => ({

// Export current state
exportState: () => {
const { conversations, activeConversationId, currentUserId } = get();
const { conversations, activeConversationId } = get();
return {
conversations,
activeConversationId,
currentUserId,
};
},
}));
Expand Down
15 changes: 15 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ Tests the Tailwind demo simulator functionality:
- Responsive layout in simulator mode
- Rapid toggling stability

### `system-messages.spec.js`
Tests system message functionality:
- System message creation when new conversation is created
- Center alignment of system messages
- Yellow background styling (distinct from regular messages)
- No sender name displayed for system messages
- Timestamp display
- Visual distinction from regular messages
- Proper rendering with text-center alignment
- Handling multiple system messages in a conversation
- No channel icons displayed for system messages

### `link-builder.spec.js`
Tests custom link builder functionality for references.

## Running Tests

### Run all tests
Expand Down
8 changes: 4 additions & 4 deletions tests/image-attachment.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ test.describe('Image Attachment Feature', () => {
const attachButton = page.getByRole('button', { name: 'Attach image' });

// Create a temporary test image file
const testImagePath = path.join(process.cwd(), 'test-image.png');
const testImagePath = path.join(process.cwd(), 'test-image-preview.png');
fs.writeFileSync(testImagePath, createTestImageBuffer());

try {
Expand Down Expand Up @@ -75,7 +75,7 @@ test.describe('Image Attachment Feature', () => {
const attachButton = page.getByRole('button', { name: 'Attach image' });

// Create a temporary test image file
const testImagePath = path.join(process.cwd(), 'test-image.png');
const testImagePath = path.join(process.cwd(), 'test-image-remove.png');
fs.writeFileSync(testImagePath, createTestImageBuffer());

try {
Expand Down Expand Up @@ -109,7 +109,7 @@ test.describe('Image Attachment Feature', () => {
const attachButton = page.getByRole('button', { name: 'Attach image' });

// Create a temporary test image file
const testImagePath = path.join(process.cwd(), 'test-image.png');
const testImagePath = path.join(process.cwd(), 'test-image-send.png');
fs.writeFileSync(testImagePath, createTestImageBuffer());

try {
Expand Down Expand Up @@ -158,7 +158,7 @@ test.describe('Image Attachment Feature', () => {
const attachButton = page.getByRole('button', { name: 'Attach image' });

// Create a temporary test image file
const testImagePath = path.join(process.cwd(), 'test-image.png');
const testImagePath = path.join(process.cwd(), 'test-image-only.png');
fs.writeFileSync(testImagePath, createTestImageBuffer());

try {
Expand Down
Loading
Loading