attached suggestions to chats

This commit is contained in:
2026-02-28 12:06:36 +10:00
parent 2dc3bcedcd
commit ff6e946529
7 changed files with 192 additions and 14 deletions

View File

@@ -475,3 +475,69 @@
.chat-send-btn:not(:disabled):hover {
opacity: 0.85;
}
/* ── Suggestion summary block inside assistant messages ─── */
.chat-message-suggestions {
margin-top: 6px;
border-top: 1px solid var(--border);
padding-top: 7px;
}
.chat-suggestions-label {
display: block;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 5px;
}
.chat-suggestion-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.chat-suggestion-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.chat-suggestion-badge {
display: inline-block;
border-radius: 3px;
padding: 1px 5px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #fff;
flex-shrink: 0;
}
.chat-suggestion-excerpt {
font-size: 11px;
color: var(--text-muted);
font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.chat-suggestion-applied {
display: inline-block;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #1a1208;
background: rgba(30, 200, 120, 0.85);
border-radius: 3px;
padding: 1px 5px;
flex-shrink: 0;
}

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import type { ChatMessage } from '../../types/editor'
import type { ChatMessage, TextAnnotation } from '../../types/editor'
marked.setOptions({ breaks: true })
@@ -11,11 +11,22 @@ function attachmentIcon(mimeType: string): string {
return '📝'
}
interface Props {
message: ChatMessage
function badgeColor(type: TextAnnotation['type']): string {
switch (type) {
case 'passive_voice': return 'rgba(255, 200, 0, 0.75)'
case 'consistency': return 'rgba(220, 80, 80, 0.75)'
case 'style': return 'rgba(80, 160, 255, 0.75)'
case 'critique': return 'rgba(160, 80, 220, 0.75)'
case 'custom': return 'rgba(30, 200, 150, 0.8)'
}
}
export function ChatMessageItem({ message }: Props): JSX.Element {
interface Props {
message: ChatMessage
linkedAnnotations?: TextAnnotation[]
}
export function ChatMessageItem({ message, linkedAnnotations }: Props): JSX.Element {
const html = useMemo(() => {
if (message.role !== 'assistant' || !message.content) return null
const raw = marked.parse(message.content) as string
@@ -47,6 +58,34 @@ export function ChatMessageItem({ message }: Props): JSX.Element {
message.content || <span className="chat-streaming-cursor"></span>
)}
</div>
{linkedAnnotations && linkedAnnotations.length > 0 && (
<div className="chat-message-suggestions">
<span className="chat-suggestions-label">
{linkedAnnotations.length} suggestion{linkedAnnotations.length !== 1 ? 's' : ''} created
</span>
<div className="chat-suggestion-list">
{linkedAnnotations.map(ann => (
<div key={ann.id} className="chat-suggestion-row">
<span
className="chat-suggestion-badge"
style={{ backgroundColor: badgeColor(ann.type) }}
>
{ann.type.replace(/_/g, ' ')}
</span>
<span className="chat-suggestion-excerpt">
"{ann.matchedText.length > 40
? ann.matchedText.slice(0, 38) + '…'
: ann.matchedText}"
</span>
{ann.applied && (
<span className="chat-suggestion-applied">Applied</span>
)}
</div>
))}
</div>
</div>
)}
</div>
)
}

View File

@@ -4,7 +4,7 @@ import { ChatMessageItem } from './ChatMessageItem'
import { ChatInput } from './ChatInput'
import { FeedbackPanel } from '../Feedback/FeedbackPanel'
import { parseAnnotationsFromAIResponse } from '../../utils/annotationParser'
import type { Attachment } from '../../types/editor'
import type { Attachment, TextAnnotation } from '../../types/editor'
import './Chat.css'
type TabId = 'chat' | 'feedback'
@@ -18,12 +18,14 @@ export function ChatPanel(): JSX.Element {
activeFilePath,
analysisMode,
annotations,
annotationsByFile,
addUserMessage,
startAssistantMessage,
appendToLastAssistantMessage,
setAILoading,
setAIError,
setAnnotations,
linkAnnotationsToMessage,
clearChat
} = useEditorStore()
@@ -78,6 +80,7 @@ export function ChatPanel(): JSX.Element {
const parsed = parseAnnotationsFromAIResponse(lastMsg.content, activeFileContent, overrideType)
if (parsed.length > 0) {
setAnnotations(parsed)
linkAnnotationsToMessage(lastMsg.id, parsed.map(a => a.id))
}
}
} catch (err) {
@@ -97,6 +100,8 @@ export function ChatPanel(): JSX.Element {
}, [chatHistory])
const hasFile = Boolean(activeFilePath)
const allAnnotationsForFile: TextAnnotation[] =
(activeFilePath ? annotationsByFile[activeFilePath]?.annotations : undefined) ?? []
return (
<div className="chat-panel">
@@ -148,9 +153,18 @@ export function ChatPanel(): JSX.Element {
Ask anything about the current chapter passive voice, plot, character, style...
</p>
)}
{chatHistory.map((msg) => (
<ChatMessageItem key={msg.id} message={msg} />
))}
{chatHistory.map((msg) => {
const linkedAnnotations = (msg.annotationIds ?? [])
.map(id => allAnnotationsForFile.find(a => a.id === id))
.filter((a): a is TextAnnotation => a !== undefined)
return (
<ChatMessageItem
key={msg.id}
message={msg}
linkedAnnotations={linkedAnnotations}
/>
)
})}
{isAILoading && chatHistory[chatHistory.length - 1]?.content === '' && (
<div className="chat-typing">
<span />

View File

@@ -155,11 +155,11 @@ export function scrollToAnnotation(ann: TextAnnotation): void {
})
}
// Apply a suggestion to the document and remove that annotation
// Apply a suggestion to the document and mark that annotation as applied
export function applyAnnotation(ann: TextAnnotation, suggestion: string): void {
const view = currentEditorView
if (!view) return
const { annotations: anns, setAnnotations } = useEditorStore.getState()
const { annotations: anns, markAnnotationApplied } = useEditorStore.getState()
const changeSpec = { from: ann.from, to: ann.to, insert: suggestion }
// Map surviving annotation positions through the text change so
// their from/to reflect the new document offsets.
@@ -173,7 +173,9 @@ export function applyAnnotation(ann: TextAnnotation, suggestion: string): void {
changes: changeSpec,
effects: setAnnotationsEffect.of(remaining)
})
setAnnotations(remaining)
// Mark as applied in annotationsByFile (preserves it for chat history) and
// remove from the active annotations list.
markAnnotationApplied(ann.id)
tooltipAnalysisCache.delete(ann.id)
}