@alpaca-editor/core 1.0.3896 → 1.0.3898

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 (124) hide show
  1. package/dist/components/ActionButton.js +2 -2
  2. package/dist/components/ActionButton.js.map +1 -1
  3. package/dist/components/ui/button.js +3 -3
  4. package/dist/components/ui/button.js.map +1 -1
  5. package/dist/config/config.js +44 -22
  6. package/dist/config/config.js.map +1 -1
  7. package/dist/editor/FieldListField.js +1 -1
  8. package/dist/editor/FieldListField.js.map +1 -1
  9. package/dist/editor/Titlebar.js +2 -1
  10. package/dist/editor/Titlebar.js.map +1 -1
  11. package/dist/editor/client/EditorClient.d.ts +27 -2
  12. package/dist/editor/client/EditorClient.js +140 -1
  13. package/dist/editor/client/EditorClient.js.map +1 -1
  14. package/dist/editor/client/editContext.d.ts +6 -1
  15. package/dist/editor/client/editContext.js.map +1 -1
  16. package/dist/editor/client/itemsRepository.js +1 -1
  17. package/dist/editor/client/itemsRepository.js.map +1 -1
  18. package/dist/editor/client/operations.js +1 -1
  19. package/dist/editor/client/operations.js.map +1 -1
  20. package/dist/editor/control-center/About.d.ts +1 -0
  21. package/dist/editor/control-center/About.js +8 -0
  22. package/dist/editor/control-center/About.js.map +1 -0
  23. package/dist/editor/control-center/ControlCenterMenu.js +3 -0
  24. package/dist/editor/control-center/ControlCenterMenu.js.map +1 -1
  25. package/dist/editor/control-center/Info.d.ts +1 -0
  26. package/dist/editor/control-center/Info.js +10 -0
  27. package/dist/editor/control-center/Info.js.map +1 -0
  28. package/dist/editor/control-center/QuotaInfo.d.ts +1 -0
  29. package/dist/editor/control-center/QuotaInfo.js +102 -0
  30. package/dist/editor/control-center/QuotaInfo.js.map +1 -0
  31. package/dist/editor/control-center/Status.js +69 -2
  32. package/dist/editor/control-center/Status.js.map +1 -1
  33. package/dist/editor/control-center/WebSocketMessages.d.ts +1 -0
  34. package/dist/editor/control-center/WebSocketMessages.js +66 -0
  35. package/dist/editor/control-center/WebSocketMessages.js.map +1 -0
  36. package/dist/editor/page-editor-chrome/FieldActionIndicator.js +7 -6
  37. package/dist/editor/page-editor-chrome/FieldActionIndicator.js.map +1 -1
  38. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  39. package/dist/editor/services/aiService.d.ts +7 -1
  40. package/dist/editor/services/aiService.js +8 -1
  41. package/dist/editor/services/aiService.js.map +1 -1
  42. package/dist/editor/sidebar/ComponentTree.js +1 -1
  43. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  44. package/dist/editor/sidebar/ViewSelector.js +9 -4
  45. package/dist/editor/sidebar/ViewSelector.js.map +1 -1
  46. package/dist/editor/ui/Icons.d.ts +19 -1
  47. package/dist/editor/ui/Icons.js +23 -5
  48. package/dist/editor/ui/Icons.js.map +1 -1
  49. package/dist/editor/ui/SimpleMenu.js +1 -1
  50. package/dist/editor/ui/SimpleMenu.js.map +1 -1
  51. package/dist/fonts/index.d.ts +4 -0
  52. package/dist/fonts/index.js +9 -0
  53. package/dist/fonts/index.js.map +1 -0
  54. package/dist/images/wizard-bg.png +0 -0
  55. package/dist/index.d.ts +2 -1
  56. package/dist/index.js +1 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/page-wizard/WizardBox.d.ts +8 -0
  59. package/dist/page-wizard/WizardBox.js +6 -0
  60. package/dist/page-wizard/WizardBox.js.map +1 -0
  61. package/dist/page-wizard/WizardBoxConnector.d.ts +3 -0
  62. package/dist/page-wizard/WizardBoxConnector.js +6 -0
  63. package/dist/page-wizard/WizardBoxConnector.js.map +1 -0
  64. package/dist/page-wizard/WizardSteps.d.ts +4 -2
  65. package/dist/page-wizard/WizardSteps.js +44 -18
  66. package/dist/page-wizard/WizardSteps.js.map +1 -1
  67. package/dist/page-wizard/steps/CollectStep.js +16 -21
  68. package/dist/page-wizard/steps/CollectStep.js.map +1 -1
  69. package/dist/page-wizard/steps/ComponentTypesSelector.js +50 -45
  70. package/dist/page-wizard/steps/ComponentTypesSelector.js.map +1 -1
  71. package/dist/page-wizard/steps/CreatePage.js +6 -3
  72. package/dist/page-wizard/steps/CreatePage.js.map +1 -1
  73. package/dist/page-wizard/steps/CreatePageAndLayoutStep.js +21 -28
  74. package/dist/page-wizard/steps/CreatePageAndLayoutStep.js.map +1 -1
  75. package/dist/page-wizard/steps/Generate.js +27 -5
  76. package/dist/page-wizard/steps/Generate.js.map +1 -1
  77. package/dist/page-wizard/steps/ImagesStep.js +46 -44
  78. package/dist/page-wizard/steps/ImagesStep.js.map +1 -1
  79. package/dist/page-wizard/steps/SelectStep.js +11 -19
  80. package/dist/page-wizard/steps/SelectStep.js.map +1 -1
  81. package/dist/page-wizard/steps/usePageCreator.js +41 -12
  82. package/dist/page-wizard/steps/usePageCreator.js.map +1 -1
  83. package/dist/revision.d.ts +2 -2
  84. package/dist/revision.js +2 -2
  85. package/dist/styles.css +236 -120
  86. package/images/wizard-bg.png +0 -0
  87. package/package.json +1 -1
  88. package/src/components/ActionButton.tsx +6 -8
  89. package/src/components/ui/button.tsx +3 -3
  90. package/src/config/config.tsx +54 -22
  91. package/src/editor/FieldListField.tsx +2 -2
  92. package/src/editor/Titlebar.tsx +2 -1
  93. package/src/editor/client/EditorClient.tsx +192 -9
  94. package/src/editor/client/editContext.ts +12 -2
  95. package/src/editor/client/itemsRepository.ts +1 -1
  96. package/src/editor/client/operations.ts +1 -1
  97. package/src/editor/control-center/About.tsx +342 -0
  98. package/src/editor/control-center/ControlCenterMenu.tsx +5 -0
  99. package/src/editor/control-center/Info.tsx +104 -0
  100. package/src/editor/control-center/QuotaInfo.tsx +301 -0
  101. package/src/editor/control-center/Status.tsx +108 -2
  102. package/src/editor/control-center/WebSocketMessages.tsx +155 -0
  103. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +20 -5
  104. package/src/editor/page-viewer/PageViewer.tsx +1 -1
  105. package/src/editor/services/aiService.ts +17 -2
  106. package/src/editor/sidebar/ComponentTree.tsx +1 -1
  107. package/src/editor/sidebar/ViewSelector.tsx +10 -11
  108. package/src/editor/ui/Icons.tsx +146 -26
  109. package/src/editor/ui/SimpleMenu.tsx +1 -1
  110. package/src/fonts/index.ts +10 -0
  111. package/src/index.ts +7 -1
  112. package/src/page-wizard/WizardBox.tsx +40 -0
  113. package/src/page-wizard/WizardBoxConnector.tsx +21 -0
  114. package/src/page-wizard/WizardSteps.tsx +236 -116
  115. package/src/page-wizard/steps/CollectStep.tsx +129 -67
  116. package/src/page-wizard/steps/ComponentTypesSelector.tsx +32 -11
  117. package/src/page-wizard/steps/CreatePage.tsx +130 -84
  118. package/src/page-wizard/steps/CreatePageAndLayoutStep.tsx +47 -30
  119. package/src/page-wizard/steps/Generate.tsx +45 -17
  120. package/src/page-wizard/steps/ImagesStep.tsx +161 -141
  121. package/src/page-wizard/steps/SelectStep.tsx +92 -76
  122. package/src/page-wizard/steps/usePageCreator.ts +40 -14
  123. package/src/revision.ts +2 -2
  124. package/styles.css +49 -8
@@ -0,0 +1,301 @@
1
+ import React from "react";
2
+ import { useEditContext } from "../client/editContext";
3
+
4
+ export function QuotaInfo() {
5
+ const editContext = useEditContext();
6
+ const quotaInfo = editContext?.quotaInfo;
7
+
8
+ const formatQuotaPercentage = (used: number, limit: number) => {
9
+ if (!limit || limit === 0 || limit === -1) return "No limit";
10
+ if (!used && used !== 0) return "0%";
11
+ if (isNaN(used) || isNaN(limit)) return "0%";
12
+ const percentage = Math.round((used / limit) * 100);
13
+ return `${percentage}%`;
14
+ };
15
+
16
+ const getQuotaStatus = (used: number, limit: number) => {
17
+ if (!limit || limit === 0 || limit === -1) return "unlimited";
18
+ if (!used && used !== 0) return "ok";
19
+ if (isNaN(used) || isNaN(limit)) return "ok";
20
+ const percentage = (used / limit) * 100;
21
+ if (percentage >= 100) return "exceeded";
22
+ if (percentage >= 90) return "warning";
23
+ if (percentage >= 75) return "caution";
24
+ return "ok";
25
+ };
26
+
27
+ const getProgressWidth = (used: number, limit: number) => {
28
+ if (
29
+ !limit ||
30
+ limit === 0 ||
31
+ limit === -1 ||
32
+ !used ||
33
+ isNaN(used) ||
34
+ isNaN(limit)
35
+ )
36
+ return 0;
37
+ return Math.min(100, (used / limit) * 100);
38
+ };
39
+
40
+ if (!quotaInfo) {
41
+ return (
42
+ <div className="space-y-6 p-4">
43
+ <div className="rounded-lg border border-gray-200 bg-white p-4">
44
+ <h2 className="mb-3 text-xl font-semibold text-gray-800">
45
+ AI Usage Quota
46
+ </h2>
47
+ <p className="text-gray-600">No quota information available</p>
48
+ </div>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <div className="space-y-6 p-4">
55
+ {/* Token Usage Box */}
56
+ <div className="rounded-lg border border-gray-200 bg-white p-4">
57
+ <h3 className="mb-3 text-lg font-semibold text-gray-800">
58
+ Token Usage
59
+ </h3>
60
+ <div className="space-y-4">
61
+ {/* Total Token Usage */}
62
+ <div>
63
+ <div className="mb-2 flex items-center justify-between">
64
+ <label className="text-sm font-medium text-gray-500">
65
+ Total Usage
66
+ </label>
67
+ <span className="text-sm text-gray-600">
68
+ {quotaInfo.usage.totalTokens.toLocaleString()} /{" "}
69
+ {quotaInfo.limits.totalTokens &&
70
+ quotaInfo.limits.totalTokens > 0
71
+ ? quotaInfo.limits.totalTokens.toLocaleString()
72
+ : "∞"}
73
+ </span>
74
+ </div>
75
+ {quotaInfo.limits.totalTokens &&
76
+ quotaInfo.limits.totalTokens > 0 && (
77
+ <div className="h-2 w-full rounded-full bg-gray-200">
78
+ <div
79
+ className={`h-2 rounded-full ${
80
+ getQuotaStatus(
81
+ quotaInfo.usage.totalTokens,
82
+ quotaInfo.limits.totalTokens,
83
+ ) === "exceeded"
84
+ ? "bg-red-500"
85
+ : getQuotaStatus(
86
+ quotaInfo.usage.totalTokens,
87
+ quotaInfo.limits.totalTokens,
88
+ ) === "warning"
89
+ ? "bg-orange-500"
90
+ : getQuotaStatus(
91
+ quotaInfo.usage.totalTokens,
92
+ quotaInfo.limits.totalTokens,
93
+ ) === "caution"
94
+ ? "bg-yellow-500"
95
+ : "bg-green-500"
96
+ }`}
97
+ style={{
98
+ width: `${getProgressWidth(quotaInfo.usage.totalTokens, quotaInfo.limits.totalTokens)}%`,
99
+ }}
100
+ ></div>
101
+ </div>
102
+ )}
103
+ <p className="mt-1 text-xs text-gray-500">
104
+ {formatQuotaPercentage(
105
+ quotaInfo.usage.totalTokens,
106
+ quotaInfo.limits.totalTokens,
107
+ )}{" "}
108
+ used
109
+ </p>
110
+ </div>
111
+
112
+ {/* Daily Token Usage */}
113
+ <div>
114
+ <div className="mb-2 flex items-center justify-between">
115
+ <label className="text-sm font-medium text-gray-500">
116
+ Daily Usage
117
+ </label>
118
+ <span className="text-sm text-gray-600">
119
+ {quotaInfo.usage.dailyTokens.toLocaleString()} /{" "}
120
+ {quotaInfo.limits.dailyTokens &&
121
+ quotaInfo.limits.dailyTokens > 0
122
+ ? quotaInfo.limits.dailyTokens.toLocaleString()
123
+ : "∞"}
124
+ </span>
125
+ </div>
126
+ {quotaInfo.limits.dailyTokens &&
127
+ quotaInfo.limits.dailyTokens > 0 && (
128
+ <div className="h-2 w-full rounded-full bg-gray-200">
129
+ <div
130
+ className={`h-2 rounded-full ${
131
+ getQuotaStatus(
132
+ quotaInfo.usage.dailyTokens,
133
+ quotaInfo.limits.dailyTokens,
134
+ ) === "exceeded"
135
+ ? "bg-red-500"
136
+ : getQuotaStatus(
137
+ quotaInfo.usage.dailyTokens,
138
+ quotaInfo.limits.dailyTokens,
139
+ ) === "warning"
140
+ ? "bg-orange-500"
141
+ : getQuotaStatus(
142
+ quotaInfo.usage.dailyTokens,
143
+ quotaInfo.limits.dailyTokens,
144
+ ) === "caution"
145
+ ? "bg-yellow-500"
146
+ : "bg-green-500"
147
+ }`}
148
+ style={{
149
+ width: `${getProgressWidth(quotaInfo.usage.dailyTokens, quotaInfo.limits.dailyTokens)}%`,
150
+ }}
151
+ ></div>
152
+ </div>
153
+ )}
154
+ <p className="mt-1 text-xs text-gray-500">
155
+ {quotaInfo.limits.dailyTokens === -1
156
+ ? "No daily limit"
157
+ : `${formatQuotaPercentage(
158
+ quotaInfo.usage.dailyTokens,
159
+ quotaInfo.limits.dailyTokens,
160
+ )} used today`}
161
+ </p>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ {/* Image Usage Box */}
167
+ <div className="rounded-lg border border-gray-200 bg-white p-4">
168
+ <h3 className="mb-3 text-lg font-semibold text-gray-800">
169
+ Image Usage
170
+ </h3>
171
+ <div className="space-y-4">
172
+ {/* Total Image Usage */}
173
+ <div>
174
+ <div className="mb-2 flex items-center justify-between">
175
+ <label className="text-sm font-medium text-gray-500">
176
+ Total Usage
177
+ </label>
178
+ <span className="text-sm text-gray-600">
179
+ {quotaInfo.usage.totalImages.toLocaleString()} /{" "}
180
+ {quotaInfo.limits.totalImages &&
181
+ quotaInfo.limits.totalImages > 0
182
+ ? quotaInfo.limits.totalImages.toLocaleString()
183
+ : "∞"}
184
+ </span>
185
+ </div>
186
+ {quotaInfo.limits.totalImages &&
187
+ quotaInfo.limits.totalImages > 0 && (
188
+ <div className="h-2 w-full rounded-full bg-gray-200">
189
+ <div
190
+ className={`h-2 rounded-full ${
191
+ getQuotaStatus(
192
+ quotaInfo.usage.totalImages,
193
+ quotaInfo.limits.totalImages,
194
+ ) === "exceeded"
195
+ ? "bg-red-500"
196
+ : getQuotaStatus(
197
+ quotaInfo.usage.totalImages,
198
+ quotaInfo.limits.totalImages,
199
+ ) === "warning"
200
+ ? "bg-orange-500"
201
+ : getQuotaStatus(
202
+ quotaInfo.usage.totalImages,
203
+ quotaInfo.limits.totalImages,
204
+ ) === "caution"
205
+ ? "bg-yellow-500"
206
+ : "bg-green-500"
207
+ }`}
208
+ style={{
209
+ width: `${getProgressWidth(quotaInfo.usage.totalImages, quotaInfo.limits.totalImages)}%`,
210
+ }}
211
+ ></div>
212
+ </div>
213
+ )}
214
+ <p className="mt-1 text-xs text-gray-500">
215
+ {formatQuotaPercentage(
216
+ quotaInfo.usage.totalImages,
217
+ quotaInfo.limits.totalImages,
218
+ )}{" "}
219
+ used
220
+ </p>
221
+ </div>
222
+
223
+ {/* Daily Image Usage */}
224
+ <div>
225
+ <div className="mb-2 flex items-center justify-between">
226
+ <label className="text-sm font-medium text-gray-500">
227
+ Daily Usage
228
+ </label>
229
+ <span className="text-sm text-gray-600">
230
+ {quotaInfo.usage.dailyImages.toLocaleString()} /{" "}
231
+ {quotaInfo.limits.dailyImages &&
232
+ quotaInfo.limits.dailyImages > 0
233
+ ? quotaInfo.limits.dailyImages.toLocaleString()
234
+ : "∞"}
235
+ </span>
236
+ </div>
237
+ {quotaInfo.limits.dailyImages &&
238
+ quotaInfo.limits.dailyImages > 0 && (
239
+ <div className="h-2 w-full rounded-full bg-gray-200">
240
+ <div
241
+ className={`h-2 rounded-full ${
242
+ getQuotaStatus(
243
+ quotaInfo.usage.dailyImages,
244
+ quotaInfo.limits.dailyImages,
245
+ ) === "exceeded"
246
+ ? "bg-red-500"
247
+ : getQuotaStatus(
248
+ quotaInfo.usage.dailyImages,
249
+ quotaInfo.limits.dailyImages,
250
+ ) === "warning"
251
+ ? "bg-orange-500"
252
+ : getQuotaStatus(
253
+ quotaInfo.usage.dailyImages,
254
+ quotaInfo.limits.dailyImages,
255
+ ) === "caution"
256
+ ? "bg-yellow-500"
257
+ : "bg-green-500"
258
+ }`}
259
+ style={{
260
+ width: `${getProgressWidth(quotaInfo.usage.dailyImages, quotaInfo.limits.dailyImages)}%`,
261
+ }}
262
+ ></div>
263
+ </div>
264
+ )}
265
+ <p className="mt-1 text-xs text-gray-500">
266
+ {quotaInfo.limits.dailyImages === -1
267
+ ? "No daily limit"
268
+ : `${formatQuotaPercentage(
269
+ quotaInfo.usage.dailyImages,
270
+ quotaInfo.limits.dailyImages,
271
+ )} used today`}
272
+ </p>
273
+ </div>
274
+ </div>
275
+ </div>
276
+
277
+ {/* Quota Status Summary */}
278
+ <div className="rounded-lg border border-gray-200 bg-white p-4">
279
+ <h4 className="mb-2 text-sm font-medium text-gray-700">Quota Status</h4>
280
+ <div className="space-y-1 text-sm">
281
+ {editContext?.isQuotaExceeded && (
282
+ <p className="font-medium text-red-600">
283
+ ⚠️ Quota limits have been exceeded
284
+ </p>
285
+ )}
286
+ {editContext?.getQuotaWarningMessage &&
287
+ editContext.getQuotaWarningMessage() &&
288
+ !editContext.isQuotaExceeded && (
289
+ <p className="text-orange-600">
290
+ ⚠️ {editContext.getQuotaWarningMessage()}
291
+ </p>
292
+ )}
293
+ {!editContext?.isQuotaExceeded &&
294
+ !editContext?.getQuotaWarningMessage?.() && (
295
+ <p className="text-green-600">✅ All quotas are within limits</p>
296
+ )}
297
+ </div>
298
+ </div>
299
+ </div>
300
+ );
301
+ }
@@ -1,7 +1,113 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useRouter } from "next/navigation";
3
+ import { usePathname, useSearchParams } from "next/navigation";
4
+ import { useEditContext } from "../client/editContext";
5
+ import { SimpleMenu } from "../ui/SimpleMenu";
6
+ import { Splitter, SplitterPanel } from "../ui/Splitter";
7
+
1
8
  export function Status() {
9
+ const editContext = useEditContext();
10
+ const config = editContext?.configuration;
11
+ const searchParams = useSearchParams();
12
+ const urlActiveItemKey = searchParams.get("ccpanel");
13
+
14
+ // Get the first available panel as default
15
+ const defaultActiveItemKey = config?.controlCenter.groups?.flatMap(
16
+ (x) => x.panels,
17
+ )?.[0]?.id;
18
+
19
+ const [activeItemKey, setActiveItemKey] = useState<string | null>(
20
+ urlActiveItemKey || defaultActiveItemKey || null,
21
+ );
22
+
23
+ const router = useRouter();
24
+ const pathname = usePathname();
25
+
26
+ const updateUrl = (key: string | null) => {
27
+ if (urlActiveItemKey === key) return;
28
+
29
+ const current = new URLSearchParams(Array.from(searchParams.entries()));
30
+
31
+ if (key) {
32
+ current.set("ccpanel", key);
33
+ } else {
34
+ current.delete("ccpanel");
35
+ }
36
+ router.push(`${pathname}?${current.toString()}`, { scroll: false });
37
+ };
38
+
39
+ // Set default active item when config loads and no active item is set
40
+ useEffect(() => {
41
+ if (!activeItemKey && defaultActiveItemKey) {
42
+ setActiveItemKey(defaultActiveItemKey);
43
+ }
44
+ }, [defaultActiveItemKey, activeItemKey]);
45
+
46
+ const items = config?.controlCenter.groups.map((group) => {
47
+ return {
48
+ id: group.title,
49
+ label: group.title,
50
+ icon: group.icon,
51
+ items:
52
+ group.panels.map((panel) => ({
53
+ id: panel.id,
54
+ label: panel.title,
55
+ })) || [],
56
+ };
57
+ });
58
+
59
+ // Find the currently selected panel content
60
+ const selectedPanel = config?.controlCenter.groups
61
+ ?.flatMap((x) => x.panels)
62
+ ?.find((item) => item.id === activeItemKey);
63
+
64
+ if (!items) {
65
+ return (
66
+ <div className="flex h-full flex-col items-center justify-center">
67
+ Loading...
68
+ </div>
69
+ );
70
+ }
71
+
72
+ const panels: SplitterPanel[] = [
73
+ {
74
+ name: "menu",
75
+ defaultSize: 300,
76
+ content: (
77
+ <div className="h-full border-r border-gray-200">
78
+ <SimpleMenu
79
+ items={items}
80
+ activeItemKey={activeItemKey}
81
+ onItemClick={(item) => {
82
+ setActiveItemKey(item.id);
83
+ // Only update URL when user explicitly clicks on an item
84
+ updateUrl(item.id);
85
+ }}
86
+ />
87
+ </div>
88
+ ),
89
+ },
90
+ {
91
+ name: "content",
92
+ defaultSize: "auto",
93
+ content: (
94
+ <div className="absolute inset-0 overflow-auto">
95
+ {selectedPanel ? (
96
+ selectedPanel.content
97
+ ) : (
98
+ <div className="flex h-full flex-col items-center justify-center text-gray-500">
99
+ <i className="pi pi-info-circle mb-4 text-4xl"></i>
100
+ <p>Select a panel from the menu to view its content</p>
101
+ </div>
102
+ )}
103
+ </div>
104
+ ),
105
+ },
106
+ ];
107
+
2
108
  return (
3
- <div className="flex flex-col justify-center items-center h-full">
4
- Status
109
+ <div className="h-full">
110
+ <Splitter panels={panels} localStorageKey="control-center-splitter" />
5
111
  </div>
6
112
  );
7
113
  }
@@ -0,0 +1,155 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import { useEditContext } from "../client/editContext";
3
+ import { WebSocketMessage } from "../client/EditorClient";
4
+
5
+ export function WebSocketMessages() {
6
+ const editContext = useEditContext();
7
+ const [expandedMessages, setExpandedMessages] = useState<Set<string>>(
8
+ new Set(),
9
+ );
10
+ const [autoScroll, setAutoScroll] = useState(true);
11
+ const messagesEndRef = useRef<HTMLDivElement>(null);
12
+ const containerRef = useRef<HTMLDivElement>(null);
13
+
14
+ // Get messages from central store
15
+ const messages = editContext?.webSocketMessages || [];
16
+
17
+ // Auto-scroll to bottom when new messages arrive
18
+ useEffect(() => {
19
+ if (autoScroll && messagesEndRef.current) {
20
+ messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
21
+ }
22
+ }, [messages, autoScroll]);
23
+
24
+ const toggleExpanded = (messageId: string) => {
25
+ setExpandedMessages((prev) => {
26
+ const newSet = new Set(prev);
27
+ if (newSet.has(messageId)) {
28
+ newSet.delete(messageId);
29
+ } else {
30
+ newSet.add(messageId);
31
+ }
32
+ return newSet;
33
+ });
34
+ };
35
+
36
+ const formatTimestamp = (isoString: string) => {
37
+ const date = new Date(isoString);
38
+ return (
39
+ date.toLocaleTimeString() +
40
+ "." +
41
+ date.getMilliseconds().toString().padStart(3, "0")
42
+ );
43
+ };
44
+
45
+ const getMessageTypeColor = (type: string) => {
46
+ const colors: { [key: string]: string } = {
47
+ "active-sessions": "bg-blue-100 text-blue-800",
48
+ "item-deleted": "bg-red-100 text-red-800",
49
+ "item-changed": "bg-yellow-100 text-yellow-800",
50
+ "item-version-added": "bg-green-100 text-green-800",
51
+ "edit-operation": "bg-purple-100 text-purple-800",
52
+ "executing-field-action": "bg-orange-100 text-orange-800",
53
+ "comment-updated": "bg-cyan-100 text-cyan-800",
54
+ "comment-deleted": "bg-red-100 text-red-800",
55
+ "suggested-edit-updated": "bg-indigo-100 text-indigo-800",
56
+ "suggested-edit-deleted": "bg-red-100 text-red-800",
57
+ "update-quota": "bg-gray-100 text-gray-800",
58
+ };
59
+ return colors[type] || "bg-gray-100 text-gray-800";
60
+ };
61
+
62
+ const clearMessages = () => {
63
+ editContext?.clearWebSocketMessages();
64
+ };
65
+
66
+ const handleScroll = () => {
67
+ if (!containerRef.current) return;
68
+
69
+ const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
70
+ const isAtBottom = scrollHeight - scrollTop <= clientHeight + 10;
71
+ setAutoScroll(isAtBottom);
72
+ };
73
+
74
+ return (
75
+ <div className="flex h-full flex-col">
76
+ {/* Header */}
77
+ <div className="flex items-center justify-between border-b border-gray-200 p-4">
78
+ <div className="flex items-center gap-2">
79
+ <i className="pi pi-comments text-lg" />
80
+ <h3 className="text-lg font-semibold">WebSocket Messages</h3>
81
+ <span className="rounded bg-gray-100 px-2 py-1 text-sm text-gray-800">
82
+ {messages.length}
83
+ </span>
84
+ </div>
85
+ <div className="flex items-center gap-2">
86
+ <label className="flex items-center gap-1 text-sm">
87
+ <input
88
+ type="checkbox"
89
+ checked={autoScroll}
90
+ onChange={(e) => setAutoScroll(e.target.checked)}
91
+ className="rounded"
92
+ />
93
+ Auto-scroll
94
+ </label>
95
+ <button
96
+ onClick={clearMessages}
97
+ className="rounded bg-red-500 px-3 py-1 text-sm text-white hover:bg-red-600"
98
+ >
99
+ Clear
100
+ </button>
101
+ </div>
102
+ </div>
103
+
104
+ {/* Messages List */}
105
+ <div
106
+ ref={containerRef}
107
+ className="flex-1 space-y-2 overflow-y-auto p-4"
108
+ onScroll={handleScroll}
109
+ >
110
+ {messages.length === 0 ? (
111
+ <div className="py-8 text-center text-gray-500">
112
+ No WebSocket messages received yet
113
+ </div>
114
+ ) : (
115
+ messages.map((message) => (
116
+ <div key={message.id} className="rounded-lg border border-gray-200">
117
+ <div
118
+ className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50"
119
+ onClick={() => toggleExpanded(message.id)}
120
+ >
121
+ <div className="flex min-w-0 flex-1 items-center gap-3">
122
+ <span className="font-mono text-xs text-gray-500">
123
+ {formatTimestamp(message.timestamp)}
124
+ </span>
125
+ <span
126
+ className={`rounded px-2 py-1 text-xs font-medium ${getMessageTypeColor(message.type)}`}
127
+ >
128
+ {message.type}
129
+ </span>
130
+ <span className="truncate text-sm text-gray-600">
131
+ {typeof message.payload === "object"
132
+ ? Object.keys(message.payload).join(", ")
133
+ : String(message.payload)}
134
+ </span>
135
+ </div>
136
+ <i
137
+ className={`pi ${expandedMessages.has(message.id) ? "pi-chevron-up" : "pi-chevron-down"} text-gray-400`}
138
+ />
139
+ </div>
140
+
141
+ {expandedMessages.has(message.id) && (
142
+ <div className="border-t border-gray-200 bg-gray-50 p-3">
143
+ <pre className="overflow-x-auto font-mono text-xs break-words whitespace-pre-wrap">
144
+ {message.rawMessage}
145
+ </pre>
146
+ </div>
147
+ )}
148
+ </div>
149
+ ))
150
+ )}
151
+ <div ref={messagesEndRef} />
152
+ </div>
153
+ </div>
154
+ );
155
+ }
@@ -1,3 +1,4 @@
1
+ import { ProgressSpinner } from "primereact/progressspinner";
1
2
  import { FieldAction } from "../client/EditorClient";
2
3
  import { useEditContext } from "../client/editContext";
3
4
 
@@ -9,7 +10,7 @@ export function FieldActionIndicator({ action }: { action: FieldAction }) {
9
10
  const field = action.field;
10
11
  const fieldElements =
11
12
  pageViewContext.editorIframeRef.current?.contentWindow?.document.querySelectorAll(
12
- `[data-fieldid="${field.fieldId}"][data-itemid="${field.item.id}"][data-language="${field.item.language}"]`
13
+ `[data-fieldid="${field.fieldId}"][data-itemid="${field.item.id}"][data-language="${field.item.language}"][data-version="${field.item.version}"]`,
13
14
  );
14
15
 
15
16
  if (!fieldElements) return null;
@@ -19,19 +20,25 @@ export function FieldActionIndicator({ action }: { action: FieldAction }) {
19
20
  <SingleIndicator
20
21
  element={element}
21
22
  key={action.field.fieldId + action.field + index}
23
+ action={action}
22
24
  />
23
25
  ))}
24
26
  </>
25
27
  );
26
28
  }
27
29
 
28
- function SingleIndicator({ element }: { element: Element }) {
30
+ function SingleIndicator({
31
+ element,
32
+ action,
33
+ }: {
34
+ element: Element;
35
+ action: FieldAction;
36
+ }) {
29
37
  const rect = element.getBoundingClientRect();
30
-
31
38
  const indicatorRect = rect;
32
39
  return (
33
40
  <div
34
- className={`pointer-events-none absolute focus-shadow executing-action`}
41
+ className={`focus-shadow executing-action pointer-events-none absolute flex items-center justify-center bg-blue-500/20`}
35
42
  style={{
36
43
  left: indicatorRect.x,
37
44
  top: indicatorRect.y,
@@ -39,6 +46,14 @@ function SingleIndicator({ element }: { element: Element }) {
39
46
  height: indicatorRect.height,
40
47
  zIndex: 800,
41
48
  }}
42
- />
49
+ >
50
+ <div className="flex flex-col items-center justify-center gap-1.5 rounded-md bg-gray-100 p-3 text-sm font-bold text-blue-500">
51
+ <div className="flex items-center gap-1">
52
+ <ProgressSpinner style={{ width: "1rem", height: "1rem" }} />
53
+ {action.label}
54
+ </div>
55
+ <div className="text-xs">{action.message}</div>
56
+ </div>
57
+ </div>
43
58
  );
44
59
  }
@@ -5,7 +5,7 @@ import { PageViewerFrame } from "./PageViewerFrame";
5
5
  import { useEffect, useState } from "react";
6
6
  import { useRef } from "react";
7
7
  import { SimpleIconButton } from "../ui/SimpleIconButton";
8
- import { EditorMode, useEditContext } from "../client/editContext";
8
+ import { useEditContext } from "../client/editContext";
9
9
  import { useDebouncedCallback } from "use-debounce";
10
10
  import { Ellipsis, PanelLeftClose, PanelLeftOpen } from "lucide-react";
11
11
 
@@ -1,7 +1,8 @@
1
1
  import { AiContext } from "../ai/AiTerminal";
2
2
  import { EditContextType } from "../client/editContext";
3
- import { ItemDescriptor } from "../pageModel";
4
- import { ExecutionResult, post } from "./serviceHelper";
3
+ import { FieldDescriptor, ItemDescriptor } from "../pageModel";
4
+
5
+ import { ExecutionResult, get, post } from "./serviceHelper";
5
6
 
6
7
  export type AiProfile = {
7
8
  id: string;
@@ -157,3 +158,17 @@ export async function executeSearch({
157
158
 
158
159
  return { type: "success", response, data: await response.json() };
159
160
  }
161
+
162
+ export async function generateImage(
163
+ options: FieldDescriptor & {
164
+ prompt: string;
165
+ sessionId: string;
166
+ pageItem: ItemDescriptor;
167
+ },
168
+ ): Promise<ExecutionResult<any>> {
169
+ const response = await post("/alpaca/editor/ai/generateImage", options);
170
+ return response;
171
+ }
172
+ export async function requestQuota() {
173
+ await get("/alpaca/editor/ai/requestQuota");
174
+ }
@@ -474,7 +474,7 @@ export function ComponentTree({}) {
474
474
  function renderNode(node: CustomTreeNode) {
475
475
  return (
476
476
  <div>
477
- <div className="font-geist-sans flex items-center gap-2 text-[12px] text-gray-600">
477
+ <div className="flex items-center gap-2 text-[12px] text-gray-600">
478
478
  {typeof node.icon === "string" ? (
479
479
  <i className={node.icon}></i>
480
480
  ) : (