Skip to content

Commit 3ab4e34

Browse files
milispclaude
andcommitted
feat: add text selection and note integration for chat messages
Add SelectableText component that allows users to select text from assistant messages, tool outputs, and user messages, then add the selected content to notes. Includes floating action buttons, smart positioning, and automatic note creation/updating. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent db5138f commit 3ab4e34

File tree

3 files changed

+577
-85
lines changed

3 files changed

+577
-85
lines changed

src/components/ClaudeCodeSession.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
111111
// Three-pane layout state
112112
const [selectedFilePath, setSelectedFilePath] = useState<string | null>(null);
113113
const [leftPanelTab, setLeftPanelTab] = useState<'files' | 'notes'>('files');
114-
const [leftPanelWidth, setLeftPanelWidth] = useState(300);
115-
const [middlePanelWidth, setMiddlePanelWidth] = useState(400);
114+
const [leftPanelWidth] = useState(300);
115+
const [middlePanelWidth] = useState(400);
116116

117117
// Add collapsed state for queued prompts
118118
const [queuedPromptsCollapsed, setQueuedPromptsCollapsed] = useState(false);
@@ -1213,6 +1213,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
12131213
message={message}
12141214
streamMessages={messages}
12151215
onLinkDetected={handleLinkDetected}
1216+
onSwitchToNotes={() => setLeftPanelTab('notes')}
12161217
/>
12171218
</motion.div>
12181219
);

src/components/SelectableText.tsx

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import React, { useState, useRef } from "react";
2+
import { Button } from "@/components/ui/button";
3+
import { FileText, Plus } from "lucide-react";
4+
import { cn } from "@/lib/utils";
5+
import { useNoteStore } from "@/hooks/useNoteStore";
6+
7+
interface SelectableTextProps {
8+
children: React.ReactNode;
9+
className?: string;
10+
source?: string; // Source identifier for the content
11+
onTabSwitch?: () => void; // Callback to switch to notes tab
12+
}
13+
14+
export function SelectableText({
15+
children,
16+
className,
17+
source = "Chat Message",
18+
onTabSwitch
19+
}: SelectableTextProps) {
20+
const [selectedText, setSelectedText] = useState<string>("");
21+
const [showActions, setShowActions] = useState(false);
22+
const [selectionPosition, setSelectionPosition] = useState({ x: 0, y: 0 });
23+
const containerRef = useRef<HTMLDivElement>(null);
24+
25+
const handleTextSelection = () => {
26+
const selection = window.getSelection();
27+
console.log('Text selection event:', { selection, text: selection?.toString() });
28+
29+
if (selection && selection.toString().trim()) {
30+
const selectedString = selection.toString().trim();
31+
setSelectedText(selectedString);
32+
33+
// Get selection position for floating actions
34+
const range = selection.getRangeAt(0);
35+
const rect = range.getBoundingClientRect();
36+
37+
// Get the viewport dimensions
38+
const viewportWidth = window.innerWidth;
39+
const viewportHeight = window.innerHeight;
40+
const buttonWidth = 220; // Approximate width of the button container
41+
const buttonHeight = 60; // Approximate height of the button container
42+
43+
// Calculate position relative to viewport
44+
let x = rect.left + rect.width / 2;
45+
let y = rect.top - buttonHeight - 15; // Position above the selection with margin
46+
47+
// Ensure horizontal position stays within viewport bounds
48+
const minX = 10; // Minimum margin from left edge
49+
const maxX = viewportWidth - buttonWidth - 10; // Maximum position considering button width
50+
51+
if (x - buttonWidth / 2 < minX) {
52+
x = minX + buttonWidth / 2;
53+
} else if (x - buttonWidth / 2 > maxX) {
54+
x = maxX + buttonWidth / 2;
55+
}
56+
57+
// Ensure vertical position is visible
58+
const minY = 10; // Minimum margin from top
59+
const maxY = viewportHeight - buttonHeight - 10; // Maximum position considering button height
60+
61+
if (y < minY) {
62+
// If no space above, position below the selection
63+
y = rect.bottom + 15;
64+
if (y > maxY) {
65+
// If still no space below, position at max allowed position
66+
y = maxY;
67+
}
68+
} else if (y > maxY) {
69+
y = maxY;
70+
}
71+
72+
const position = { x, y };
73+
74+
console.log('Setting position:', position, 'viewport:', { viewportWidth, viewportHeight }, 'selection rect:', rect);
75+
setSelectionPosition(position);
76+
setShowActions(true);
77+
78+
// Debug: Log the state changes
79+
console.log('showActions set to true, selectedText:', selectedString);
80+
} else {
81+
setSelectedText("");
82+
setShowActions(false);
83+
}
84+
};
85+
86+
const handleAddToNote = () => {
87+
if (!selectedText) return;
88+
89+
console.log('Adding to note:', selectedText);
90+
91+
try {
92+
const { currentNoteId, getCurrentNote, createNoteFromContent, addContentToNote } = useNoteStore.getState();
93+
94+
console.log('Current note ID:', currentNoteId);
95+
96+
if (currentNoteId && getCurrentNote()) {
97+
// Add to existing note
98+
console.log('Adding to existing note');
99+
addContentToNote(currentNoteId, selectedText, source);
100+
} else {
101+
// Create new note
102+
console.log('Creating new note');
103+
const newNote = createNoteFromContent(selectedText, source);
104+
console.log('Created note:', newNote);
105+
}
106+
107+
// Switch to notes tab to show the result
108+
onTabSwitch?.();
109+
110+
// Clear selection and hide actions
111+
window.getSelection()?.removeAllRanges();
112+
setSelectedText("");
113+
setShowActions(false);
114+
} catch (error) {
115+
console.error('Error adding to note:', error);
116+
}
117+
};
118+
119+
const handleCreateNewNote = () => {
120+
if (!selectedText) return;
121+
122+
const { createNoteFromContent } = useNoteStore.getState();
123+
createNoteFromContent(selectedText, source);
124+
125+
// Switch to notes tab to show the result
126+
onTabSwitch?.();
127+
128+
// Clear selection and hide actions
129+
window.getSelection()?.removeAllRanges();
130+
setSelectedText("");
131+
setShowActions(false);
132+
};
133+
134+
const handleClickOutside = (e: React.MouseEvent) => {
135+
if (!containerRef.current?.contains(e.target as Node)) {
136+
setShowActions(false);
137+
setSelectedText("");
138+
}
139+
};
140+
141+
return (
142+
<>
143+
<div
144+
ref={containerRef}
145+
className={cn("relative", className)}
146+
onMouseUp={handleTextSelection}
147+
onMouseDown={() => setShowActions(false)}
148+
>
149+
{children}
150+
</div>
151+
152+
{/* Floating Action Buttons */}
153+
{(() => {
154+
console.log('Render check - showActions:', showActions, 'selectedText:', selectedText);
155+
return showActions && selectedText;
156+
})() && (
157+
<>
158+
{/* Backdrop to handle clicks outside */}
159+
<div
160+
className="fixed inset-0 bg-black/10"
161+
style={{ zIndex: 999998 }}
162+
onClick={handleClickOutside}
163+
/>
164+
165+
{/* Action Buttons */}
166+
<div
167+
className="fixed flex items-center gap-2 rounded-lg shadow-2xl p-3"
168+
style={{
169+
left: `${selectionPosition.x}px`,
170+
top: `${selectionPosition.y}px`,
171+
transform: 'translateX(-50%)',
172+
backgroundColor: '#1e293b', // Dark background
173+
border: '2px solid #3b82f6', // Blue border
174+
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05)',
175+
zIndex: 999999, // Very high z-index to appear above panels
176+
minWidth: '220px',
177+
maxWidth: '300px',
178+
}}
179+
>
180+
<Button
181+
size="sm"
182+
onClick={handleAddToNote}
183+
title="Add to current note or create new note"
184+
style={{
185+
height: '32px',
186+
padding: '0 12px',
187+
fontSize: '12px',
188+
fontWeight: '500',
189+
backgroundColor: '#3b82f6',
190+
border: 'none',
191+
color: 'white',
192+
borderRadius: '6px',
193+
display: 'flex',
194+
alignItems: 'center',
195+
gap: '4px'
196+
}}
197+
>
198+
<FileText className="w-3 h-3" />
199+
Add to Note
200+
</Button>
201+
202+
<Button
203+
size="sm"
204+
onClick={handleCreateNewNote}
205+
title="Create new note with this text"
206+
style={{
207+
height: '32px',
208+
padding: '0 12px',
209+
fontSize: '12px',
210+
fontWeight: '500',
211+
backgroundColor: 'transparent',
212+
border: '1px solid #3b82f6',
213+
color: '#60a5fa',
214+
borderRadius: '6px',
215+
display: 'flex',
216+
alignItems: 'center',
217+
gap: '4px'
218+
}}
219+
>
220+
<Plus className="w-3 h-3" />
221+
New Note
222+
</Button>
223+
</div>
224+
</>
225+
)}
226+
</>
227+
);
228+
}

0 commit comments

Comments
 (0)