mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(web): queue preview comments while busy and pin deck markers
While a chat run is in flight, preview comments now queue instead of showing a stuck Sending state. Deck slide changes no longer collapse comment markers to 0,0 by filtering hidden targets and persisting slideIndex. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cfde84b038
commit
51aa968d75
10 changed files with 297 additions and 43 deletions
|
|
@ -265,6 +265,9 @@ function migrate(db: SqliteDb): void {
|
||||||
if (!previewCommentCols.some((c: DbRow) => c.name === 'style_json')) {
|
if (!previewCommentCols.some((c: DbRow) => c.name === 'style_json')) {
|
||||||
db.exec(`ALTER TABLE preview_comments ADD COLUMN style_json TEXT`);
|
db.exec(`ALTER TABLE preview_comments ADD COLUMN style_json TEXT`);
|
||||||
}
|
}
|
||||||
|
if (!previewCommentCols.some((c: DbRow) => c.name === 'slide_index')) {
|
||||||
|
db.exec(`ALTER TABLE preview_comments ADD COLUMN slide_index INTEGER`);
|
||||||
|
}
|
||||||
const deploymentCols = db.prepare(`PRAGMA table_info(deployments)`).all() as DbRow[];
|
const deploymentCols = db.prepare(`PRAGMA table_info(deployments)`).all() as DbRow[];
|
||||||
if (!deploymentCols.some((c: DbRow) => c.name === 'status')) {
|
if (!deploymentCols.some((c: DbRow) => c.name === 'status')) {
|
||||||
db.exec(`ALTER TABLE deployments ADD COLUMN status TEXT NOT NULL DEFAULT 'ready'`);
|
db.exec(`ALTER TABLE deployments ADD COLUMN status TEXT NOT NULL DEFAULT 'ready'`);
|
||||||
|
|
@ -1072,6 +1075,7 @@ export function listPreviewComments(db: SqliteDb, projectId: string, conversatio
|
||||||
text, position_json AS positionJson, html_hint AS htmlHint,
|
text, position_json AS positionJson, html_hint AS htmlHint,
|
||||||
selection_kind AS selectionKind, member_count AS memberCount,
|
selection_kind AS selectionKind, member_count AS memberCount,
|
||||||
pod_members_json AS podMembersJson, style_json AS styleJson,
|
pod_members_json AS podMembersJson, style_json AS styleJson,
|
||||||
|
slide_index AS slideIndex,
|
||||||
note, status, created_at AS createdAt, updated_at AS updatedAt
|
note, status, created_at AS createdAt, updated_at AS updatedAt
|
||||||
FROM preview_comments
|
FROM preview_comments
|
||||||
WHERE project_id = ? AND conversation_id = ?
|
WHERE project_id = ? AND conversation_id = ?
|
||||||
|
|
@ -1102,6 +1106,7 @@ export function upsertPreviewComment(db: SqliteDb, projectId: string, conversati
|
||||||
? Math.max(0, Math.round(target.memberCount))
|
? Math.max(0, Math.round(target.memberCount))
|
||||||
: 0)
|
: 0)
|
||||||
: 0;
|
: 0;
|
||||||
|
const slideIndex = Number.isFinite(target.slideIndex) ? Math.max(0, Math.round(target.slideIndex)) : null;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const existing = db
|
const existing = db
|
||||||
.prepare(
|
.prepare(
|
||||||
|
|
@ -1116,8 +1121,8 @@ export function upsertPreviewComment(db: SqliteDb, projectId: string, conversati
|
||||||
`INSERT INTO preview_comments
|
`INSERT INTO preview_comments
|
||||||
(id, project_id, conversation_id, file_path, element_id, selector, label,
|
(id, project_id, conversation_id, file_path, element_id, selector, label,
|
||||||
text, position_json, html_hint, selection_kind, member_count, pod_members_json,
|
text, position_json, html_hint, selection_kind, member_count, pod_members_json,
|
||||||
style_json, note, status, created_at, updated_at)
|
style_json, slide_index, note, status, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(project_id, conversation_id, file_path, element_id) DO UPDATE SET
|
ON CONFLICT(project_id, conversation_id, file_path, element_id) DO UPDATE SET
|
||||||
selector = excluded.selector,
|
selector = excluded.selector,
|
||||||
label = excluded.label,
|
label = excluded.label,
|
||||||
|
|
@ -1128,6 +1133,7 @@ export function upsertPreviewComment(db: SqliteDb, projectId: string, conversati
|
||||||
member_count = excluded.member_count,
|
member_count = excluded.member_count,
|
||||||
pod_members_json = excluded.pod_members_json,
|
pod_members_json = excluded.pod_members_json,
|
||||||
style_json = excluded.style_json,
|
style_json = excluded.style_json,
|
||||||
|
slide_index = excluded.slide_index,
|
||||||
note = excluded.note,
|
note = excluded.note,
|
||||||
status = 'open',
|
status = 'open',
|
||||||
updated_at = excluded.updated_at`,
|
updated_at = excluded.updated_at`,
|
||||||
|
|
@ -1146,6 +1152,7 @@ export function upsertPreviewComment(db: SqliteDb, projectId: string, conversati
|
||||||
selectionKind === 'pod' ? memberCount : null,
|
selectionKind === 'pod' ? memberCount : null,
|
||||||
selectionKind === 'pod' ? JSON.stringify(podMembers) : null,
|
selectionKind === 'pod' ? JSON.stringify(podMembers) : null,
|
||||||
style ? JSON.stringify(style) : null,
|
style ? JSON.stringify(style) : null,
|
||||||
|
slideIndex,
|
||||||
note,
|
note,
|
||||||
'open',
|
'open',
|
||||||
createdAt,
|
createdAt,
|
||||||
|
|
@ -1183,6 +1190,7 @@ function getPreviewComment(db: SqliteDb, projectId: string, conversationId: stri
|
||||||
text, position_json AS positionJson, html_hint AS htmlHint,
|
text, position_json AS positionJson, html_hint AS htmlHint,
|
||||||
selection_kind AS selectionKind, member_count AS memberCount,
|
selection_kind AS selectionKind, member_count AS memberCount,
|
||||||
pod_members_json AS podMembersJson, style_json AS styleJson,
|
pod_members_json AS podMembersJson, style_json AS styleJson,
|
||||||
|
slide_index AS slideIndex,
|
||||||
note, status, created_at AS createdAt, updated_at AS updatedAt
|
note, status, created_at AS createdAt, updated_at AS updatedAt
|
||||||
FROM preview_comments
|
FROM preview_comments
|
||||||
WHERE id = ? AND project_id = ? AND conversation_id = ?`,
|
WHERE id = ? AND project_id = ? AND conversation_id = ?`,
|
||||||
|
|
@ -1214,6 +1222,7 @@ function normalizePreviewComment(row: DbRow) {
|
||||||
? row.memberCount
|
? row.memberCount
|
||||||
: undefined,
|
: undefined,
|
||||||
podMembers: normalizedPodMembers,
|
podMembers: normalizedPodMembers,
|
||||||
|
slideIndex: Number.isFinite(row.slideIndex) ? row.slideIndex : undefined,
|
||||||
note: row.note,
|
note: row.note,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export interface PreviewCommentSnapshot {
|
||||||
selectionKind?: PreviewCommentSelectionKind;
|
selectionKind?: PreviewCommentSelectionKind;
|
||||||
memberCount?: number;
|
memberCount?: number;
|
||||||
podMembers?: PreviewCommentMember[];
|
podMembers?: PreviewCommentMember[];
|
||||||
|
slideIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommentOverlayBounds {
|
export interface CommentOverlayBounds {
|
||||||
|
|
@ -95,9 +96,34 @@ export function targetFromSnapshot(snapshot: PreviewCommentSnapshot): PreviewCom
|
||||||
: 0)
|
: 0)
|
||||||
: undefined,
|
: undefined,
|
||||||
podMembers: podMembers.length > 0 ? podMembers : undefined,
|
podMembers: podMembers.length > 0 ? podMembers : undefined,
|
||||||
|
...(snapshot.slideIndex === undefined ? {} : { slideIndex: snapshot.slideIndex }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidCommentOverlayPosition(
|
||||||
|
position: { x: number; y: number; width: number; height: number } | undefined | null,
|
||||||
|
): boolean {
|
||||||
|
if (!position) return false;
|
||||||
|
const normalized = normalizePosition(position);
|
||||||
|
return (
|
||||||
|
Number.isFinite(normalized.x)
|
||||||
|
&& Number.isFinite(normalized.y)
|
||||||
|
&& Number.isFinite(normalized.width)
|
||||||
|
&& Number.isFinite(normalized.height)
|
||||||
|
&& normalized.width > 0
|
||||||
|
&& normalized.height > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commentVisibleOnDeckSlide(
|
||||||
|
comment: Pick<PreviewComment, 'slideIndex'>,
|
||||||
|
activeSlideIndex: number | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
if (activeSlideIndex == null) return true;
|
||||||
|
if (typeof comment.slideIndex !== 'number') return true;
|
||||||
|
return comment.slideIndex === activeSlideIndex;
|
||||||
|
}
|
||||||
|
|
||||||
export function overlayBoundsFromSnapshot(
|
export function overlayBoundsFromSnapshot(
|
||||||
snapshot: PreviewCommentSnapshot,
|
snapshot: PreviewCommentSnapshot,
|
||||||
scale: number,
|
scale: number,
|
||||||
|
|
@ -118,8 +144,11 @@ export function liveSnapshotForComment(
|
||||||
snapshots: Map<string, PreviewCommentSnapshot>,
|
snapshots: Map<string, PreviewCommentSnapshot>,
|
||||||
): PreviewCommentSnapshot | null {
|
): PreviewCommentSnapshot | null {
|
||||||
const snapshot = snapshots.get(comment.elementId);
|
const snapshot = snapshots.get(comment.elementId);
|
||||||
if (snapshot && snapshot.filePath === comment.filePath) return snapshot;
|
if (snapshot && snapshot.filePath === comment.filePath && isValidCommentOverlayPosition(snapshot.position)) {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
if (!comment.elementId.startsWith('pin-')) return null;
|
if (!comment.elementId.startsWith('pin-')) return null;
|
||||||
|
if (!isValidCommentOverlayPosition(comment.position)) return null;
|
||||||
return {
|
return {
|
||||||
filePath: comment.filePath,
|
filePath: comment.filePath,
|
||||||
elementId: comment.elementId,
|
elementId: comment.elementId,
|
||||||
|
|
@ -132,6 +161,7 @@ export function liveSnapshotForComment(
|
||||||
selectionKind: comment.selectionKind === 'pod' ? 'pod' : 'element',
|
selectionKind: comment.selectionKind === 'pod' ? 'pod' : 'element',
|
||||||
memberCount: comment.memberCount,
|
memberCount: comment.memberCount,
|
||||||
podMembers: normalizeMembers(comment.podMembers),
|
podMembers: normalizeMembers(comment.podMembers),
|
||||||
|
slideIndex: comment.slideIndex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,8 @@ export function BoardComposerPopover({
|
||||||
onHoverMember,
|
onHoverMember,
|
||||||
onDeleteComment,
|
onDeleteComment,
|
||||||
sending,
|
sending,
|
||||||
|
queueOnSend = false,
|
||||||
|
sendDisabled = false,
|
||||||
t,
|
t,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -254,6 +256,8 @@ export function BoardComposerPopover({
|
||||||
onHoverMember?: (elementId: string | null) => void;
|
onHoverMember?: (elementId: string | null) => void;
|
||||||
onDeleteComment?: (commentId: string) => void | Promise<void>;
|
onDeleteComment?: (commentId: string) => void | Promise<void>;
|
||||||
sending: boolean;
|
sending: boolean;
|
||||||
|
queueOnSend?: boolean;
|
||||||
|
sendDisabled?: boolean;
|
||||||
t: TranslateFn;
|
t: TranslateFn;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
bounds?: PopoverBounds;
|
bounds?: PopoverBounds;
|
||||||
|
|
@ -265,7 +269,12 @@ export function BoardComposerPopover({
|
||||||
const hasCommentChange = !existing || draft.trim() !== existing.note.trim();
|
const hasCommentChange = !existing || draft.trim() !== existing.note.trim();
|
||||||
const podMembers = target.podMembers ?? [];
|
const podMembers = target.podMembers ?? [];
|
||||||
const composingRef = useRef(false);
|
const composingRef = useRef(false);
|
||||||
const sendDisabled = pendingCount === 0 || sending;
|
const submitDisabled = pendingCount === 0 || sending || sendDisabled;
|
||||||
|
const primaryLabel = sending
|
||||||
|
? t('chat.comments.sending')
|
||||||
|
: queueOnSend
|
||||||
|
? t('chat.annotationQueue')
|
||||||
|
: t('chat.comments.sendToChat');
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`comment-popover${docked ? ' comment-popover-docked' : ''}`}
|
className={`comment-popover${docked ? ' comment-popover-docked' : ''}`}
|
||||||
|
|
@ -354,7 +363,7 @@ export function BoardComposerPopover({
|
||||||
(event.metaKey || event.ctrlKey)
|
(event.metaKey || event.ctrlKey)
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (sendDisabled) return;
|
if (submitDisabled) return;
|
||||||
void onSendBatch();
|
void onSendBatch();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
@ -409,10 +418,10 @@ export function BoardComposerPopover({
|
||||||
type="button"
|
type="button"
|
||||||
className="primary"
|
className="primary"
|
||||||
data-testid="comment-add-send"
|
data-testid="comment-add-send"
|
||||||
disabled={sendDisabled}
|
disabled={submitDisabled}
|
||||||
onClick={() => void onSendBatch()}
|
onClick={() => void onSendBatch()}
|
||||||
>
|
>
|
||||||
{sending ? t('chat.comments.sending') : t('chat.comments.sendToChat')}
|
{primaryLabel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,9 @@ import { PreviewDrawOverlay } from './PreviewDrawOverlay';
|
||||||
import {
|
import {
|
||||||
buildBoardCommentAttachments,
|
buildBoardCommentAttachments,
|
||||||
commentTargetDisplayName,
|
commentTargetDisplayName,
|
||||||
|
commentVisibleOnDeckSlide,
|
||||||
commentsToAttachments,
|
commentsToAttachments,
|
||||||
|
isValidCommentOverlayPosition,
|
||||||
liveSnapshotForComment,
|
liveSnapshotForComment,
|
||||||
overlayBoundsFromSnapshot,
|
overlayBoundsFromSnapshot,
|
||||||
selectionKindLabel,
|
selectionKindLabel,
|
||||||
|
|
@ -711,6 +713,8 @@ interface Props {
|
||||||
isDeck?: boolean;
|
isDeck?: boolean;
|
||||||
onExportAsPptx?: ((fileName: string) => void) | undefined;
|
onExportAsPptx?: ((fileName: string) => void) | undefined;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
|
commentQueueOnSend?: boolean;
|
||||||
|
commentSendDisabled?: boolean;
|
||||||
previewComments?: PreviewComment[];
|
previewComments?: PreviewComment[];
|
||||||
onSavePreviewComment?: (target: PreviewCommentTarget, note: string, attachAfterSave: boolean) => Promise<PreviewComment | null>;
|
onSavePreviewComment?: (target: PreviewCommentTarget, note: string, attachAfterSave: boolean) => Promise<PreviewComment | null>;
|
||||||
onRemovePreviewComment?: (commentId: string) => Promise<void>;
|
onRemovePreviewComment?: (commentId: string) => Promise<void>;
|
||||||
|
|
@ -733,6 +737,8 @@ export function FileViewer({
|
||||||
isDeck,
|
isDeck,
|
||||||
onExportAsPptx,
|
onExportAsPptx,
|
||||||
streaming,
|
streaming,
|
||||||
|
commentQueueOnSend = false,
|
||||||
|
commentSendDisabled = false,
|
||||||
previewComments = [],
|
previewComments = [],
|
||||||
onSavePreviewComment,
|
onSavePreviewComment,
|
||||||
onRemovePreviewComment,
|
onRemovePreviewComment,
|
||||||
|
|
@ -773,6 +779,8 @@ export function FileViewer({
|
||||||
isDeck={rendererMatch.renderer.id === 'deck-html'}
|
isDeck={rendererMatch.renderer.id === 'deck-html'}
|
||||||
onExportAsPptx={onExportAsPptx}
|
onExportAsPptx={onExportAsPptx}
|
||||||
streaming={Boolean(streaming)}
|
streaming={Boolean(streaming)}
|
||||||
|
commentQueueOnSend={commentQueueOnSend}
|
||||||
|
commentSendDisabled={commentSendDisabled}
|
||||||
previewComments={previewComments}
|
previewComments={previewComments}
|
||||||
onSavePreviewComment={onSavePreviewComment}
|
onSavePreviewComment={onSavePreviewComment}
|
||||||
onRemovePreviewComment={onRemovePreviewComment}
|
onRemovePreviewComment={onRemovePreviewComment}
|
||||||
|
|
@ -2094,6 +2102,8 @@ export function CommentSidePanel({
|
||||||
onSendSelected,
|
onSendSelected,
|
||||||
onCreateComment,
|
onCreateComment,
|
||||||
sending,
|
sending,
|
||||||
|
queueOnSend = false,
|
||||||
|
sendDisabled = false,
|
||||||
t,
|
t,
|
||||||
composer,
|
composer,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -2110,6 +2120,8 @@ export function CommentSidePanel({
|
||||||
onSendSelected: () => void | Promise<void>;
|
onSendSelected: () => void | Promise<void>;
|
||||||
onCreateComment?: (note: string) => boolean | Promise<boolean>;
|
onCreateComment?: (note: string) => boolean | Promise<boolean>;
|
||||||
sending: boolean;
|
sending: boolean;
|
||||||
|
queueOnSend?: boolean;
|
||||||
|
sendDisabled?: boolean;
|
||||||
t: TranslateFn;
|
t: TranslateFn;
|
||||||
composer?: ReactNode;
|
composer?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -2119,7 +2131,7 @@ export function CommentSidePanel({
|
||||||
const selectedCount = visibleSelectedIds.size;
|
const selectedCount = visibleSelectedIds.size;
|
||||||
const allSelected = comments.length > 0 && selectedCount === comments.length;
|
const allSelected = comments.length > 0 && selectedCount === comments.length;
|
||||||
const commentsLabel = t('chat.tabComments');
|
const commentsLabel = t('chat.tabComments');
|
||||||
const canCreateComment = Boolean(onCreateComment) && newCommentDraft.trim().length > 0 && !sending;
|
const canCreateComment = Boolean(onCreateComment) && newCommentDraft.trim().length > 0 && !sending && !sendDisabled;
|
||||||
const submitNewComment = async () => {
|
const submitNewComment = async () => {
|
||||||
if (!onCreateComment || !newCommentDraft.trim()) return;
|
if (!onCreateComment || !newCommentDraft.trim()) return;
|
||||||
const saved = await onCreateComment(newCommentDraft.trim());
|
const saved = await onCreateComment(newCommentDraft.trim());
|
||||||
|
|
@ -2228,10 +2240,14 @@ export function CommentSidePanel({
|
||||||
type="button"
|
type="button"
|
||||||
className="primary"
|
className="primary"
|
||||||
data-testid="comment-side-send-claude"
|
data-testid="comment-side-send-claude"
|
||||||
disabled={sending}
|
disabled={sending || sendDisabled}
|
||||||
onClick={() => void onSendSelected()}
|
onClick={() => void onSendSelected()}
|
||||||
>
|
>
|
||||||
{sending ? t('chat.comments.sending') : t('chat.comments.sendToChat')}
|
{sending
|
||||||
|
? t('chat.comments.sending')
|
||||||
|
: queueOnSend
|
||||||
|
? t('chat.annotationQueue')
|
||||||
|
: t('chat.comments.sendToChat')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -3015,6 +3031,7 @@ function CommentPreviewOverlays({
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
strokePoints,
|
strokePoints,
|
||||||
|
activeSlideIndex = null,
|
||||||
onOpenComment,
|
onOpenComment,
|
||||||
}: {
|
}: {
|
||||||
comments: PreviewComment[];
|
comments: PreviewComment[];
|
||||||
|
|
@ -3028,10 +3045,12 @@ function CommentPreviewOverlays({
|
||||||
offsetX: number;
|
offsetX: number;
|
||||||
offsetY: number;
|
offsetY: number;
|
||||||
strokePoints: StrokePoint[];
|
strokePoints: StrokePoint[];
|
||||||
|
activeSlideIndex?: number | null;
|
||||||
onOpenComment: (comment: PreviewComment, snapshot: PreviewCommentSnapshot) => void;
|
onOpenComment: (comment: PreviewComment, snapshot: PreviewCommentSnapshot) => void;
|
||||||
}) {
|
}) {
|
||||||
const overlayOffset = { x: offsetX, y: offsetY };
|
const overlayOffset = { x: offsetX, y: offsetY };
|
||||||
const visibleComments = comments
|
const visibleComments = comments
|
||||||
|
.filter((comment) => commentVisibleOnDeckSlide(comment, activeSlideIndex))
|
||||||
.map((comment, index) => ({
|
.map((comment, index) => ({
|
||||||
comment,
|
comment,
|
||||||
index,
|
index,
|
||||||
|
|
@ -3868,6 +3887,8 @@ function HtmlViewer({
|
||||||
isDeck,
|
isDeck,
|
||||||
onExportAsPptx,
|
onExportAsPptx,
|
||||||
streaming,
|
streaming,
|
||||||
|
commentQueueOnSend = false,
|
||||||
|
commentSendDisabled = false,
|
||||||
previewComments = [],
|
previewComments = [],
|
||||||
onSavePreviewComment,
|
onSavePreviewComment,
|
||||||
onRemovePreviewComment,
|
onRemovePreviewComment,
|
||||||
|
|
@ -3884,6 +3905,8 @@ function HtmlViewer({
|
||||||
isDeck: boolean;
|
isDeck: boolean;
|
||||||
onExportAsPptx?: ((fileName: string) => void) | undefined;
|
onExportAsPptx?: ((fileName: string) => void) | undefined;
|
||||||
streaming: boolean;
|
streaming: boolean;
|
||||||
|
commentQueueOnSend?: boolean;
|
||||||
|
commentSendDisabled?: boolean;
|
||||||
previewComments?: PreviewComment[];
|
previewComments?: PreviewComment[];
|
||||||
onSavePreviewComment?: (target: PreviewCommentTarget, note: string, attachAfterSave: boolean) => Promise<PreviewComment | null>;
|
onSavePreviewComment?: (target: PreviewCommentTarget, note: string, attachAfterSave: boolean) => Promise<PreviewComment | null>;
|
||||||
onRemovePreviewComment?: (commentId: string) => Promise<void>;
|
onRemovePreviewComment?: (commentId: string) => Promise<void>;
|
||||||
|
|
@ -4928,22 +4951,25 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
data.targets.forEach((item) => {
|
data.targets.forEach((item) => {
|
||||||
const elementId = String(item?.elementId || '');
|
const elementId = String(item?.elementId || '');
|
||||||
if (!elementId) return;
|
if (!elementId) return;
|
||||||
|
const position = {
|
||||||
|
x: clampBridgeCoordinate(item?.position?.x),
|
||||||
|
y: clampBridgeCoordinate(item?.position?.y),
|
||||||
|
width: clampBridgeCoordinate(item?.position?.width),
|
||||||
|
height: clampBridgeCoordinate(item?.position?.height),
|
||||||
|
};
|
||||||
|
if (!isValidCommentOverlayPosition(position)) return;
|
||||||
next.set(elementId, {
|
next.set(elementId, {
|
||||||
filePath: file.name,
|
filePath: file.name,
|
||||||
elementId,
|
elementId,
|
||||||
selector: String(item?.selector || ''),
|
selector: String(item?.selector || ''),
|
||||||
label: String(item?.label || ''),
|
label: String(item?.label || ''),
|
||||||
text: String(item?.text || ''),
|
text: String(item?.text || ''),
|
||||||
position: {
|
position,
|
||||||
x: clampBridgeCoordinate(item?.position?.x),
|
|
||||||
y: clampBridgeCoordinate(item?.position?.y),
|
|
||||||
width: clampBridgeCoordinate(item?.position?.width),
|
|
||||||
height: clampBridgeCoordinate(item?.position?.height),
|
|
||||||
},
|
|
||||||
htmlHint: String(item?.htmlHint || ''),
|
htmlHint: String(item?.htmlHint || ''),
|
||||||
style: normalizeAnnotationStyle(item?.style),
|
style: normalizeAnnotationStyle(item?.style),
|
||||||
selectionKind: 'element',
|
selectionKind: 'element',
|
||||||
memberCount: undefined,
|
memberCount: undefined,
|
||||||
|
...(typeof item?.slideIndex === 'number' ? { slideIndex: item.slideIndex } : {}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
setLiveCommentTargets(next);
|
setLiveCommentTargets(next);
|
||||||
|
|
@ -5053,6 +5079,7 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
selectionKind: data.selectionKind === 'pod' ? 'pod' : 'element',
|
selectionKind: data.selectionKind === 'pod' ? 'pod' : 'element',
|
||||||
memberCount: finiteBridgeInteger(data.memberCount),
|
memberCount: finiteBridgeInteger(data.memberCount),
|
||||||
podMembers: Array.isArray(data.podMembers) ? data.podMembers : undefined,
|
podMembers: Array.isArray(data.podMembers) ? data.podMembers : undefined,
|
||||||
|
...(typeof data.slideIndex === 'number' ? { slideIndex: data.slideIndex } : {}),
|
||||||
});
|
});
|
||||||
function onMessage(ev: MessageEvent) {
|
function onMessage(ev: MessageEvent) {
|
||||||
if (!isOurPreviewIframeSource(ev.source)) return;
|
if (!isOurPreviewIframeSource(ev.source)) return;
|
||||||
|
|
@ -5066,28 +5093,29 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
const next = new Map<string, PreviewCommentSnapshot>();
|
const next = new Map<string, PreviewCommentSnapshot>();
|
||||||
data.targets.forEach((item) => {
|
data.targets.forEach((item) => {
|
||||||
const snapshot = snapshotFromData(item);
|
const snapshot = snapshotFromData(item);
|
||||||
if (snapshot.elementId) next.set(snapshot.elementId, snapshot);
|
if (!snapshot.elementId || !isValidCommentOverlayPosition(snapshot.position)) return;
|
||||||
|
next.set(snapshot.elementId, snapshot);
|
||||||
});
|
});
|
||||||
setLiveCommentTargets(next);
|
setLiveCommentTargets(next);
|
||||||
setActiveCommentTarget((current) => (
|
setActiveCommentTarget((current) => {
|
||||||
current
|
if (!current) return null;
|
||||||
? current.selectionKind === 'pod'
|
if (current.selectionKind === 'pod') return current;
|
||||||
? current
|
const updated = next.get(current.elementId);
|
||||||
: next.get(current.elementId) ?? current
|
if (updated && isValidCommentOverlayPosition(updated.position)) return updated;
|
||||||
: null
|
return null;
|
||||||
));
|
});
|
||||||
setHoveredCommentTarget((current) => (
|
setHoveredCommentTarget((current) => {
|
||||||
current
|
if (!current) return null;
|
||||||
? current.selectionKind === 'pod'
|
if (current.selectionKind === 'pod') return current;
|
||||||
? current
|
const updated = next.get(current.elementId);
|
||||||
: next.get(current.elementId) ?? null
|
if (updated && isValidCommentOverlayPosition(updated.position)) return updated;
|
||||||
: null
|
return null;
|
||||||
));
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.type === 'od:comment-active-target-update') {
|
if (data.type === 'od:comment-active-target-update') {
|
||||||
const snapshot = snapshotFromData(data);
|
const snapshot = snapshotFromData(data);
|
||||||
if (!snapshot.elementId) return;
|
if (!snapshot.elementId || !isValidCommentOverlayPosition(snapshot.position)) return;
|
||||||
setLiveCommentTargets((current) => new Map(current).set(snapshot.elementId, snapshot));
|
setLiveCommentTargets((current) => new Map(current).set(snapshot.elementId, snapshot));
|
||||||
setActiveCommentTarget((current) => (
|
setActiveCommentTarget((current) => (
|
||||||
current && current.elementId === snapshot.elementId ? snapshot : current
|
current && current.elementId === snapshot.elementId ? snapshot : current
|
||||||
|
|
@ -5103,14 +5131,14 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
}
|
}
|
||||||
if (data.type === 'od:comment-hover') {
|
if (data.type === 'od:comment-hover') {
|
||||||
const snapshot = snapshotFromData(data);
|
const snapshot = snapshotFromData(data);
|
||||||
if (!snapshot.elementId) return;
|
if (!snapshot.elementId || !isValidCommentOverlayPosition(snapshot.position)) return;
|
||||||
setHoveredCommentTarget(snapshot);
|
setHoveredCommentTarget(snapshot);
|
||||||
setLiveCommentTargets((current) => new Map(current).set(snapshot.elementId, snapshot));
|
setLiveCommentTargets((current) => new Map(current).set(snapshot.elementId, snapshot));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.type === 'od:comment-target') {
|
if (data.type === 'od:comment-target') {
|
||||||
const snapshot = snapshotFromData(data);
|
const snapshot = snapshotFromData(data);
|
||||||
if (!snapshot.elementId) return;
|
if (!snapshot.elementId || !isValidCommentOverlayPosition(snapshot.position)) return;
|
||||||
const shouldOpenComposer = boardMode || commentCreateMode;
|
const shouldOpenComposer = boardMode || commentCreateMode;
|
||||||
setActiveCommentTarget((current) => (shouldOpenComposer ? snapshot : current));
|
setActiveCommentTarget((current) => (shouldOpenComposer ? snapshot : current));
|
||||||
setHoveredCommentTarget(snapshot);
|
setHoveredCommentTarget(snapshot);
|
||||||
|
|
@ -6167,6 +6195,12 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
setCommentDraft('');
|
setCommentDraft('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withDeckSlideIndex(target: PreviewCommentTarget): PreviewCommentTarget {
|
||||||
|
if (!effectiveDeck || typeof slideState?.active !== 'number') return target;
|
||||||
|
if (typeof target.slideIndex === 'number') return target;
|
||||||
|
return { ...target, slideIndex: slideState.active };
|
||||||
|
}
|
||||||
|
|
||||||
async function sendBoardBatch() {
|
async function sendBoardBatch() {
|
||||||
if (!activeCommentTarget || !onSendBoardCommentAttachments) return;
|
if (!activeCommentTarget || !onSendBoardCommentAttachments) return;
|
||||||
const nextNotes = [...queuedBoardNotes];
|
const nextNotes = [...queuedBoardNotes];
|
||||||
|
|
@ -6176,7 +6210,7 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
try {
|
try {
|
||||||
await onSendBoardCommentAttachments(
|
await onSendBoardCommentAttachments(
|
||||||
buildBoardCommentAttachments({
|
buildBoardCommentAttachments({
|
||||||
target: targetFromSnapshot(activeCommentTarget),
|
target: withDeckSlideIndex(targetFromSnapshot(activeCommentTarget)),
|
||||||
notes: nextNotes,
|
notes: nextNotes,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -6191,8 +6225,9 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
const isFreePin = activeCommentTarget.elementId.startsWith('pin-');
|
const isFreePin = activeCommentTarget.elementId.startsWith('pin-');
|
||||||
setSendingBoardBatch(true);
|
setSendingBoardBatch(true);
|
||||||
try {
|
try {
|
||||||
|
const target = withDeckSlideIndex(targetFromSnapshot(activeCommentTarget));
|
||||||
const saved = await onSavePreviewComment(
|
const saved = await onSavePreviewComment(
|
||||||
targetFromSnapshot(activeCommentTarget),
|
target,
|
||||||
commentDraft.trim(),
|
commentDraft.trim(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
@ -6383,6 +6418,14 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
const stillOpen = visibleSideComments.some((comment) => comment.id === activePreviewCommentId);
|
const stillOpen = visibleSideComments.some((comment) => comment.id === activePreviewCommentId);
|
||||||
if (!stillOpen) clearBoardComposer();
|
if (!stillOpen) clearBoardComposer();
|
||||||
}, [activePreviewCommentId, boardMode, visibleSideComments]);
|
}, [activePreviewCommentId, boardMode, visibleSideComments]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!effectiveDeck || slideState == null || !boardMode) return;
|
||||||
|
if (!activePreviewCommentId) return;
|
||||||
|
const activeComment = visibleSideComments.find((comment) => comment.id === activePreviewCommentId);
|
||||||
|
if (activeComment && !commentVisibleOnDeckSlide(activeComment, slideState.active)) {
|
||||||
|
clearBoardComposer();
|
||||||
|
}
|
||||||
|
}, [activePreviewCommentId, boardMode, effectiveDeck, slideState?.active, visibleSideComments]);
|
||||||
const activeDeployment = deployResult || deployment;
|
const activeDeployment = deployResult || deployment;
|
||||||
const activeDeployedUrl = activeDeployment?.url?.trim() || '';
|
const activeDeployedUrl = activeDeployment?.url?.trim() || '';
|
||||||
const activeDeploymentDelayed = activeDeployment?.status === 'link-delayed';
|
const activeDeploymentDelayed = activeDeployment?.status === 'link-delayed';
|
||||||
|
|
@ -6578,7 +6621,9 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
});
|
});
|
||||||
setActivePreviewCommentId((current) => (current === commentId ? null : current));
|
setActivePreviewCommentId((current) => (current === commentId ? null : current));
|
||||||
} : undefined}
|
} : undefined}
|
||||||
sending={sendingBoardBatch || streaming}
|
sending={sendingBoardBatch}
|
||||||
|
queueOnSend={commentQueueOnSend}
|
||||||
|
sendDisabled={commentSendDisabled}
|
||||||
t={t}
|
t={t}
|
||||||
scale={overlayPreviewScale}
|
scale={overlayPreviewScale}
|
||||||
offset={{ x: overlayPreviewTransform.offsetX, y: overlayPreviewTransform.offsetY }}
|
offset={{ x: overlayPreviewTransform.offsetX, y: overlayPreviewTransform.offsetY }}
|
||||||
|
|
@ -6656,7 +6701,9 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onCreateComment={savePanelComment}
|
onCreateComment={savePanelComment}
|
||||||
sending={sendingBoardBatch || streaming}
|
sending={sendingBoardBatch}
|
||||||
|
queueOnSend={commentQueueOnSend}
|
||||||
|
sendDisabled={commentSendDisabled}
|
||||||
t={t}
|
t={t}
|
||||||
composer={null}
|
composer={null}
|
||||||
/>
|
/>
|
||||||
|
|
@ -7253,6 +7300,7 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
|
||||||
offsetX={overlayPreviewTransform.offsetX}
|
offsetX={overlayPreviewTransform.offsetX}
|
||||||
offsetY={overlayPreviewTransform.offsetY}
|
offsetY={overlayPreviewTransform.offsetY}
|
||||||
strokePoints={strokePoints}
|
strokePoints={strokePoints}
|
||||||
|
activeSlideIndex={effectiveDeck ? slideState?.active ?? null : null}
|
||||||
onOpenComment={(comment, snapshot) => {
|
onOpenComment={(comment, snapshot) => {
|
||||||
setCommentPanelOpen(true);
|
setCommentPanelOpen(true);
|
||||||
setCommentSidePanelCollapsed(false);
|
setCommentSidePanelCollapsed(false);
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ interface Props {
|
||||||
isDeck: boolean;
|
isDeck: boolean;
|
||||||
onExportAsPptx?: ((fileName: string) => void) | undefined;
|
onExportAsPptx?: ((fileName: string) => void) | undefined;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
|
commentQueueOnSend?: boolean;
|
||||||
|
commentSendDisabled?: boolean;
|
||||||
openRequest?: { name: string; nonce: number } | null;
|
openRequest?: { name: string; nonce: number } | null;
|
||||||
liveArtifactEvents?: LiveArtifactEventItem[];
|
liveArtifactEvents?: LiveArtifactEventItem[];
|
||||||
designSystemActivityEvents?: AgentEvent[];
|
designSystemActivityEvents?: AgentEvent[];
|
||||||
|
|
@ -200,6 +202,8 @@ export function FileWorkspace({
|
||||||
isDeck,
|
isDeck,
|
||||||
onExportAsPptx,
|
onExportAsPptx,
|
||||||
streaming,
|
streaming,
|
||||||
|
commentQueueOnSend = false,
|
||||||
|
commentSendDisabled = false,
|
||||||
openRequest,
|
openRequest,
|
||||||
liveArtifactEvents = [],
|
liveArtifactEvents = [],
|
||||||
designSystemActivityEvents = [],
|
designSystemActivityEvents = [],
|
||||||
|
|
@ -1063,6 +1067,8 @@ export function FileWorkspace({
|
||||||
isDeck={isDeck}
|
isDeck={isDeck}
|
||||||
onExportAsPptx={onExportAsPptx}
|
onExportAsPptx={onExportAsPptx}
|
||||||
streaming={streaming}
|
streaming={streaming}
|
||||||
|
commentQueueOnSend={commentQueueOnSend}
|
||||||
|
commentSendDisabled={commentSendDisabled}
|
||||||
previewComments={previewComments.filter((comment) => comment.filePath === activeFile.name)}
|
previewComments={previewComments.filter((comment) => comment.filePath === activeFile.name)}
|
||||||
onSavePreviewComment={onSavePreviewComment}
|
onSavePreviewComment={onSavePreviewComment}
|
||||||
onRemovePreviewComment={onRemovePreviewComment}
|
onRemovePreviewComment={onRemovePreviewComment}
|
||||||
|
|
|
||||||
|
|
@ -3064,13 +3064,15 @@ export function ProjectView({
|
||||||
|
|
||||||
const handleSendBoardCommentAttachments = useCallback(
|
const handleSendBoardCommentAttachments = useCallback(
|
||||||
async (commentAttachments: ChatCommentAttachment[]) => {
|
async (commentAttachments: ChatCommentAttachment[]) => {
|
||||||
if (currentConversationActionDisabled || commentAttachments.length === 0) return;
|
// Match ChatComposer: while a run is in flight we queue instead of blocking.
|
||||||
|
if (currentConversationSendDisabled || commentAttachments.length === 0) return;
|
||||||
setWorkspaceFocused(false);
|
setWorkspaceFocused(false);
|
||||||
setCommentInspectorActive(false);
|
setCommentInspectorActive(false);
|
||||||
await handleSend('', [], commentAttachments);
|
await handleSend('', [], commentAttachments);
|
||||||
},
|
},
|
||||||
[handleSend, currentConversationActionDisabled],
|
[handleSend, currentConversationSendDisabled],
|
||||||
);
|
);
|
||||||
|
const commentQueueOnSend = currentConversationBusy && !currentConversationSendDisabled;
|
||||||
|
|
||||||
const handleContinueRemainingTasks = useCallback(
|
const handleContinueRemainingTasks = useCallback(
|
||||||
(_assistantMessage: ChatMessage, todos: TodoItem[]) => {
|
(_assistantMessage: ChatMessage, todos: TodoItem[]) => {
|
||||||
|
|
@ -4556,6 +4558,8 @@ export function ProjectView({
|
||||||
isDeck={isDeck}
|
isDeck={isDeck}
|
||||||
onExportAsPptx={handleExportAsPptx}
|
onExportAsPptx={handleExportAsPptx}
|
||||||
streaming={currentConversationActionDisabled}
|
streaming={currentConversationActionDisabled}
|
||||||
|
commentQueueOnSend={commentQueueOnSend}
|
||||||
|
commentSendDisabled={currentConversationSendDisabled}
|
||||||
openRequest={openRequest}
|
openRequest={openRequest}
|
||||||
liveArtifactEvents={liveArtifactEvents}
|
liveArtifactEvents={liveArtifactEvents}
|
||||||
designSystemActivityEvents={designSystemActivityEvents}
|
designSystemActivityEvents={designSystemActivityEvents}
|
||||||
|
|
|
||||||
|
|
@ -846,6 +846,21 @@ function injectSelectionBridge(
|
||||||
// brackets (close the <style> tag), and newlines (defense in depth).
|
// brackets (close the <style> tag), and newlines (defense in depth).
|
||||||
var UNSAFE_VALUE = /[;{}<>\\n\\r]/;
|
var UNSAFE_VALUE = /[;{}<>\\n\\r]/;
|
||||||
function active(){ return commentEnabled || inspectEnabled; }
|
function active(){ return commentEnabled || inspectEnabled; }
|
||||||
|
function deckSlideIndexForPayload(){
|
||||||
|
try {
|
||||||
|
var state = window.__odDeckSlideState && window.__odDeckSlideState();
|
||||||
|
if (state && typeof state.active === 'number' && state.count > 1) return state.active;
|
||||||
|
} catch (_) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function elementVisibleForComment(el, rect){
|
||||||
|
if (!el || !rect || rect.width <= 0 || rect.height <= 0) return false;
|
||||||
|
try {
|
||||||
|
var cs = window.getComputedStyle(el);
|
||||||
|
if (cs.display === 'none' || cs.visibility === 'hidden' || Number(cs.opacity) === 0) return false;
|
||||||
|
} catch (_) {}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
function esc(value){ try { return window.CSS && CSS.escape ? CSS.escape(value) : String(value).replace(/"/g, '\\\\"'); } catch (_) { return String(value); } }
|
function esc(value){ try { return window.CSS && CSS.escape ? CSS.escape(value) : String(value).replace(/"/g, '\\\\"'); } catch (_) { return String(value); } }
|
||||||
// Recompute the selector from elementId rather than trusting the one in
|
// Recompute the selector from elementId rather than trusting the one in
|
||||||
// the inbound message — a forged selector like
|
// the inbound message — a forged selector like
|
||||||
|
|
@ -1077,6 +1092,7 @@ function meaningfulDomFallbackTarget(el) {
|
||||||
var html = '';
|
var html = '';
|
||||||
try { html = (el.outerHTML || '').replace(/\\s+/g, ' ').match(/^<[^>]+>/)?.[0] || ''; } catch (_) {}
|
try { html = (el.outerHTML || '').replace(/\\s+/g, ' ').match(/^<[^>]+>/)?.[0] || ''; } catch (_) {}
|
||||||
var position = { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) };
|
var position = { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) };
|
||||||
|
if (!elementVisibleForComment(el, position)) return null;
|
||||||
var payload = {
|
var payload = {
|
||||||
type: 'od:comment-target',
|
type: 'od:comment-target',
|
||||||
elementId: id,
|
elementId: id,
|
||||||
|
|
@ -1087,6 +1103,8 @@ function meaningfulDomFallbackTarget(el) {
|
||||||
htmlHint: html.slice(0, 180),
|
htmlHint: html.slice(0, 180),
|
||||||
style: styleSnapshot(el)
|
style: styleSnapshot(el)
|
||||||
};
|
};
|
||||||
|
var slideIndex = deckSlideIndexForPayload();
|
||||||
|
if (typeof slideIndex === 'number') payload.slideIndex = slideIndex;
|
||||||
if (clickPoint) {
|
if (clickPoint) {
|
||||||
payload.hoverPoint = { x: Math.round(clickPoint.x), y: Math.round(clickPoint.y) };
|
payload.hoverPoint = { x: Math.round(clickPoint.x), y: Math.round(clickPoint.y) };
|
||||||
}
|
}
|
||||||
|
|
@ -1437,9 +1455,9 @@ function meaningfulDomFallbackTarget(el) {
|
||||||
var pinX = Math.round(ev.clientX);
|
var pinX = Math.round(ev.clientX);
|
||||||
var pinY = Math.round(ev.clientY);
|
var pinY = Math.round(ev.clientY);
|
||||||
var pinId = 'pin-' + Date.now().toString(36) + '-' + Math.floor(Math.random() * 1e6).toString(36);
|
var pinId = 'pin-' + Date.now().toString(36) + '-' + Math.floor(Math.random() * 1e6).toString(36);
|
||||||
window.parent.postMessage({
|
var pinSlideIndex = deckSlideIndexForPayload();
|
||||||
|
var pinPayload = {
|
||||||
type: 'od:comment-target',
|
type: 'od:comment-target',
|
||||||
elementId: pinId,
|
|
||||||
// Synthetic selector / label so daemon upsert validation (which
|
// Synthetic selector / label so daemon upsert validation (which
|
||||||
// requires both to be non-empty) accepts the saved free-pin.
|
// requires both to be non-empty) accepts the saved free-pin.
|
||||||
selector: '[data-od-pin="' + pinId + '"]',
|
selector: '[data-od-pin="' + pinId + '"]',
|
||||||
|
|
@ -1450,7 +1468,10 @@ function meaningfulDomFallbackTarget(el) {
|
||||||
htmlHint: '',
|
htmlHint: '',
|
||||||
style: null,
|
style: null,
|
||||||
freePin: true
|
freePin: true
|
||||||
}, '*');
|
};
|
||||||
|
pinPayload.elementId = pinId;
|
||||||
|
if (typeof pinSlideIndex === 'number') pinPayload.slideIndex = pinSlideIndex;
|
||||||
|
window.parent.postMessage(pinPayload, '*');
|
||||||
}, true);
|
}, true);
|
||||||
// Pod drawing — only active in comment mode with the 'pod' tool.
|
// Pod drawing — only active in comment mode with the 'pod' tool.
|
||||||
document.addEventListener('pointerdown', function(ev){
|
document.addEventListener('pointerdown', function(ev){
|
||||||
|
|
@ -1505,6 +1526,7 @@ function meaningfulDomFallbackTarget(el) {
|
||||||
setTimeout(requestPreviewScrollRestore, 0);
|
setTimeout(requestPreviewScrollRestore, 0);
|
||||||
setTimeout(requestPreviewScrollRestore, 80);
|
setTimeout(requestPreviewScrollRestore, 80);
|
||||||
setTimeout(requestPreviewScrollRestore, 240);
|
setTimeout(requestPreviewScrollRestore, 240);
|
||||||
|
window.__odScheduleCommentTargets = schedulePostTargets;
|
||||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', postTargets);
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', postTargets);
|
||||||
else setTimeout(postTargets, 0);
|
else setTimeout(postTargets, 0);
|
||||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', postPreviewScroll);
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', postPreviewScroll);
|
||||||
|
|
@ -1896,8 +1918,15 @@ function injectDeckBridge(doc: string, initialSlideIndex = 0): string {
|
||||||
if (el.querySelector('span,.bar')) return;
|
if (el.querySelector('span,.bar')) return;
|
||||||
el.style.width=progressWidth;
|
el.style.width=progressWidth;
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
if (typeof window.__odScheduleCommentTargets === 'function') window.__odScheduleCommentTargets();
|
||||||
|
} catch (_) {}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
window.__odDeckSlideState = function(){
|
||||||
|
var list = slides();
|
||||||
|
return { active: activeIndex(list), count: list.length };
|
||||||
|
};
|
||||||
function restoreInitialSlide(){
|
function restoreInitialSlide(){
|
||||||
if (didRestoreInitialSlide) { report(); return; }
|
if (didRestoreInitialSlide) { report(); return; }
|
||||||
var list = slides();
|
var list = slides();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
buildBoardCommentAttachments,
|
buildBoardCommentAttachments,
|
||||||
buildVisualAnnotationAttachment,
|
buildVisualAnnotationAttachment,
|
||||||
|
commentVisibleOnDeckSlide,
|
||||||
commentsToAttachments,
|
commentsToAttachments,
|
||||||
historyWithCommentAttachmentContext,
|
historyWithCommentAttachmentContext,
|
||||||
liveSnapshotForComment,
|
liveSnapshotForComment,
|
||||||
|
|
@ -241,6 +242,29 @@ describe('preview comment attachment helpers', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignores collapsed live snapshots so deck slide changes do not jump markers to 0,0', () => {
|
||||||
|
const saved = comment({ filePath: 'index.html', elementId: 'hero-title' });
|
||||||
|
const snapshots = new Map([
|
||||||
|
['hero-title', {
|
||||||
|
filePath: 'index.html',
|
||||||
|
elementId: 'hero-title',
|
||||||
|
selector: '[data-od-id="hero-title"]',
|
||||||
|
label: 'h1.hero-title',
|
||||||
|
text: '',
|
||||||
|
htmlHint: '',
|
||||||
|
position: { x: 0, y: 0, width: 0, height: 0 },
|
||||||
|
}],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(liveSnapshotForComment(saved, snapshots)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows deck comments only on their saved slide index', () => {
|
||||||
|
expect(commentVisibleOnDeckSlide({ slideIndex: 2 }, 2)).toBe(true);
|
||||||
|
expect(commentVisibleOnDeckSlide({ slideIndex: 2 }, 1)).toBe(false);
|
||||||
|
expect(commentVisibleOnDeckSlide({}, 1)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('serializes selected comments into API-mode prompt context without visible input', () => {
|
it('serializes selected comments into API-mode prompt context without visible input', () => {
|
||||||
const attachments = commentsToAttachments([
|
const attachments = commentsToAttachments([
|
||||||
comment({
|
comment({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
// Regression: while a chat run is in flight the comment popover must queue
|
||||||
|
// instead of showing a stuck "Sending..." state with a disabled primary action.
|
||||||
|
|
||||||
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { BoardComposerPopover } from '../../src/components/BoardComposerPopover';
|
||||||
|
import type { PreviewCommentSnapshot } from '../../src/comments';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
const target: PreviewCommentSnapshot = {
|
||||||
|
filePath: 'index.html',
|
||||||
|
elementId: 'hero-title',
|
||||||
|
selector: '#hero-title',
|
||||||
|
label: 'Hero title',
|
||||||
|
text: '',
|
||||||
|
position: { x: 0, y: 0, width: 100, height: 24 },
|
||||||
|
htmlHint: '',
|
||||||
|
selectionKind: 'element',
|
||||||
|
};
|
||||||
|
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'chat.comments.sendToChat': 'Send to chat',
|
||||||
|
'chat.comments.sending': 'Sending…',
|
||||||
|
'chat.annotationQueue': 'Queue',
|
||||||
|
'chat.comments.placeholder': 'Comment on this element…',
|
||||||
|
'chat.comments.comment': 'Comment',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('BoardComposerPopover queue on busy conversation', () => {
|
||||||
|
it('shows Queue and stays clickable while a run is in flight', () => {
|
||||||
|
const onSendBatch = vi.fn();
|
||||||
|
render(
|
||||||
|
<BoardComposerPopover
|
||||||
|
target={target}
|
||||||
|
existing={null}
|
||||||
|
draft="Make this text white"
|
||||||
|
notes={[]}
|
||||||
|
onDraft={() => {}}
|
||||||
|
onAddDraft={() => {}}
|
||||||
|
onRemoveQueuedNote={() => {}}
|
||||||
|
onClose={() => {}}
|
||||||
|
onSaveComment={() => {}}
|
||||||
|
onSendBatch={onSendBatch}
|
||||||
|
onRemoveMember={() => {}}
|
||||||
|
sending={false}
|
||||||
|
queueOnSend
|
||||||
|
sendDisabled={false}
|
||||||
|
t={((key: string) => labels[key] ?? key) as never}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const send = screen.getByTestId('comment-add-send') as HTMLButtonElement;
|
||||||
|
expect(send.textContent).toBe('Queue');
|
||||||
|
expect(send.disabled).toBe(false);
|
||||||
|
|
||||||
|
fireEvent.click(send);
|
||||||
|
expect(onSendBatch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows Sending… only while the batch submit is in flight', () => {
|
||||||
|
render(
|
||||||
|
<BoardComposerPopover
|
||||||
|
target={target}
|
||||||
|
existing={null}
|
||||||
|
draft="Make this text white"
|
||||||
|
notes={[]}
|
||||||
|
onDraft={() => {}}
|
||||||
|
onAddDraft={() => {}}
|
||||||
|
onRemoveQueuedNote={() => {}}
|
||||||
|
onClose={() => {}}
|
||||||
|
onSaveComment={() => {}}
|
||||||
|
onSendBatch={() => {}}
|
||||||
|
onRemoveMember={() => {}}
|
||||||
|
sending
|
||||||
|
queueOnSend
|
||||||
|
sendDisabled={false}
|
||||||
|
t={((key: string) => labels[key] ?? key) as never}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const send = screen.getByTestId('comment-add-send') as HTMLButtonElement;
|
||||||
|
expect(send.textContent).toBe('Sending…');
|
||||||
|
expect(send.disabled).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -55,6 +55,8 @@ export interface PreviewCommentTarget {
|
||||||
selectionKind?: PreviewCommentSelectionKind;
|
selectionKind?: PreviewCommentSelectionKind;
|
||||||
memberCount?: number;
|
memberCount?: number;
|
||||||
podMembers?: PreviewCommentMember[];
|
podMembers?: PreviewCommentMember[];
|
||||||
|
/** Zero-based deck slide index when the comment was placed. */
|
||||||
|
slideIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreviewComment {
|
export interface PreviewComment {
|
||||||
|
|
@ -72,6 +74,8 @@ export interface PreviewComment {
|
||||||
selectionKind?: PreviewCommentSelectionKind;
|
selectionKind?: PreviewCommentSelectionKind;
|
||||||
memberCount?: number;
|
memberCount?: number;
|
||||||
podMembers?: PreviewCommentMember[];
|
podMembers?: PreviewCommentMember[];
|
||||||
|
/** Zero-based deck slide index when the comment was placed. */
|
||||||
|
slideIndex?: number;
|
||||||
note: string;
|
note: string;
|
||||||
status: PreviewCommentStatus;
|
status: PreviewCommentStatus;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue