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
8 changes: 8 additions & 0 deletions frontend/chat-ui/src/components/MessageActions.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import React from 'react';
import { useStore } from '../store';

/** @typedef {import('../types').Message} Message */

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
import { API_BASE_URL } from '../api';

/**
* Action buttons associated with a message.
* @param {{message: Message}} props
*/
export default function MessageActions({ message }) {
const { state } = useStore();

Expand Down
6 changes: 6 additions & 0 deletions frontend/chat-ui/src/components/MessageBubble.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import React from 'react';
import MessageActions from './MessageActions.jsx';

/** @typedef {import('../types').Message} Message */

/**
* Displays a single chat message.
* @param {{message: Message}} props
*/
export default function MessageBubble({ message }) {
const isUser = message.role === 'user' || message.sender === 'user' || message.from === 'user';
const alignment = isUser ? 'items-end text-right' : 'items-start text-left';
Expand Down
29 changes: 29 additions & 0 deletions frontend/chat-ui/src/components/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@
*/
import React, { useEffect } from 'react';
import { useStore } from '../store';

/**
* @typedef {import('../types').Conversation} Conversation
* @typedef {import('../types').Message} Message
*/

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';

import { API_BASE_URL } from '../api';
import { get, post } from '../api';

/**
* Sidebar containing conversation list and controls.
* @returns {JSX.Element}
*/
export default function Sidebar() {
const { state, dispatch } = useStore();

Expand All @@ -38,8 +50,19 @@ export default function Sidebar() {
}
};

/**
* Load a conversation and its messages.
* @param {Conversation} c
*/
const openConversation = async (c) => {
try {
const res = await fetch(`${API_BASE_URL}/messages/${c.id}`, {
headers: { 'x-api-key': import.meta.env.VITE_API_KEY },
});
const msgs = /** @type {Message[]} */ (await res.json());
dispatch({ type: 'SET_CURRENT_CONVERSATION', conversation: c });
dispatch({ type: 'SET_MESSAGES', messages: msgs });

const msgs = await get(`/messages/${c.id}`);
dispatch({ type: 'SET_CURRENT_CONVERSATION', conversation: c });
dispatch({ type: 'SET_MESSAGES', messages: msgs });
Expand All @@ -48,6 +71,10 @@ export default function Sidebar() {
}
};

/**
* Toggle the pinned state of a conversation.
* @param {Conversation} c
*/
const togglePin = async (c) => {
try {
await post(`/conversations/${c.id}/pin`, { pinned: !c.pinned });
Expand All @@ -61,7 +88,9 @@ export default function Sidebar() {
}
};

/** @type {Conversation[]} */
const pinned = state.conversations.filter((c) => c.pinned);
/** @type {Conversation[]} */
const others = state.conversations.filter((c) => !c.pinned);

return (
Expand Down
46 changes: 46 additions & 0 deletions frontend/chat-ui/src/store.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
import React, { createContext, useContext, useReducer } from 'react';

/**
* @typedef {import('./types').Message} Message
* @typedef {import('./types').Conversation} Conversation
*/

/**
* @typedef {Object} State
* @property {Message[]} messages
* @property {Conversation[]} conversations
* @property {Conversation|null} currentConversation
* @property {boolean} sidebarOpen
* @property {string} theme
* @property {boolean} memory
*/

/**
* @typedef {(
* | { type: 'ADD_MESSAGE', message: Message }
* | { type: 'UPSERT_MESSAGE', message: Message }
* | { type: 'APPEND_MESSAGE_CONTENT', id: string, content: string }
* | { type: 'SET_MESSAGES', messages: Message[] }
* | { type: 'SET_CONVERSATIONS', conversations: Conversation[] }
* | { type: 'SET_CURRENT_CONVERSATION', conversation: Conversation | null }
* | { type: 'TOGGLE_SIDEBAR' }
* | { type: 'SET_THEME', theme: string }
* | { type: 'TOGGLE_MEMORY' }
* )} Action
*/

/** @type {State} */
const initialState = {
messages: [],
conversations: [],
Expand All @@ -9,6 +39,12 @@ const initialState = {
memory: true,
};

/**
* Reducer handling chat UI actions.
* @param {State} state
* @param {Action} action
* @returns {State}
*/
function reducer(state, action) {
switch (action.type) {
case 'ADD_MESSAGE':
Expand Down Expand Up @@ -46,8 +82,14 @@ function reducer(state, action) {
}
}

/** @type {React.Context<{state: State, dispatch: React.Dispatch<Action>}>} */
const StoreContext = createContext();

/**
* Provides the global store context.
* @param {{children: React.ReactNode}} props
* @returns {JSX.Element}
*/
export function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
Expand All @@ -57,6 +99,10 @@ export function StoreProvider({ children }) {
);
}

/**
* Hook to access store state and dispatch.
* @returns {{state: State, dispatch: React.Dispatch<Action>}}
*/
export function useStore() {
return useContext(StoreContext);
}
24 changes: 24 additions & 0 deletions frontend/chat-ui/src/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Represents a single chat message.
* @typedef {Object} Message
* @property {string} id - Unique identifier for the message.
* @property {string} [role] - Role associated with the message sender.
* @property {string} [sender] - Alternative field for the sender.
* @property {string} [from] - Original sender of the message.
* @property {string} [to] - Intended recipient of the message.
* @property {string} [type] - Type of message, e.g. 'text' or 'file'.
* @property {string} [content] - Textual content of the message.
* @property {string} [message] - Alternate field for message content.
* @property {{path:string,originalname:string}} [file] - File metadata when type is 'file'.
* @property {number} [timestamp] - Unix epoch timestamp for when the message was created.
*/

/**
* Represents a chat conversation.
* @typedef {Object} Conversation
* @property {string} id - Unique identifier for the conversation.
* @property {string} title - Display title of the conversation.
* @property {boolean} [pinned] - Whether the conversation is pinned in the sidebar.
*/

export {};