@alpaca-editor/core 1.0.4104 → 1.0.4106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/config/config.js +7 -0
  2. package/dist/config/config.js.map +1 -1
  3. package/dist/editor/MainLayout.js +1 -1
  4. package/dist/editor/MainLayout.js.map +1 -1
  5. package/dist/editor/Terminal.js +2 -2
  6. package/dist/editor/Terminal.js.map +1 -1
  7. package/dist/editor/ai/AgentTerminal.js +5 -3
  8. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  9. package/dist/editor/ai/Agents.js +9 -1
  10. package/dist/editor/ai/Agents.js.map +1 -1
  11. package/dist/editor/ai/AiResponseMessage.js +4 -2
  12. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  13. package/dist/editor/client/EditorShell.js +2 -0
  14. package/dist/editor/client/EditorShell.js.map +1 -1
  15. package/dist/editor/control-center/IndexOverview.js +51 -32
  16. package/dist/editor/control-center/IndexOverview.js.map +1 -1
  17. package/dist/editor/control-center/LatestFeedback.d.ts +1 -0
  18. package/dist/editor/control-center/LatestFeedback.js +134 -0
  19. package/dist/editor/control-center/LatestFeedback.js.map +1 -0
  20. package/dist/editor/control-center/WebSocketMessages.js +3 -3
  21. package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
  22. package/dist/editor/field-types/richtext/contextMenuFactory.js +11 -0
  23. package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -1
  24. package/dist/editor/media-selector/MediaFolderBrowser.js +2 -2
  25. package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
  26. package/dist/editor/menubar/ActiveUsers.js +3 -2
  27. package/dist/editor/menubar/ActiveUsers.js.map +1 -1
  28. package/dist/editor/menubar/toolbar-sections/EditControls.js +1 -2
  29. package/dist/editor/menubar/toolbar-sections/EditControls.js.map +1 -1
  30. package/dist/editor/menubar/toolbar-sections/ViewportControls.js +3 -2
  31. package/dist/editor/menubar/toolbar-sections/ViewportControls.js.map +1 -1
  32. package/dist/editor/reviews/Comment.js +12 -2
  33. package/dist/editor/reviews/Comment.js.map +1 -1
  34. package/dist/editor/reviews/CommentView.d.ts +3 -1
  35. package/dist/editor/reviews/CommentView.js +9 -6
  36. package/dist/editor/reviews/CommentView.js.map +1 -1
  37. package/dist/editor/reviews/Comments.js +64 -76
  38. package/dist/editor/reviews/Comments.js.map +1 -1
  39. package/dist/editor/reviews/SuggestedEdit.js +8 -6
  40. package/dist/editor/reviews/SuggestedEdit.js.map +1 -1
  41. package/dist/editor/reviews/SuggestionDisplayPopover.js +7 -5
  42. package/dist/editor/reviews/SuggestionDisplayPopover.js.map +1 -1
  43. package/dist/editor/services/reviewsService.d.ts +1 -0
  44. package/dist/editor/services/reviewsService.js +8 -0
  45. package/dist/editor/services/reviewsService.js.map +1 -1
  46. package/dist/editor/services/suggestedEditsService.d.ts +1 -0
  47. package/dist/editor/services/suggestedEditsService.js +8 -0
  48. package/dist/editor/services/suggestedEditsService.js.map +1 -1
  49. package/dist/editor/sidebar/Completions.js +2 -1
  50. package/dist/editor/sidebar/Completions.js.map +1 -1
  51. package/dist/editor/ui/Icons.d.ts +2 -1
  52. package/dist/editor/ui/Icons.js +2 -2
  53. package/dist/editor/ui/Icons.js.map +1 -1
  54. package/dist/editor/ui/SimpleTable.js +1 -1
  55. package/dist/editor/ui/SimpleTable.js.map +1 -1
  56. package/dist/editor/utils.d.ts +12 -1
  57. package/dist/editor/utils.js +60 -12
  58. package/dist/editor/utils.js.map +1 -1
  59. package/dist/revision.d.ts +2 -2
  60. package/dist/revision.js +2 -2
  61. package/dist/types.d.ts +4 -0
  62. package/package.json +1 -1
  63. package/src/config/config.tsx +7 -0
  64. package/src/editor/MainLayout.tsx +1 -1
  65. package/src/editor/Terminal.tsx +2 -2
  66. package/src/editor/ai/AgentTerminal.tsx +8 -20
  67. package/src/editor/ai/Agents.tsx +14 -2
  68. package/src/editor/ai/AiResponseMessage.tsx +7 -5
  69. package/src/editor/client/EditorShell.tsx +2 -0
  70. package/src/editor/control-center/IndexOverview.tsx +70 -35
  71. package/src/editor/control-center/LatestFeedback.tsx +198 -0
  72. package/src/editor/control-center/WebSocketMessages.tsx +3 -5
  73. package/src/editor/field-types/richtext/contextMenuFactory.tsx +14 -0
  74. package/src/editor/media-selector/MediaFolderBrowser.tsx +2 -2
  75. package/src/editor/menubar/ActiveUsers.tsx +4 -3
  76. package/src/editor/menubar/toolbar-sections/EditControls.tsx +2 -3
  77. package/src/editor/menubar/toolbar-sections/ViewportControls.tsx +19 -1
  78. package/src/editor/reviews/Comment.tsx +16 -1
  79. package/src/editor/reviews/CommentView.tsx +24 -24
  80. package/src/editor/reviews/Comments.tsx +122 -145
  81. package/src/editor/reviews/SuggestedEdit.tsx +16 -10
  82. package/src/editor/reviews/SuggestionDisplayPopover.tsx +14 -9
  83. package/src/editor/services/reviewsService.ts +10 -0
  84. package/src/editor/services/suggestedEditsService.ts +10 -0
  85. package/src/editor/sidebar/Completions.tsx +2 -1
  86. package/src/editor/ui/Icons.tsx +3 -0
  87. package/src/editor/ui/SimpleTable.tsx +2 -2
  88. package/src/editor/utils.ts +73 -15
  89. package/src/revision.ts +2 -2
  90. package/src/types.ts +4 -0
@@ -1,14 +1,16 @@
1
- import { useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useState } from "react";
2
2
  import {
3
3
  RefreshCw,
4
+ ExternalLink,
4
5
  Database,
5
6
  Clock,
6
7
  CheckCircle,
7
8
  AlertCircle,
8
9
  Trash2,
9
10
  } from "lucide-react";
10
-
11
+ import { Button } from "../../components/ui/button";
11
12
  import { classNames } from "primereact/utils";
13
+ import { formatDateOnly, formatDateTime } from "../utils";
12
14
  import {
13
15
  IndexStatus,
14
16
  StagingStatus,
@@ -25,6 +27,8 @@ import {
25
27
  startDirectGeneration,
26
28
  } from "../services/indexService";
27
29
 
30
+ import { useEditContext } from "../client/editContext";
31
+
28
32
  export function IndexOverview() {
29
33
  const [indexStatus, setIndexStatus] = useState<IndexStatus | null>(null);
30
34
  const [isLoading, setIsLoading] = useState(false);
@@ -190,6 +194,54 @@ export function IndexOverview() {
190
194
  }
191
195
  };
192
196
 
197
+ const editContext = useEditContext();
198
+ const goToSettings = useCallback(() => {
199
+ // Ensure URL reflects the target view and panel before switching views
200
+ editContext?.updateUrl({ view: "control-center", ccpanel: "setup" });
201
+ editContext?.switchView("control-center");
202
+ }, [editContext]);
203
+
204
+
205
+ const getIndexStatusInfo = (
206
+ isCollecting: boolean,
207
+ isSubmitting: boolean,
208
+ indexStatus: IndexStatus | null,
209
+ importStatus: ImportStatus | null,
210
+ centroidsStatus: CentroidsStatus | null,
211
+ ) => {
212
+ const batches = indexStatus?.batches || [];
213
+ const inProgress = batches.some((b) => {
214
+ const s = (b.status || "").toLowerCase();
215
+ return (
216
+ s === "submitted" || s === "processing" || s === "queued"
217
+ );
218
+ });
219
+
220
+ if (isCollecting) {
221
+ return { statusLabel: "Collecting items", statusClass: "text-indigo-600", hasSettingsIssue: false};
222
+ } else if (isSubmitting || inProgress) {
223
+ return { statusLabel: "Generating Embeddings", statusClass: "text-blue-600", hasSettingsIssue: false };
224
+ } else if (importStatus?.isImporting) {
225
+ return {
226
+ statusLabel: importStatus.status || "Importing embeddings",
227
+ statusClass: "text-purple-600",
228
+ hasSettingsIssue: false
229
+ };
230
+ } else if (centroidsStatus?.isPopulating) {
231
+ return {
232
+ statusLabel: centroidsStatus.status || "Populating centroids",
233
+ statusClass: "text-pink-600",
234
+ hasSettingsIssue: false
235
+ };
236
+ } else if (indexStatus?.rebuilding) {
237
+ return { statusLabel: "Rebuilding", statusClass: "text-orange-600", hasSettingsIssue: false };
238
+ } else if (indexStatus?.messages != null && indexStatus.messages.length > 0 && indexStatus.messages[0]?.message){
239
+ return { statusLabel: indexStatus.messages[0].message, statusClass: "text-red-600", hasSettingsIssue: true };
240
+ }else {
241
+ return { statusLabel: "Ready", statusClass: "text-green-600", hasSettingsIssue: false };
242
+ }
243
+ };
244
+
193
245
  return (
194
246
  <div className="flex h-full items-stretch">
195
247
  <div className="flex min-w-96 flex-col gap-4 p-4">
@@ -375,41 +427,24 @@ export function IndexOverview() {
375
427
  <div className="rounded bg-gray-50 p-3">
376
428
  <div className="text-sm text-gray-600">Status</div>
377
429
  {(() => {
378
- const batches = indexStatus?.batches || [];
379
- const inProgress = batches.some((b) => {
380
- const s = (b.status || "").toLowerCase();
381
- return (
382
- s === "submitted" || s === "processing" || s === "queued"
383
- );
384
- });
385
- const statusLabel = isCollecting
386
- ? "Collecting items"
387
- : isSubmitting || inProgress
388
- ? "Generating Embeddings"
389
- : importStatus?.isImporting
390
- ? importStatus.status || "Importing embeddings"
391
- : centroidsStatus?.isPopulating
392
- ? centroidsStatus.status || "Populating centroids"
393
- : indexStatus.rebuilding
394
- ? "Rebuilding"
395
- : "Ready";
396
- const statusClass =
397
- statusLabel === "Rebuilding"
398
- ? "text-orange-600"
399
- : statusLabel === "Generating Embeddings"
400
- ? "text-blue-600"
401
- : statusLabel === "Collecting items"
402
- ? "text-indigo-600"
403
- : importStatus?.isImporting
404
- ? "text-purple-600"
405
- : centroidsStatus?.isPopulating
406
- ? "text-pink-600"
407
- : "text-green-600";
430
+ const { statusLabel, statusClass, hasSettingsIssue } = getIndexStatusInfo(
431
+ isCollecting,
432
+ isSubmitting,
433
+ indexStatus,
434
+ importStatus,
435
+ centroidsStatus,
436
+ );
408
437
  return (
409
438
  <div
410
- className={classNames("text-sm font-medium", statusClass)}
439
+ className={classNames("text-sm font-medium flex items-center gap-2", statusClass)}
411
440
  >
412
441
  {statusLabel}
442
+ {hasSettingsIssue && (
443
+ <Button size="sm" variant="outline" onClick={goToSettings}>
444
+ <ExternalLink className="h-4 w-4" strokeWidth={1} />
445
+ Settings
446
+ </Button>
447
+ )}
413
448
  </div>
414
449
  );
415
450
  })()}
@@ -527,7 +562,7 @@ export function IndexOverview() {
527
562
  </div>
528
563
  <div className="text-xs text-gray-500">
529
564
  {batch.created
530
- ? new Date(batch.created).toLocaleDateString()
565
+ ? formatDateOnly(new Date(batch.created))
531
566
  : "Unknown"}
532
567
  </div>
533
568
  </div>
@@ -584,7 +619,7 @@ export function IndexOverview() {
584
619
  )}
585
620
  </div>
586
621
  <div className="ml-2 shrink-0 text-xs text-gray-500">
587
- {new Date(it.created).toLocaleString()}
622
+ {formatDateTime(new Date(it.created))}
588
623
  </div>
589
624
  </div>
590
625
  ))}
@@ -0,0 +1,198 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useEditContext } from "../client/editContext";
3
+ import { Comment, SuggestedEdit } from "../../types";
4
+ import { getLatestComments } from "../services/reviewsService";
5
+ import { getLatestSuggestedEdits } from "../services/suggestedEditsService";
6
+ import { Lightbulb, MessageSquare } from "lucide-react";
7
+
8
+ type FeedbackItem =
9
+ | (Comment & { kind: "comment" })
10
+ | (SuggestedEdit & { kind: "suggestion" });
11
+
12
+ export function LatestFeedback() {
13
+ const editContext = useEditContext();
14
+ const [items, setItems] = useState<FeedbackItem[]>([]);
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ const [error, setError] = useState<string | null>(null);
17
+
18
+ // Cutoff is now computed server-side. Keep memo in case we later want client hints.
19
+ const sinceIso = useMemo(() => null, []);
20
+
21
+ const load = useCallback(async () => {
22
+ let cancelled = false;
23
+ setIsLoading(true);
24
+ setError(null);
25
+ try {
26
+ const take = 50;
27
+ const [cRes, sRes] = await Promise.all([
28
+ getLatestComments(take),
29
+ getLatestSuggestedEdits(take),
30
+ ]);
31
+
32
+ const comments = ((cRes.data || []) as Comment[]).map((c) => ({
33
+ ...(c as Comment),
34
+ kind: "comment" as const,
35
+ }));
36
+ const suggestions = ((sRes.data || []) as SuggestedEdit[]).map((s) => ({
37
+ ...(s as SuggestedEdit),
38
+ kind: "suggestion" as const,
39
+ }));
40
+
41
+ const merged = [...comments, ...suggestions].sort((a, b) => {
42
+ const ad = new Date((a as any).created || 0).getTime();
43
+ const bd = new Date((b as any).created || 0).getTime();
44
+ return bd - ad;
45
+ });
46
+
47
+ const latest = merged.slice(0, 50);
48
+ if (!cancelled) setItems(latest);
49
+ } catch (e: any) {
50
+ if (!cancelled) setError(e?.message || "Failed to load latest items");
51
+ } finally {
52
+ if (!cancelled) setIsLoading(false);
53
+ }
54
+ return () => {
55
+ cancelled = true;
56
+ };
57
+ }, [sinceIso]);
58
+
59
+ useEffect(() => {
60
+ let disposed: any;
61
+ (async () => {
62
+ disposed = await load();
63
+ })();
64
+ load();
65
+ return () => {
66
+ try {
67
+ disposed?.();
68
+ } catch {}
69
+ };
70
+ }, [load]);
71
+
72
+ useEffect(() => {
73
+ const typesToRefresh = new Set([
74
+ "comment-updated",
75
+ "comment-deleted",
76
+ "suggested-edit-updated",
77
+ "suggested-edit-deleted",
78
+ ]);
79
+ const dispose = editContext?.addSocketMessageListener?.((message: any) => {
80
+ try {
81
+ if (typesToRefresh.has(message?.type)) {
82
+ void load();
83
+ }
84
+ } catch {}
85
+ });
86
+ return () => {
87
+ try {
88
+ dispose?.();
89
+ } catch {}
90
+ };
91
+ }, [editContext?.addSocketMessageListener, load]);
92
+
93
+ const onOpenItem = async (item: FeedbackItem) => {
94
+ try {
95
+ const mainId =
96
+ item.kind === "comment" ? item.mainItemId : item.mainItemId;
97
+ const lang =
98
+ item.kind === "comment" ? item.language : item.mainItemLanguage;
99
+ const ver = item.kind === "comment" ? item.version : item.mainItemVersion;
100
+ await editContext?.loadItem({ id: mainId, language: lang, version: ver });
101
+ editContext?.setSelectedComment(
102
+ item.kind === "comment" ? (item as Comment) : undefined,
103
+ );
104
+ if (item.kind === "comment") {
105
+ editContext?.setScrollIntoView((item as Comment).itemId);
106
+ }
107
+ editContext?.switchView("comments");
108
+ } catch {}
109
+ };
110
+
111
+ const Row = ({ item }: { item: FeedbackItem }) => {
112
+ const isComment = item.kind === "comment";
113
+ const created = (item as any).created
114
+ ? new Date((item as any).created)
115
+ : undefined;
116
+ const dateText = created
117
+ ? `${created.toLocaleDateString()} ${created.toLocaleTimeString()}`
118
+ : "";
119
+ const comment = isComment ? (item as Comment) : undefined;
120
+ const pageName = isComment
121
+ ? comment?.mainItemName ||
122
+ (comment?.relatedItems || []).find(
123
+ (ri) => ri.itemId === comment?.mainItemId,
124
+ )?.itemName
125
+ : (item as SuggestedEdit).mainItemName;
126
+ const componentName = isComment
127
+ ? comment?.itemName
128
+ : (item as SuggestedEdit).itemName;
129
+ const fieldName = isComment ? comment?.fieldName : undefined;
130
+ const user = (item as any).authorDisplayName || (item as any).author || "";
131
+ const label = isComment ? "Comment" : "Suggestion";
132
+
133
+ return (
134
+ <button
135
+ className="w-full cursor-pointer rounded p-1 text-left hover:bg-gray-100"
136
+ onClick={() => onOpenItem(item)}
137
+ >
138
+ <div className="flex items-center justify-between gap-2 text-xs">
139
+ <div className="flex items-center gap-2 truncate">
140
+ <span
141
+ className="inline-flex items-center justify-center rounded py-0.5 text-gray-700"
142
+ aria-label={label}
143
+ title={label}
144
+ >
145
+ {isComment ? (
146
+ <MessageSquare className="h-3.5 w-3.5" strokeWidth={1} />
147
+ ) : (
148
+ <Lightbulb className="h-3.5 w-3.5" strokeWidth={1} />
149
+ )}
150
+ </span>
151
+ <div className="flex items-center">
152
+ {[
153
+ pageName,
154
+ componentName,
155
+ fieldName ||
156
+ (item.kind === "suggestion" ? item.fieldId : undefined),
157
+ ]
158
+ .filter((x): x is string => !!x && x.length > 0)
159
+ .map((seg, idx, arr) => (
160
+ <span key={idx} className="truncate">
161
+ {idx > 0 && (
162
+ <span className="mx-1 text-gray-400">&gt;</span>
163
+ )}
164
+ <span
165
+ className={
166
+ idx === 0
167
+ ? "font-medium text-gray-900"
168
+ : "text-gray-500"
169
+ }
170
+ >
171
+ {seg}
172
+ </span>
173
+ </span>
174
+ ))}
175
+ </div>
176
+ {user && <span className="text-gray-500"> · {user}</span>}
177
+ </div>
178
+ <div className="shrink-0 text-[10px] text-gray-500">{dateText}</div>
179
+ </div>
180
+ </button>
181
+ );
182
+ };
183
+
184
+ return (
185
+ <div className="h-full overflow-auto p-2">
186
+ {isLoading && <div className="p-2 text-xs text-gray-500">Loading…</div>}
187
+ {error && <div className="p-2 text-xs text-red-600">{error}</div>}
188
+ {!isLoading && !error && items.length === 0 && (
189
+ <div className="p-2 text-xs text-gray-500">No recent activity</div>
190
+ )}
191
+ <div className="divide-y">
192
+ {items.map((it) => (
193
+ <Row key={`${it.kind}-${(it as any).id}`} item={it} />
194
+ ))}
195
+ </div>
196
+ </div>
197
+ );
198
+ }
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
+ import { formatTime } from "../utils";
2
3
  import { useEditContext } from "../client/editContext";
3
4
 
4
5
  export function WebSocketMessages() {
@@ -34,11 +35,8 @@ export function WebSocketMessages() {
34
35
 
35
36
  const formatTimestamp = (isoString: string) => {
36
37
  const date = new Date(isoString);
37
- return (
38
- date.toLocaleTimeString() +
39
- "." +
40
- date.getMilliseconds().toString().padStart(3, "0")
41
- );
38
+ const base = formatTime(date);
39
+ return base + "." + date.getMilliseconds().toString().padStart(3, "0");
42
40
  };
43
41
 
44
42
  const getMessageTypeColor = (type: string) => {
@@ -3,6 +3,7 @@ import { Field } from "../../pageModel";
3
3
  import { EditContextType } from "../../client/editContext";
4
4
  import { getRichTextProfile } from "../../services/contentService";
5
5
  import { getCachedParsedProfile } from "./utils/profileServiceCache";
6
+ import { getComponentById } from "../../componentTreeHelper";
6
7
  import {
7
8
  RichTextEditorProfile,
8
9
  SLATE_MARKS,
@@ -82,6 +83,19 @@ export async function createRichTextContextMenu(
82
83
  const richTextField = field as RichTextField;
83
84
  const profilePath = richTextField.customProperties?.profile;
84
85
 
86
+ // Hide formatting when layout components are disabled and this field belongs to a layout component
87
+ try {
88
+ if (editContext?.showLayoutComponents === false && editContext?.page) {
89
+ const owner = getComponentById(
90
+ field.descriptor.item.id,
91
+ editContext.page,
92
+ );
93
+ if (owner?.layoutId) {
94
+ return [];
95
+ }
96
+ }
97
+ } catch {}
98
+
85
99
  // Get the iframe document
86
100
  const iframeDocument =
87
101
  editContext.pageView?.editorIframe?.contentWindow?.document;
@@ -195,7 +195,7 @@ export function MediaFolderBrowser({
195
195
  />
196
196
  </div>
197
197
  <div className="min-w-0 flex-1">
198
- <div className="truncate text-sm font-medium text-gray-900">
198
+ <div className="text-dark truncate text-xs font-medium">
199
199
  {t.name}
200
200
  </div>
201
201
  <div className="mt-1 text-xs text-gray-500">Media File</div>
@@ -305,7 +305,7 @@ export function MediaFolderBrowser({
305
305
  {/* Selected Item Info */}
306
306
  {selectedImage && (
307
307
  <div className="border-t bg-gray-50 p-3">
308
- <div className="truncate text-sm font-medium text-gray-900">
308
+ <div className="text-dark truncate text-xs font-medium">
309
309
  Selected: {selectedImage.name}
310
310
  </div>
311
311
  <div className="mt-1 text-xs text-gray-500">
@@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
9
9
  import { getItemVisitors, ItemVisitor } from "../services/editService";
10
10
  import { VerticalDotsIcon } from "../ui/Icons";
11
11
  import { AboutDialog } from "../client/AboutDialog";
12
+ import { formatDateOnly } from "../utils";
12
13
 
13
14
  type UserListItem = {
14
15
  type: "active" | "visitor";
@@ -258,7 +259,7 @@ export function ActiveUsers() {
258
259
  />
259
260
  </div>
260
261
  <div className="min-w-0 flex-1">
261
- <div className="text-sm font-medium text-gray-900">About</div>
262
+ <div className="text-dark text-xs font-medium">About</div>
262
263
  </div>
263
264
  </div>
264
265
  <div
@@ -272,7 +273,7 @@ export function ActiveUsers() {
272
273
  />
273
274
  </div>
274
275
  <div className="min-w-0 flex-1">
275
- <div className="text-sm font-medium text-gray-900">Log Out</div>
276
+ <div className="text-dark text-xs font-medium">Log Out</div>
276
277
  </div>
277
278
  </div>
278
279
  </div>
@@ -296,5 +297,5 @@ function formatTimeAgo(date: Date): string {
296
297
  if (diffInSeconds < 604800)
297
298
  return `${Math.floor(diffInSeconds / 86400)}d ago`;
298
299
 
299
- return date.toLocaleDateString();
300
+ return formatDateOnly(date);
300
301
  }
@@ -18,12 +18,11 @@ export function EditControls({
18
18
  if (!editContext) return null;
19
19
 
20
20
  // Calculate total count of comments and suggestions
21
- const commentsCount =
22
- editContext.comments?.filter((x) => !x.isResolved).length || 0;
21
+
23
22
  const suggestionsCount =
24
23
  editContext.suggestedEdits?.filter((x) => x.status === "pending").length ||
25
24
  0;
26
- const totalCount = commentsCount + suggestionsCount;
25
+ const totalCount = suggestionsCount;
27
26
 
28
27
  return (
29
28
  <>
@@ -1,7 +1,7 @@
1
1
  import { CompareIcon } from "../../ui/Icons";
2
2
  import { useEditContext } from "../../client/editContext";
3
3
  import { SimpleIconButton } from "../../ui/SimpleIconButton";
4
- import { Monitor, Smartphone, Table } from "lucide-react";
4
+ import { Monitor, Smartphone, Table, MessageSquareMore } from "lucide-react";
5
5
  import { Separator } from "../Separator";
6
6
 
7
7
  export function ViewportControls() {
@@ -12,6 +12,8 @@ export function ViewportControls() {
12
12
  const pageViewContext = editContext.pageView;
13
13
  const device = pageViewContext.device;
14
14
  const setDevice = pageViewContext.setDevice;
15
+ const commentsCount =
16
+ editContext.comments?.filter((x) => !x.isResolved).length || 0;
15
17
 
16
18
  return (
17
19
  <>
@@ -56,6 +58,22 @@ export function ViewportControls() {
56
58
  onClick={() => editContext.setCompareMode(!editContext.compareMode)}
57
59
  />
58
60
  )}
61
+ <Separator size="large" />{" "}
62
+ <SimpleIconButton
63
+ className="relative"
64
+ selected={!!editContext.showComments}
65
+ icon={<MessageSquareMore className="h-6 w-6 p-1" strokeWidth={1} />}
66
+ label={editContext.showComments ? "Hide comments" : "Show comments"}
67
+ size="large"
68
+ data-testid="toggle-comments-button"
69
+ onClick={() => editContext.setShowComments(!editContext.showComments)}
70
+ >
71
+ {commentsCount > 0 && (
72
+ <div className="bg-theme-secondary text-3xs absolute -top-1 -right-1 flex h-[16px] min-w-[16px] items-center justify-center rounded-full px-1 text-white">
73
+ {commentsCount > 99 ? "99+" : commentsCount}
74
+ </div>
75
+ )}
76
+ </SimpleIconButton>
59
77
  </>
60
78
  );
61
79
  }
@@ -136,7 +136,20 @@ export function Comment({
136
136
  className={`mb-3 cursor-pointer rounded-lg border-2 bg-white p-3 shadow-sm hover:bg-gray-50 ${
137
137
  isSelected ? "border-blue-500" : "border-transparent"
138
138
  }`}
139
- onClick={() => {
139
+ onClick={async () => {
140
+ const currentPageId = editContext?.currentItemDescriptor?.id;
141
+ if (
142
+ currentPageId &&
143
+ comment.mainItemId &&
144
+ comment.mainItemId !== currentPageId
145
+ ) {
146
+ await editContext?.loadItem({
147
+ id: comment.mainItemId,
148
+ language: comment.language,
149
+ version: comment.version,
150
+ });
151
+ }
152
+
140
153
  editContext?.setSelectedComment(comment);
141
154
  editContext?.setScrollIntoView(comment.itemId);
142
155
  editContext?.select([comment.itemId]);
@@ -161,6 +174,8 @@ export function Comment({
161
174
  canDelete={canDelete}
162
175
  canResolve={canResolve}
163
176
  availableTags={availableTags}
177
+ basePageName={editContext?.currentItemDescriptor?.name}
178
+ basePageId={editContext?.currentItemDescriptor?.id}
164
179
  onEdit={() => setIsEditing(true)}
165
180
  onDelete={handleDelete}
166
181
  onResolve={handleResolve}
@@ -16,6 +16,11 @@ export interface CommentViewProps {
16
16
  canDelete: boolean;
17
17
  canResolve: boolean;
18
18
  availableTags?: { label: string; color: string }[];
19
+ // The id of the page currently open in the editor. Used to decide when to
20
+ // show the page name for comments belonging to a different page (child page).
21
+ basePageId?: string;
22
+ // The name of the page to display in the breadcrumb
23
+ basePageName?: string;
19
24
  onEdit?: () => void;
20
25
  onDelete?: () => void;
21
26
  onResolve?: () => void;
@@ -31,6 +36,8 @@ export function CommentView({
31
36
  canDelete,
32
37
  canResolve,
33
38
  availableTags = [],
39
+ basePageId,
40
+ basePageName,
34
41
  onEdit,
35
42
  onDelete,
36
43
  onResolve,
@@ -73,36 +80,29 @@ export function CommentView({
73
80
  };
74
81
 
75
82
  const renderContextInfo = () => {
76
- const showItemName =
77
- comment.itemId && comment.mainItemId !== comment.itemId;
78
- const showFieldName = comment.fieldId;
83
+ const pageName = basePageName;
84
+ const isComponentDifferent =
85
+ !!comment.itemId && comment.mainItemId !== comment.itemId;
86
+ const componentName = isComponentDifferent ? comment.itemName : undefined;
87
+ const fieldName = comment.fieldName;
79
88
 
80
- if (!showItemName && !showFieldName) return null;
89
+ const segments = [pageName, componentName, fieldName].filter(
90
+ (x): x is string => !!x && x.length > 0,
91
+ );
92
+
93
+ if (segments.length === 0) return null;
81
94
 
82
95
  return (
83
96
  <div
84
97
  className={`${compact ? "mt-2" : "mt-3"} flex items-center border-t pt-${compact ? "2" : "3"} text-xs`}
85
98
  data-testid="comment-context-info"
86
99
  >
87
- {showItemName && (
88
- <div
89
- className="text-2xs text-gray-500"
90
- data-testid="comment-item-name"
91
- >
92
- {comment.itemName}
93
- </div>
94
- )}
95
- {showFieldName && showItemName && (
96
- <div className="text-2xs mx-2 text-gray-500">&gt;</div>
97
- )}
98
- {showFieldName && (
99
- <div
100
- className="text-2xs text-gray-500"
101
- data-testid="comment-field-name"
102
- >
103
- {comment.fieldName}
104
- </div>
105
- )}
100
+ {segments.map((seg, idx) => (
101
+ <React.Fragment key={idx}>
102
+ {idx > 0 && <div className="text-2xs mx-2 text-gray-500">&gt;</div>}
103
+ <div className="text-2xs text-gray-500">{seg}</div>
104
+ </React.Fragment>
105
+ ))}
106
106
  </div>
107
107
  );
108
108
  };
@@ -115,7 +115,7 @@ export function CommentView({
115
115
  >
116
116
  <div>
117
117
  <div
118
- className={`${compact ? "text-xs" : "text-sm"} font-bold text-gray-900`}
118
+ className={`text-dark text-xs font-medium`}
119
119
  title={comment.author}
120
120
  >
121
121
  {comment.authorDisplayName}