diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index ac86bad..2385180 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -3,11 +3,12 @@ import { ChatMessageComponent } from './ChatMessage'; import { ChatInput } from './ChatInput'; import { TypingIndicator } from './TypingIndicator'; import type { ChatMessage, ConnectionStatus } from '../types'; -import { Bot, ArrowDown, Loader2, ChevronsDownUp, ChevronsUpDown, Sparkles } from 'lucide-react'; +import { Bot, ArrowDown, Loader2, ChevronsDownUp, ChevronsUpDown, Sparkles, Bookmark } from 'lucide-react'; import { MessageSearch } from './MessageSearch'; import { useT } from '../hooks/useLocale'; import { getLocale, type TranslationKey } from '../lib/i18n'; import { useToolCollapse } from '../hooks/useToolCollapse'; +import { useBookmarks } from '../hooks/useBookmarks'; interface Props { messages: ChatMessage[]; @@ -174,6 +175,9 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session const showTyping = isGenerating && !hasStreamedText(messages); const { globalState, collapseAll, expandAll } = useToolCollapse(); + const { toggle: toggleBookmark, isBookmarked, getForSession: getBookmarks } = useBookmarks(); + const sessionBookmarks = useMemo(() => sessionKey ? getBookmarks(sessionKey) : [], [getBookmarks, sessionKey]); + const [showBookmarks, setShowBookmarks] = useState(false); const hasToolCalls = useMemo(() => messages.some(m => m.blocks.some(b => b.type === 'tool_use' || b.type === 'tool_result')), [messages]); // Message search @@ -280,7 +284,7 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session )}
- + toggleBookmark(msg.id, sessionKey, (msg.content || '').slice(0, 120), msg.timestamp) : undefined} />
); @@ -288,6 +292,28 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session {showTyping && }
+ {/* Bookmarks panel */} + {showBookmarks && sessionBookmarks.length > 0 && ( +
+
+
{t('chat.bookmarks')}
+ {sessionBookmarks.map(b => ( + + ))} +
+
+ )} {/* Floating action buttons — sticky to bottom of scroll area */} {(hasToolCalls || showScrollBtn || newMessageCount > 0) && (
@@ -302,6 +328,17 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session {globalState === 'expand-all' ? : } )} + {sessionBookmarks.length > 0 && ( + + )} {(showScrollBtn || newMessageCount > 0) && ( + )} setShowRawJson(o => !o)} />
@@ -538,8 +548,11 @@ export const ChatMessageComponent = memo(function ChatMessageComponent({ message {/* Raw JSON viewer */} {showRawJson && } - {(message.timestamp || wasWebhookMessage) && ( + {(message.timestamp || wasWebhookMessage || isBookmarked) && (
+ {isBookmarked && ( + + )} {wasWebhookMessage && ( diff --git a/src/hooks/useBookmarks.ts b/src/hooks/useBookmarks.ts new file mode 100644 index 0000000..bf05ab6 --- /dev/null +++ b/src/hooks/useBookmarks.ts @@ -0,0 +1,68 @@ +import { useState, useCallback } from 'react'; + +const STORAGE_KEY = 'pinchchat-bookmarks'; + +export interface Bookmark { + messageId: string; + sessionKey: string; + preview: string; + timestamp: number; + bookmarkedAt: number; +} + +function loadBookmarks(): Bookmark[] { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) return JSON.parse(raw) as Bookmark[]; + } catch { /* noop */ } + return []; +} + +function saveBookmarks(bookmarks: Bookmark[]) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks)); + } catch { /* noop */ } +} + +export function useBookmarks() { + const [bookmarks, setBookmarks] = useState(loadBookmarks); + + const toggle = useCallback((messageId: string, sessionKey: string, preview: string, timestamp: number) => { + setBookmarks(prev => { + const exists = prev.some(b => b.messageId === messageId); + const next = exists + ? prev.filter(b => b.messageId !== messageId) + : [...prev, { messageId, sessionKey, preview: preview.slice(0, 120), timestamp, bookmarkedAt: Date.now() }]; + saveBookmarks(next); + return next; + }); + }, []); + + const isBookmarked = useCallback((messageId: string) => { + return bookmarks.some(b => b.messageId === messageId); + }, [bookmarks]); + + const getForSession = useCallback((sessionKey: string) => { + return bookmarks + .filter(b => b.sessionKey === sessionKey) + .sort((a, b) => a.timestamp - b.timestamp); + }, [bookmarks]); + + const remove = useCallback((messageId: string) => { + setBookmarks(prev => { + const next = prev.filter(b => b.messageId !== messageId); + saveBookmarks(next); + return next; + }); + }, []); + + const clearSession = useCallback((sessionKey: string) => { + setBookmarks(prev => { + const next = prev.filter(b => b.sessionKey !== sessionKey); + saveBookmarks(next); + return next; + }); + }, []); + + return { bookmarks, toggle, isBookmarked, getForSession, remove, clearSession }; +} diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 89ecd3a..2a287f8 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -142,6 +142,11 @@ const en = { 'settings.sendShortcut': 'Send with', 'settings.sendEnter': 'Enter', 'settings.sendCtrlEnter': 'Ctrl+Enter', + + // Bookmarks + 'message.bookmark': 'Bookmark message', + 'message.removeBookmark': 'Remove bookmark', + 'chat.bookmarks': 'Bookmarks', } as const; const fr: Record = { @@ -264,6 +269,10 @@ const fr: Record = { 'settings.sendShortcut': 'Envoyer avec', 'settings.sendEnter': 'Entrée', 'settings.sendCtrlEnter': 'Ctrl+Entrée', + + 'message.bookmark': 'Marquer le message', + 'message.removeBookmark': 'Retirer le marque-page', + 'chat.bookmarks': 'Marque-pages', }; export type TranslationKey = keyof typeof en;