Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 29 additions & 11 deletions packages/review-editor/components/AnnotationToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface AnnotationToolbarProps {
selectedOriginalCode?: string;
isEditing?: boolean;
setShowCodeModal: (show: boolean) => void;
setShowCommentModal: (show: boolean) => void;
onSubmit: () => void;
onDismiss: () => void;
onCancel: () => void;
Expand Down Expand Up @@ -54,6 +55,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
selectedOriginalCode,
isEditing = false,
setShowCodeModal,
setShowCommentModal,
onSubmit,
onDismiss,
onCancel,
Expand Down Expand Up @@ -128,7 +130,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
dragHandleProps={dragHandleProps}
/>
) : (
<div className="w-80">
<div className="w-80 max-w-[calc(100vw-2rem)] flex flex-col">
<div className="flex items-center justify-between mb-2" {...dragHandleProps}>
<span className="text-xs text-muted-foreground">
{isEditing
Expand All @@ -137,15 +139,25 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
? formatTokenContext(toolbarState.tokenSelection)
: formatLineRange(toolbarState.range.start, toolbarState.range.end)}
</span>
<button
onClick={onCancel}
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
title="Cancel"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="flex items-center gap-1">
<button
onClick={() => setShowCommentModal(true)}
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
title="Expand comment"
aria-label="Expand comment"
>
<ExpandIcon />
</button>
<button
onClick={onCancel}
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
title="Cancel"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>

{conventionalCommentsEnabled && (
Expand All @@ -162,7 +174,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="Leave feedback..."
className="w-full px-3 py-2 bg-muted rounded-lg text-xs resize-none focus:outline-none focus:ring-1 focus:ring-primary/50"
className="w-full min-h-[4.5rem] max-h-[calc(100vh-16rem)] px-3 py-2 bg-muted rounded-lg text-xs leading-6 resize-y border-0 focus:outline-none focus:ring-1 focus:ring-primary/50 placeholder:text-muted-foreground"
rows={3}
autoFocus
onKeyDown={(e) => {
Expand Down Expand Up @@ -272,3 +284,9 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({

return createPortal(content, document.body);
};

const ExpandIcon = () => (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
</svg>
);
149 changes: 149 additions & 0 deletions packages/review-editor/components/ExpandedCommentDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { SparklesIcon } from '@plannotator/ui/components/SparklesIcon';

interface ExpandedCommentDialogProps {
title: string;
commentText: string;
setCommentText: (text: string) => void;
isEditing: boolean;
canSubmit: boolean;
aiAvailable?: boolean;
onAskAI?: (question: string) => void;
onSubmit: () => void;
onCollapse: () => void;
onCancel: () => void;
}

export const ExpandedCommentDialog: React.FC<ExpandedCommentDialogProps> = ({
title,
commentText,
setCommentText,
isEditing,
canSubmit,
aiAvailable = false,
onAskAI,
onSubmit,
onCollapse,
onCancel,
}) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const askAIEnabled = aiAvailable && !!onAskAI && commentText.trim().length > 0;
const submitLabel = isEditing ? 'Update' : 'Add Comment';

useEffect(() => {
const id = window.setTimeout(() => {
const textarea = textareaRef.current;
if (!textarea) return;
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
}, 0);

return () => window.clearTimeout(id);
}, []);

const handleAskAI = () => {
if (!askAIEnabled) return;
onAskAI?.(commentText.trim());
};

return createPortal(
<div className="fixed inset-0 z-[2000] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm" onClick={onCollapse} />
<div className="relative w-full max-w-2xl max-h-[85vh] bg-popover border border-border rounded-xl shadow-2xl flex flex-col">
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50">
<span className="text-xs text-muted-foreground truncate">{title}</span>
<div className="flex items-center gap-1">
<button
type="button"
onClick={onCollapse}
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
title="Collapse"
aria-label="Collapse expanded comment"
>
<CollapseIcon />
</button>
<button
type="button"
onClick={onCancel}
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
title="Close"
aria-label="Close expanded comment"
>
<CloseIcon />
</button>
</div>
</div>

<div className="px-4 py-3 min-h-0 flex-1">
<textarea
ref={textareaRef}
value={commentText}
onChange={(event) => setCommentText(event.target.value)}
placeholder="Leave feedback..."
className="w-full min-h-72 max-h-[56vh] bg-muted text-sm leading-relaxed placeholder:text-muted-foreground resize-y focus:outline-none rounded-lg border-0 px-3 py-2"
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.stopPropagation();
onCollapse();
return;
}

if (event.key === 'Enter' && (event.metaKey || event.ctrlKey) && !event.nativeEvent.isComposing) {
event.preventDefault();
onSubmit();
}
}}
/>
</div>

<div className="flex items-center justify-between gap-3 px-4 py-3 border-t border-border/50">
<div>
{aiAvailable && (
<button
type="button"
onClick={handleAskAI}
disabled={!askAIEnabled}
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title={askAIEnabled ? 'Ask AI this question' : 'Type a question to ask AI'}
>
<SparklesIcon className="w-3 h-3" />
Ask AI
</button>
)}
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onCollapse}
className="review-toolbar-btn"
>
Collapse
</button>
<button
type="button"
onClick={onSubmit}
disabled={!canSubmit}
className="review-toolbar-btn primary disabled:opacity-50 disabled:cursor-not-allowed"
>
{submitLabel}
</button>
</div>
</div>
</div>
</div>,
document.body,
);
};

const CollapseIcon = () => (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />
</svg>
);

const CloseIcon = () => (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
);
35 changes: 34 additions & 1 deletion packages/review-editor/components/ToolbarHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { useConfigValue } from '@plannotator/ui/config';
import { useAnnotationToolbar } from '../hooks/useAnnotationToolbar';
import { AnnotationToolbar } from './AnnotationToolbar';
import { SuggestionModal } from './SuggestionModal';
import { ExpandedCommentDialog } from './ExpandedCommentDialog';
import { getEnabledLabels } from './ConventionalLabelPicker';
import type { AIChatEntry } from '../hooks/useAIChat';
import { formatLineRange, formatTokenContext } from '../utils/formatLineRange';

export interface ToolbarHostHandle {
handleLineSelectionEnd: (range: SelectedLineRange | null) => void;
Expand Down Expand Up @@ -104,10 +106,25 @@ export const ToolbarHost = forwardRef<ToolbarHostHandle, ToolbarHostProps>(funct
);

const handleCloseCodeModal = useCallback(() => toolbar.setShowCodeModal(false), [toolbar.setShowCodeModal]);
const handleCollapseCommentModal = useCallback(() => toolbar.setShowCommentModal(false), [toolbar.setShowCommentModal]);
const handleCancelCommentModal = useCallback(() => {
toolbar.setShowCommentModal(false);
toolbar.handleCancel();
}, [toolbar.handleCancel, toolbar.setShowCommentModal]);

const expandedCommentTitle = useMemo(() => {
const toolbarState = toolbar.toolbarState;
if (!toolbarState) return 'Comment';
if (toolbar.editingAnnotationId) return 'Edit annotation';
if (toolbarState.tokenSelection) return formatTokenContext(toolbarState.tokenSelection);
return formatLineRange(toolbarState.range.start, toolbarState.range.end);
}, [toolbar.editingAnnotationId, toolbar.toolbarState]);

const canSubmitAnnotation = toolbar.commentText.trim().length > 0 || toolbar.suggestedCode.trim().length > 0;

return (
<>
{toolbar.toolbarState && !toolbar.showCodeModal && (
{toolbar.toolbarState && !toolbar.showCodeModal && !toolbar.showCommentModal && (
<AnnotationToolbar
toolbarState={toolbar.toolbarState}
toolbarRef={toolbar.toolbarRef}
Expand All @@ -119,6 +136,7 @@ export const ToolbarHost = forwardRef<ToolbarHostHandle, ToolbarHostProps>(funct
setShowSuggestedCode={toolbar.setShowSuggestedCode}
selectedOriginalCode={toolbar.selectedOriginalCode}
setShowCodeModal={toolbar.setShowCodeModal}
setShowCommentModal={toolbar.setShowCommentModal}
isEditing={!!toolbar.editingAnnotationId}
onSubmit={toolbar.handleSubmitAnnotation}
onDismiss={toolbar.handleDismiss}
Expand All @@ -137,6 +155,21 @@ export const ToolbarHost = forwardRef<ToolbarHostHandle, ToolbarHostProps>(funct
/>
)}

{toolbar.toolbarState && toolbar.showCommentModal && (
<ExpandedCommentDialog
title={expandedCommentTitle}
commentText={toolbar.commentText}
setCommentText={toolbar.setCommentText}
isEditing={!!toolbar.editingAnnotationId}
canSubmit={canSubmitAnnotation}
aiAvailable={aiAvailable && !toolbar.editingAnnotationId}
onAskAI={onAskAI}
onSubmit={toolbar.handleSubmitAnnotation}
onCollapse={handleCollapseCommentModal}
onCancel={handleCancelCommentModal}
/>
)}

{toolbar.showCodeModal && (
<SuggestionModal
filePath={filePath}
Expand Down
10 changes: 9 additions & 1 deletion packages/review-editor/hooks/useAnnotationToolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function useAnnotationToolbar({ patch, filePath, isFocused, onLineSelecti
const [showSuggestedCode, setShowSuggestedCode] = useState(false);
const [selectedOriginalCode, setSelectedOriginalCode] = useState('');
const [showCodeModal, setShowCodeModal] = useState(false);
const [showCommentModal, setShowCommentModal] = useState(false);
const [modalLayout, setModalLayout] = useState<'horizontal' | 'vertical'>('horizontal');
const [editingAnnotationId, setEditingAnnotationId] = useState<string | null>(null);
const [conventionalLabel, setConventionalLabel] = useState<ConventionalLabel | null>(null);
Expand Down Expand Up @@ -128,6 +129,7 @@ export function useAnnotationToolbar({ patch, filePath, isFocused, onLineSelecti
setSelectedOriginalCode('');
setShowSuggestedCode(false);
setShowCodeModal(false);
setShowCommentModal(false);
setEditingAnnotationId(null);
setConventionalLabel(null);
setDecorations([]);
Expand All @@ -148,6 +150,8 @@ export function useAnnotationToolbar({ patch, filePath, isFocused, onLineSelecti
) => {
saveDraft();
setEditingAnnotationId(null);
setShowCodeModal(false);
setShowCommentModal(false);

const draft = draftStore.get(draftKey(filePath, range));
if (draft) {
Expand Down Expand Up @@ -233,6 +237,7 @@ export function useAnnotationToolbar({ patch, filePath, isFocused, onLineSelecti
setSelectedOriginalCode(annotation.originalCode || '');
setShowSuggestedCode(!!annotation.suggestedCode);
setShowCodeModal(false);
setShowCommentModal(false);
setConventionalLabel(annotation.conventionalLabel || null);
setDecorations(annotation.decorations || []);

Expand Down Expand Up @@ -263,7 +268,7 @@ export function useAnnotationToolbar({ patch, filePath, isFocused, onLineSelecti
}, [onLineSelection, clearDraft, resetForm]);

useDismissOnOutsideAndEscape({
enabled: !!toolbarState && !showCodeModal,
enabled: !!toolbarState && !showCodeModal && !showCommentModal,
ref: toolbarRef,
onDismiss: handleDismiss,
});
Expand Down Expand Up @@ -292,6 +297,7 @@ export function useAnnotationToolbar({ patch, filePath, isFocused, onLineSelecti
setDecorations(draft.decorations);
setEditingAnnotationId(null);
setShowCodeModal(false);
setShowCommentModal(false);
setToolbarState({
position: draft.position,
range: draft.range,
Expand Down Expand Up @@ -349,6 +355,8 @@ export function useAnnotationToolbar({ patch, filePath, isFocused, onLineSelecti
selectedOriginalCode,
showCodeModal,
setShowCodeModal,
showCommentModal,
setShowCommentModal,
modalLayout,
setModalLayout,
editingAnnotationId,
Expand Down