hmc-rag / frontend /src /hooks /useConversations.ts
webmuppet
Refactor: replace Streamlit with React+Vite SPA + FastAPI server
b5c0df4
Raw
History Blame Contribute Delete
3.03 kB
import { useState, useCallback, useEffect } from "react";
import type { Conversation } from "../types";
import { readConversations, writeConversations, titleFromMessages } from "../lib/storage";
export interface UseConversationsReturn {
conversations: Conversation[];
upsertConversation: (conv: Conversation) => void;
deleteConversation: (id: string) => void;
loadConversation: (id: string) => Conversation | undefined;
}
/**
* Manages conversation persistence in localStorage via storage.ts helpers.
*
* - Reads `hmc-conversations` on mount.
* - Exposes `upsertConversation(conv)` to insert or update a conversation,
* automatically deriving the title from the first user message and updating
* `last_active_at` to the current time.
* - Exposes `deleteConversation(id)` to remove a conversation by ID.
* - Exposes `loadConversation(id)` to retrieve a conversation by ID from
* the current in-memory list (no server round-trip).
*
* Requirements: 6.1, 6.2, 6.3, 6.5, 6.6, 6.7
*/
export function useConversations(): UseConversationsReturn {
const [conversations, setConversations] = useState<Conversation[]>(() =>
readConversations()
);
// Sync from localStorage on mount (handles cases where another tab wrote)
useEffect(() => {
setConversations(readConversations());
}, []);
/**
* Insert or update a conversation. If a conversation with the same `id`
* already exists, it is replaced. The title is re-derived from messages
* and `last_active_at` is set to now.
*
* The cap of 20 conversations is enforced by `writeConversations` in storage.ts.
*/
const upsertConversation = useCallback((conv: Conversation): void => {
setConversations((prev) => {
const now = new Date().toISOString();
const updated: Conversation = {
...conv,
title: titleFromMessages(conv.messages),
last_active_at: now,
created_at: conv.created_at || now,
};
const existing = prev.findIndex((c) => c.id === conv.id);
let next: Conversation[];
if (existing >= 0) {
next = [...prev];
next[existing] = updated;
} else {
next = [updated, ...prev];
}
writeConversations(next);
return readConversations(); // re-read to get the sorted, capped result
});
}, []);
/**
* Remove a conversation by ID from localStorage and update state.
*/
const deleteConversation = useCallback((id: string): void => {
setConversations((prev) => {
const next = prev.filter((c) => c.id !== id);
writeConversations(next);
return next;
});
}, []);
/**
* Retrieve a conversation by ID from the current in-memory list.
* Returns undefined if not found. No server round-trip.
*/
const loadConversation = useCallback(
(id: string): Conversation | undefined => {
return conversations.find((c) => c.id === id);
},
[conversations]
);
return { conversations, upsertConversation, deleteConversation, loadConversation };
}