Skip to content

Commit da8ac1e

Browse files
committed
feat: add personal notes thread type without AI involvement
1 parent c81acc0 commit da8ac1e

12 files changed

Lines changed: 271 additions & 63 deletions

File tree

App.tsx

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import {
5555
extractTitle,
5656
} from './services/sessionHistory'
5757
import { generateSessionSummary } from './services/aiService'
58-
import { Thread, ViewState, SourceMetadata, SessionMeta, Message } from './types'
58+
import { Thread, ViewState, SourceMetadata, SessionMeta, Message, ThreadType } from './types'
5959

6060
const SESSION_ID_PATTERN = /^\/([a-zA-Z0-9_-]{10,})$/
6161

@@ -163,8 +163,9 @@ const App: React.FC = () => {
163163
const el = wrapFirstOccurrenceWithThreadAnchor(root, thread.context, thread.id)
164164
if (!el) continue
165165

166-
el.title = `Open thread: ${thread.snippet}`
167-
el.setAttribute('aria-label', `Open thread: ${thread.snippet}`)
166+
const labelPrefix = thread.type === 'comment' ? 'Open note' : 'Open thread'
167+
el.title = `${labelPrefix}: ${thread.snippet}`
168+
el.setAttribute('aria-label', `${labelPrefix}: ${thread.snippet}`)
168169
setThreadAnchorActive(el, thread.id === threadManager.activeThreadId)
169170
threadAnchorElsRef.current.set(thread.id, el)
170171
}
@@ -187,6 +188,7 @@ const App: React.FC = () => {
187188
if (sessionId && session.session && !session.isLoading) {
188189
const apiThreads: Thread[] = session.session.threads.map(t => ({
189190
id: t.id,
191+
type: (t.type as ThreadType) || 'discussion', // Default to discussion for backward compat
190192
context: t.context,
191193
snippet: t.snippet,
192194
createdAt: t.createdAt,
@@ -334,20 +336,41 @@ const App: React.FC = () => {
334336
const handleExport = () => {
335337
let exportText = markdownContent
336338

339+
// Separate threads by type
340+
const comments = threadManager.threads.filter(t => t.type === 'comment')
341+
const discussions = threadManager.threads.filter(t => t.type !== 'comment')
342+
337343
if (quotes.length > 0) {
338344
exportText += '\n\n---\n\n# Saved Quotes\n\n'
339345
quotes.forEach(q => {
340346
exportText += `> "${q.text}"\n\n`
341347
})
342348
}
343349

344-
if (threadManager.threads.length > 0) {
345-
exportText += '\n\n---\n\n# Discussions\n\n'
346-
threadManager.threads.forEach(t => {
350+
// Export personal comments
351+
if (comments.length > 0) {
352+
exportText += '\n\n---\n\n# Personal Notes\n\n'
353+
exportText += '_Notes and commentary on the article_\n\n'
354+
comments.forEach(c => {
355+
exportText += `## ${c.snippet}\n\n`
356+
if (c.context !== 'Entire Document') {
357+
exportText += `> **Context**: ${c.context}\n\n`
358+
}
359+
c.messages.forEach(m => {
360+
exportText += `${m.text}\n\n`
361+
})
362+
exportText += '---\n\n'
363+
})
364+
}
365+
366+
// Export AI discussions
367+
if (discussions.length > 0) {
368+
exportText += '\n\n---\n\n# AI Discussions\n\n'
369+
discussions.forEach(t => {
347370
exportText += `## Thread: ${t.snippet}\n`
348371
exportText += `> **Context**: ${t.context}\n\n`
349372
t.messages.forEach(m => {
350-
exportText += `**${m.role === 'user' ? 'User' : 'AI'}**: ${m.text}\n\n`
373+
exportText += `**${m.role === 'user' ? 'You' : 'AI'}**: ${m.text}\n\n`
351374
})
352375
exportText += '---\n\n'
353376
})
@@ -490,24 +513,31 @@ const App: React.FC = () => {
490513
})
491514
}
492515

493-
const createThread = async (action: 'discuss' | 'summarize') => {
516+
const createThread = async (action: 'discuss' | 'summarize' | 'comment') => {
494517
if (!selection) return
495518

496519
const newThreadId = Date.now().toString()
497520
const snippet =
498521
selection.text.length > 30 ? selection.text.substring(0, 30) + '...' : selection.text
499522

523+
// Determine thread type based on action
524+
const threadType: ThreadType = action === 'comment' ? 'comment' : 'discussion'
525+
500526
const anchorEl = contentRef.current
501527
? wrapCurrentSelectionWithThreadAnchor(contentRef.current, newThreadId)
502528
: null
503529
if (anchorEl) {
504-
anchorEl.title = `Open thread: ${snippet}`
505-
anchorEl.setAttribute('aria-label', `Open thread: ${snippet}`)
530+
anchorEl.title = action === 'comment' ? `Open note: ${snippet}` : `Open thread: ${snippet}`
531+
anchorEl.setAttribute(
532+
'aria-label',
533+
action === 'comment' ? `Open note: ${snippet}` : `Open thread: ${snippet}`
534+
)
506535
threadAnchorElsRef.current.set(newThreadId, anchorEl)
507536
}
508537

509538
const newThread: Thread = {
510539
id: newThreadId,
540+
type: threadType,
511541
context: selection.text,
512542
messages: [],
513543
createdAt: Date.now(),
@@ -522,7 +552,7 @@ const App: React.FC = () => {
522552
// Save to API
523553
let apiThreadId = newThreadId
524554
if (sessionId) {
525-
const savedThreadId = await session.addThread(selection.text, snippet)
555+
const savedThreadId = await session.addThread(selection.text, snippet, threadType)
526556
if (savedThreadId && savedThreadId !== newThreadId) {
527557
threadManager.updateThreadId(newThreadId, savedThreadId)
528558
apiThreadId = savedThreadId
@@ -536,6 +566,11 @@ const App: React.FC = () => {
536566
}
537567
}
538568

569+
// For comments, we're done - no AI involvement
570+
if (action === 'comment') {
571+
return
572+
}
573+
539574
if (action === 'discuss') {
540575
return
541576
}
@@ -602,6 +637,7 @@ const App: React.FC = () => {
602637
}
603638
const newThread: Thread = {
604639
id: newThreadId,
640+
type: 'discussion', // General threads are always AI discussions
605641
context: 'Entire Document',
606642
messages: [initialUserMsg],
607643
createdAt: Date.now(),
@@ -642,6 +678,7 @@ const App: React.FC = () => {
642678
const handleSendMessage = async (text: string) => {
643679
if (!threadManager.activeThreadId || !threadManager.activeThread) return
644680

681+
const thread = threadManager.activeThread
645682
const userMessageId = generateId()
646683
const userMessage: Message = {
647684
id: userMessageId,
@@ -660,11 +697,16 @@ const App: React.FC = () => {
660697
}
661698
}
662699

700+
// Skip AI for comment threads - they're personal notes without AI involvement
701+
if (thread.type === 'comment') {
702+
return
703+
}
704+
663705
await sendRequest({
664706
threadId: threadManager.activeThreadId,
665-
context: threadManager.activeThread.context,
707+
context: thread.context,
666708
markdownContent,
667-
messages: [...threadManager.activeThread.messages, userMessage],
709+
messages: [...thread.messages, userMessage],
668710
userMessage: text,
669711
mode: 'discuss',
670712
})
@@ -686,14 +728,20 @@ const App: React.FC = () => {
686728
]
687729

688730
threadManager.updateMessageToThread(threadId, messageId, newText)
689-
threadManager.truncateThreadAfter(threadId, messageId)
731+
const shouldTruncate = thread.type !== 'comment'
732+
if (shouldTruncate) {
733+
threadManager.truncateThreadAfter(threadId, messageId)
734+
}
690735

691736
if (sessionId && session.isOwner) {
692737
await session.updateMessage(threadId, messageId, newText)
693-
await session.truncateThread(threadId, messageId)
738+
if (shouldTruncate) {
739+
await session.truncateThread(threadId, messageId)
740+
}
694741
}
695742

696-
if (isUserMessage) {
743+
// Only call AI for discussion threads, not comments
744+
if (isUserMessage && thread.type !== 'comment') {
697745
await sendRequest({
698746
threadId,
699747
context: thread.context,
@@ -720,6 +768,10 @@ const App: React.FC = () => {
720768
if (!threadManager.activeThreadId || !threadManager.activeThread) return
721769

722770
const currentThread = threadManager.activeThread
771+
772+
// No retry for comment threads - they have no AI responses
773+
if (currentThread.type === 'comment') return
774+
723775
if (currentThread.messages.length < 2) return
724776

725777
const messages = currentThread.messages

components/ThreadList.tsx

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import { X, MessageSquare, Clock, ArrowRight, FileText } from 'lucide-react'
2+
import { X, MessageSquare, Clock, ArrowRight, FileText, StickyNote } from 'lucide-react'
33
import { Thread } from '../types'
44

55
interface ThreadListProps {
@@ -58,19 +58,44 @@ const ThreadList: React.FC<ThreadListProps> = ({ threads, onSelectThread, onClos
5858
thread.messages.length > 0 ? thread.messages[thread.messages.length - 1] : null
5959

6060
const isGeneral = thread.context === 'Entire Document'
61+
const isComment = thread.type === 'comment'
62+
63+
// Determine icon and colors based on thread type
64+
const getIconStyles = () => {
65+
if (isComment) {
66+
return {
67+
className: 'bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400',
68+
icon: <StickyNote size={14} />,
69+
}
70+
}
71+
if (isGeneral) {
72+
return {
73+
className: 'bg-indigo-100 text-indigo-600 dark:bg-accent-glow dark:text-accent',
74+
icon: <FileText size={14} />,
75+
}
76+
}
77+
return {
78+
className: 'bg-cyan-100 text-cyan-600 dark:bg-accent-glow dark:text-accent',
79+
icon: <MessageSquare size={14} />,
80+
}
81+
}
82+
83+
const iconStyles = getIconStyles()
6184

6285
return (
6386
<button
6487
key={thread.id}
6588
onClick={() => onSelectThread(thread.id)}
66-
className="w-full text-left bg-white dark:bg-dark-surface p-4 rounded-xl border border-slate-200 dark:border-dark-border shadow-sm hover:shadow-md hover:border-cyan-300 dark:hover:border-accent transition-all group"
89+
className={`w-full text-left bg-white dark:bg-dark-surface p-4 rounded-xl border shadow-sm hover:shadow-md transition-all group ${
90+
isComment
91+
? 'border-amber-200 dark:border-amber-800/50 hover:border-amber-300 dark:hover:border-amber-700'
92+
: 'border-slate-200 dark:border-dark-border hover:border-cyan-300 dark:hover:border-accent'
93+
}`}
6794
>
6895
<div className="flex items-start justify-between mb-2">
6996
<div className="flex items-center gap-2">
70-
<span
71-
className={`p-1.5 rounded-md ${isGeneral ? 'bg-indigo-100 text-indigo-600 dark:bg-accent-glow dark:text-accent' : 'bg-cyan-100 text-cyan-600 dark:bg-accent-glow dark:text-accent'}`}
72-
>
73-
{isGeneral ? <FileText size={14} /> : <MessageSquare size={14} />}
97+
<span className={`p-1.5 rounded-md ${iconStyles.className}`}>
98+
{iconStyles.icon}
7499
</span>
75100
<span className="font-semibold text-slate-700 dark:text-zinc-200 text-sm truncate max-w-[180px]">
76101
{thread.snippet}
@@ -91,16 +116,25 @@ const ThreadList: React.FC<ThreadListProps> = ({ threads, onSelectThread, onClos
91116
: 'text-slate-800 dark:text-zinc-300'
92117
}
93118
>
94-
{lastMessage.role === 'user' ? 'You: ' : 'AI: '}
119+
{isComment ? '' : lastMessage.role === 'user' ? 'You: ' : 'AI: '}
95120
{lastMessage.text}
96121
</span>
97122
) : (
98-
<span className="text-slate-400 italic">No messages yet...</span>
123+
<span className="text-slate-400 italic">
124+
{isComment ? 'No notes yet...' : 'No messages yet...'}
125+
</span>
99126
)}
100127
</div>
101128

102-
<div className="flex items-center text-cyan-600 dark:text-accent text-xs font-medium opacity-0 group-hover:opacity-100 transition-opacity">
103-
Continue discussion <ArrowRight size={12} className="ml-1" />
129+
<div
130+
className={`flex items-center text-xs font-medium opacity-0 group-hover:opacity-100 transition-opacity ${
131+
isComment
132+
? 'text-amber-600 dark:text-amber-400'
133+
: 'text-cyan-600 dark:text-accent'
134+
}`}
135+
>
136+
{isComment ? 'Continue note' : 'Continue discussion'}{' '}
137+
<ArrowRight size={12} className="ml-1" />
104138
</div>
105139
</button>
106140
)

components/ThreadPanel.tsx

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import {
44
Check,
55
ChevronLeft,
66
Copy,
7+
MessageSquare,
78
Pencil,
89
RefreshCw,
910
Send,
1011
Settings,
1112
Sparkles,
13+
StickyNote,
1214
Trash2,
1315
User,
1416
X,
@@ -250,6 +252,7 @@ const ThreadPanel: React.FC<ThreadPanelProps> = ({
250252
}
251253

252254
const isGeneralThread = thread.context === 'Entire Document'
255+
const isComment = thread.type === 'comment'
253256
const lastMessage = thread.messages[thread.messages.length - 1]
254257
const showTypingIndicator =
255258
isLoading && (!lastMessage || lastMessage.role !== 'assistant' || !lastMessage.text)
@@ -266,13 +269,28 @@ const ThreadPanel: React.FC<ThreadPanelProps> = ({
266269
>
267270
<ChevronLeft size={20} className="group-hover:-translate-x-0.5 transition-transform" />
268271
</button>
269-
<div>
270-
<h3 className="font-semibold text-slate-800 dark:text-zinc-100 leading-tight">
271-
Thread
272-
</h3>
273-
<p className="text-xs text-slate-500 dark:text-zinc-400 truncate max-w-[200px]">
274-
{isGeneralThread ? 'General Discussion' : `Re: ${thread.snippet}`}
275-
</p>
272+
<div className="flex items-center gap-2">
273+
{isComment ? (
274+
<span className="p-1.5 rounded-md bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400">
275+
<StickyNote size={14} />
276+
</span>
277+
) : (
278+
<span className="p-1.5 rounded-md bg-cyan-100 dark:bg-accent-glow text-cyan-600 dark:text-accent">
279+
<MessageSquare size={14} />
280+
</span>
281+
)}
282+
<div>
283+
<h3 className="font-semibold text-slate-800 dark:text-zinc-100 leading-tight">
284+
{isComment ? 'Personal Note' : 'Thread'}
285+
</h3>
286+
<p className="text-xs text-slate-500 dark:text-zinc-400 truncate max-w-[200px]">
287+
{isGeneralThread
288+
? isComment
289+
? 'General Note'
290+
: 'General Discussion'
291+
: `Re: ${thread.snippet}`}
292+
</p>
293+
</div>
276294
</div>
277295
</div>
278296
<div className="flex items-center gap-1">
@@ -472,15 +490,27 @@ const ThreadPanel: React.FC<ThreadPanelProps> = ({
472490

473491
{/* Input Area */}
474492
<div className="p-4 border-t border-slate-100 dark:border-dark-border bg-white dark:bg-dark-surface">
475-
<ProviderModelSelector settings={settings} onSettingsChange={onSettingsChange} />
493+
{!isComment && (
494+
<ProviderModelSelector settings={settings} onSettingsChange={onSettingsChange} />
495+
)}
476496
<form onSubmit={handleSubmit} className="relative">
477497
<input
478498
ref={inputRef}
479499
type="text"
480500
value={inputValue}
481501
onChange={e => setInputValue(e.target.value)}
482-
placeholder={isGeneralThread ? 'Ask about the document...' : 'Ask a follow-up...'}
483-
className="w-full pl-4 pr-12 py-3 bg-slate-50 dark:bg-dark-elevated border border-slate-200 dark:border-dark-border text-slate-900 dark:text-zinc-100 placeholder:dark:text-zinc-500 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent transition-all text-sm"
502+
placeholder={
503+
isComment
504+
? 'Add your thoughts...'
505+
: isGeneralThread
506+
? 'Ask about the document...'
507+
: 'Ask a follow-up...'
508+
}
509+
className={`w-full pl-4 pr-12 py-3 bg-slate-50 dark:bg-dark-elevated border text-slate-900 dark:text-zinc-100 placeholder:dark:text-zinc-500 rounded-xl focus:outline-none focus:ring-2 transition-all text-sm ${
510+
isComment
511+
? 'border-amber-200 dark:border-amber-800/50 focus:ring-amber-400/20 focus:border-amber-400'
512+
: 'border-slate-200 dark:border-dark-border focus:ring-accent/20 focus:border-accent'
513+
}`}
484514
/>
485515
<button
486516
type="submit"

0 commit comments

Comments
 (0)