diff --git a/frontend/chat-ui/src/components/MessageActions.jsx b/frontend/chat-ui/src/components/MessageActions.jsx index b4c5bf0..3ce6d96 100644 --- a/frontend/chat-ui/src/components/MessageActions.jsx +++ b/frontend/chat-ui/src/components/MessageActions.jsx @@ -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(); diff --git a/frontend/chat-ui/src/components/MessageBubble.jsx b/frontend/chat-ui/src/components/MessageBubble.jsx index b8c6732..95027e7 100644 --- a/frontend/chat-ui/src/components/MessageBubble.jsx +++ b/frontend/chat-ui/src/components/MessageBubble.jsx @@ -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'; diff --git a/frontend/chat-ui/src/components/Sidebar.jsx b/frontend/chat-ui/src/components/Sidebar.jsx index e14a085..b158df6 100644 --- a/frontend/chat-ui/src/components/Sidebar.jsx +++ b/frontend/chat-ui/src/components/Sidebar.jsx @@ -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(); @@ -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 }); @@ -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 }); @@ -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 ( diff --git a/frontend/chat-ui/src/store.jsx b/frontend/chat-ui/src/store.jsx index f2a88a7..71a1948 100644 --- a/frontend/chat-ui/src/store.jsx +++ b/frontend/chat-ui/src/store.jsx @@ -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: [], @@ -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': @@ -46,8 +82,14 @@ function reducer(state, action) { } } +/** @type {React.Context<{state: State, dispatch: React.Dispatch}>} */ 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 ( @@ -57,6 +99,10 @@ export function StoreProvider({ children }) { ); } +/** + * Hook to access store state and dispatch. + * @returns {{state: State, dispatch: React.Dispatch}} + */ export function useStore() { return useContext(StoreContext); } diff --git a/frontend/chat-ui/src/types.js b/frontend/chat-ui/src/types.js new file mode 100644 index 0000000..2b025d7 --- /dev/null +++ b/frontend/chat-ui/src/types.js @@ -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 {};