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) && (
- {(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;