@parhelia/core 0.1.12393 → 0.1.12404

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 (99) hide show
  1. package/dist/components/ui/input.d.ts +1 -1
  2. package/dist/components/ui/input.js +5 -3
  3. package/dist/components/ui/input.js.map +1 -1
  4. package/dist/editor/FieldHistory.js +47 -17
  5. package/dist/editor/FieldHistory.js.map +1 -1
  6. package/dist/editor/PictureCropper.js +9 -4
  7. package/dist/editor/PictureCropper.js.map +1 -1
  8. package/dist/editor/PictureEditor.js +12 -13
  9. package/dist/editor/PictureEditor.js.map +1 -1
  10. package/dist/editor/ai/AgentTerminal.js +136 -23
  11. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  12. package/dist/editor/ai/AgentTerminalStatusBar.d.ts +5 -1
  13. package/dist/editor/ai/AgentTerminalStatusBar.js +182 -22
  14. package/dist/editor/ai/AgentTerminalStatusBar.js.map +1 -1
  15. package/dist/editor/ai/ContentInspectorPopover.js +105 -24
  16. package/dist/editor/ai/ContentInspectorPopover.js.map +1 -1
  17. package/dist/editor/ai/ContextInfoBar.d.ts +2 -1
  18. package/dist/editor/ai/ContextInfoBar.js +9 -6
  19. package/dist/editor/ai/ContextInfoBar.js.map +1 -1
  20. package/dist/editor/ai/agentDiagnostics.d.ts +36 -0
  21. package/dist/editor/ai/agentDiagnostics.js +120 -0
  22. package/dist/editor/ai/agentDiagnostics.js.map +1 -0
  23. package/dist/editor/ai/dialogs/QuestionnaireInline.d.ts +2 -1
  24. package/dist/editor/ai/dialogs/QuestionnaireInline.js +452 -63
  25. package/dist/editor/ai/dialogs/QuestionnaireInline.js.map +1 -1
  26. package/dist/editor/ai/dialogs/agentDialogTypes.d.ts +27 -5
  27. package/dist/editor/ai/dialogs/agentDialogTypes.js.map +1 -1
  28. package/dist/editor/client/EditorShell.js +8 -0
  29. package/dist/editor/client/EditorShell.js.map +1 -1
  30. package/dist/editor/client/editContext.d.ts +6 -0
  31. package/dist/editor/client/editContext.js.map +1 -1
  32. package/dist/editor/client/hooks/useEditorWebSocket.d.ts +3 -0
  33. package/dist/editor/client/hooks/useEditorWebSocket.js +66 -1
  34. package/dist/editor/client/hooks/useEditorWebSocket.js.map +1 -1
  35. package/dist/editor/client/socketDiagnostics.d.ts +19 -0
  36. package/dist/editor/client/socketDiagnostics.js +32 -0
  37. package/dist/editor/client/socketDiagnostics.js.map +1 -0
  38. package/dist/editor/pictureRawValue.d.ts +3 -0
  39. package/dist/editor/pictureRawValue.js +30 -0
  40. package/dist/editor/pictureRawValue.js.map +1 -0
  41. package/dist/editor/reviews/CreateReviewDetailsStep.d.ts +1 -1
  42. package/dist/editor/reviews/CreateReviewDialog.js +3 -2
  43. package/dist/editor/reviews/CreateReviewDialog.js.map +1 -1
  44. package/dist/editor/services/agentService.d.ts +57 -0
  45. package/dist/editor/services/agentService.js +30 -0
  46. package/dist/editor/services/agentService.js.map +1 -1
  47. package/dist/editor/services/aiService.d.ts +5 -0
  48. package/dist/editor/services/aiService.js.map +1 -1
  49. package/dist/editor/services/contentService.js.map +1 -1
  50. package/dist/editor/settings/panels/AgentProfileConfigPanel.js +1 -0
  51. package/dist/editor/settings/panels/AgentProfileConfigPanel.js.map +1 -1
  52. package/dist/editor/settings/panels/AgentProfileEditorPanel.d.ts +14 -0
  53. package/dist/editor/settings/panels/AgentProfileEditorPanel.js +7 -0
  54. package/dist/editor/settings/panels/AgentProfileEditorPanel.js.map +1 -0
  55. package/dist/editor/settings/panels/AgentsPanel.js +2 -2
  56. package/dist/editor/settings/panels/AgentsPanel.js.map +1 -1
  57. package/dist/editor/settings/panels/ProjectTemplatesPanel.js +132 -82
  58. package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
  59. package/dist/editor/ui/ItemCollectionEditor.d.ts +2 -1
  60. package/dist/editor/ui/ItemCollectionEditor.js +70 -27
  61. package/dist/editor/ui/ItemCollectionEditor.js.map +1 -1
  62. package/dist/editor/ui/TreeListSelector.d.ts +4 -1
  63. package/dist/editor/ui/TreeListSelector.js +2 -2
  64. package/dist/editor/ui/TreeListSelector.js.map +1 -1
  65. package/dist/revision.d.ts +2 -2
  66. package/dist/revision.js +2 -2
  67. package/dist/task-board/TaskBoardWorkspace.js +11 -18
  68. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  69. package/dist/task-board/components/ItemCollectionEditorDialog.js +28 -17
  70. package/dist/task-board/components/ItemCollectionEditorDialog.js.map +1 -1
  71. package/dist/task-board/components/ProjectDashboard.d.ts +4 -0
  72. package/dist/task-board/components/ProjectDashboard.js +1 -1
  73. package/dist/task-board/components/ProjectDashboard.js.map +1 -1
  74. package/dist/task-board/components/ProjectListContent.d.ts +1 -1
  75. package/dist/task-board/components/ProjectListContent.js +4 -1
  76. package/dist/task-board/components/ProjectListContent.js.map +1 -1
  77. package/dist/task-board/components/ProjectOverviewContent.d.ts +17 -0
  78. package/dist/task-board/components/ProjectOverviewContent.js +134 -0
  79. package/dist/task-board/components/ProjectOverviewContent.js.map +1 -0
  80. package/dist/task-board/components/ProjectSelector.d.ts +1 -1
  81. package/dist/task-board/components/ProjectSelector.js +1 -1
  82. package/dist/task-board/components/ProjectSelector.js.map +1 -1
  83. package/dist/task-board/components/TaskAssigneePicker.d.ts +3 -2
  84. package/dist/task-board/components/TaskAssigneePicker.js +10 -6
  85. package/dist/task-board/components/TaskAssigneePicker.js.map +1 -1
  86. package/dist/task-board/components/TaskDetailPanel.js +59 -9
  87. package/dist/task-board/components/TaskDetailPanel.js.map +1 -1
  88. package/dist/task-board/services/taskService.d.ts +5 -1
  89. package/dist/task-board/services/taskService.js +11 -0
  90. package/dist/task-board/services/taskService.js.map +1 -1
  91. package/dist/task-board/taskBoardNavStore.d.ts +3 -1
  92. package/dist/task-board/taskBoardNavStore.js.map +1 -1
  93. package/dist/task-board/types.d.ts +30 -0
  94. package/dist/task-board/utils/taskDependencyOrdering.d.ts +3 -0
  95. package/dist/task-board/utils/taskDependencyOrdering.js +64 -0
  96. package/dist/task-board/utils/taskDependencyOrdering.js.map +1 -0
  97. package/dist/task-board/views/WizardView.js +5 -15
  98. package/dist/task-board/views/WizardView.js.map +1 -1
  99. package/package.json +1 -1
@@ -1,43 +1,327 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback, useMemo, useEffect, useRef } from "react";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useCallback, useMemo, useEffect, useRef, } from "react";
3
3
  import { Button } from "../../../components/ui/button";
4
4
  import { Checkbox } from "../../../components/ui/checkbox";
5
5
  import { Textarea } from "../../../components/ui/textarea";
6
- import { X, ClipboardList, MessageSquare, AlertCircle } from "lucide-react";
6
+ import { X, ClipboardList, MessageSquare, AlertCircle, FileUp, Loader2, Paperclip, Trash2, } from "lucide-react";
7
+ import { ItemCollectionEditor, } from "../../ui/ItemCollectionEditor";
7
8
  import { TreeListSelector } from "../../ui/TreeListSelector";
8
9
  import { useEditContext } from "../../client/editContext";
9
10
  import { cn } from "../../../lib/utils";
11
+ import { uploadDocument } from "../../services/agentService";
12
+ import { uploadAttachmentFile } from "../../../task-board/services/taskService";
10
13
  const questionnaireDrafts = new Map();
14
+ const questionnairePendingFileDrafts = new Map();
15
+ const QUESTIONNAIRE_MAX_FILE_SIZE_MB = 10;
16
+ const QUESTIONNAIRE_MAX_FILE_SIZE_BYTES = QUESTIONNAIRE_MAX_FILE_SIZE_MB * 1024 * 1024;
17
+ const QUESTIONNAIRE_FILE_ACCEPT = ".txt,.md,.pdf,.docx,.xlsx,.pptx,.csv,.json,.xml,.cs,.js,.ts,.tsx,.jsx,.py,.java,.cpp,.c,.h,.hpp,.cshtml,.razor,.vb,.php,.rb,.go,.rs,.swift,.kt,.scala,.sql,.sh,.ps1,.bat,.yaml,.yml,.png,.jpg,.jpeg,.gif,.webp";
18
+ function isItemSelectionType(type) {
19
+ return type === "item-picker" || type === "item-collection";
20
+ }
21
+ function isFileUploadType(type) {
22
+ return type === "file-upload";
23
+ }
24
+ function createEmptyAnswer(questionId) {
25
+ return {
26
+ questionId,
27
+ selectedOptions: [],
28
+ selectedItems: [],
29
+ uploadedDocuments: [],
30
+ freetext: undefined,
31
+ };
32
+ }
33
+ function toUploadedDocumentReference(document) {
34
+ if (!document) {
35
+ return null;
36
+ }
37
+ return {
38
+ id: document.id,
39
+ agentId: document.agentId,
40
+ fileName: document.fileName,
41
+ fileSize: document.fileSize,
42
+ contentType: document.contentType,
43
+ uploadedBy: document.uploadedBy,
44
+ uploadedDate: document.uploadedDate,
45
+ };
46
+ }
47
+ function toItemCollectionItems(selectedItems, language) {
48
+ return (selectedItems ?? []).map((item) => ({
49
+ descriptor: {
50
+ id: item.id,
51
+ language,
52
+ version: 1,
53
+ name: item.name || "",
54
+ displayName: item.name || "",
55
+ path: item.path || "",
56
+ },
57
+ includeSubitems: Boolean(item.includeSubitems),
58
+ }));
59
+ }
60
+ function toSelectedItems(items) {
61
+ return items.map((item) => ({
62
+ id: item.descriptor.id,
63
+ name: item.descriptor.displayName ||
64
+ item.descriptor.name ||
65
+ item.descriptor.path ||
66
+ item.descriptor.id,
67
+ path: item.descriptor.path,
68
+ includeSubitems: item.includeSubitems,
69
+ }));
70
+ }
11
71
  function createInitialAnswers(parameters, savedAnswers) {
12
72
  const initial = new Map();
13
73
  parameters.questions.forEach((q) => {
14
- const preselectedItems = q.type === "item-picker"
74
+ const preselectedItems = isItemSelectionType(q.type)
15
75
  ? (q.preselectedItemIds ?? [])
16
- .slice(q.allowMultiple ? 0 : -1)
76
+ .slice(q.type === "item-picker" && !q.allowMultiple ? -1 : 0)
17
77
  .filter((id) => Boolean(id && id.trim()))
18
- .map((id) => ({ id }))
78
+ .map((id) => ({
79
+ id,
80
+ includeSubitems: false,
81
+ }))
19
82
  : [];
20
83
  const savedAnswer = savedAnswers?.get(q.id);
21
- const isTextQuestion = q.type !== "item-picker" && (q.options?.length ?? 0) === 0;
84
+ const isTextQuestion = !isItemSelectionType(q.type) &&
85
+ !isFileUploadType(q.type) &&
86
+ (q.options?.length ?? 0) === 0;
22
87
  initial.set(q.id, {
23
88
  questionId: q.id,
24
89
  selectedOptions: savedAnswer?.selectedOptions ?? [],
25
90
  selectedItems: savedAnswer?.selectedItems ?? preselectedItems,
91
+ uploadedDocuments: savedAnswer?.uploadedDocuments ?? [],
26
92
  freetext: savedAnswer?.freetext ?? (isTextQuestion ? q.default : undefined),
27
93
  });
28
94
  });
29
95
  return initial;
30
96
  }
97
+ function createInitialPendingFiles(parameters, savedFiles) {
98
+ const initial = new Map();
99
+ parameters.questions.forEach((question) => {
100
+ if (!isFileUploadType(question.type)) {
101
+ return;
102
+ }
103
+ initial.set(question.id, [...(savedFiles?.get(question.id) ?? [])]);
104
+ });
105
+ return initial;
106
+ }
107
+ function QuestionnaireFileUploadField({ questionId, agentId, allowMultiple, selectedFiles, submissionError, disabled, attachToTaskIfAvailable, taskId, onChange, }) {
108
+ const [isDragOver, setIsDragOver] = useState(false);
109
+ const [selectionError, setSelectionError] = useState(null);
110
+ const fileInputRef = useRef(null);
111
+ const handleFiles = useCallback((files) => {
112
+ if (!files || files.length === 0) {
113
+ return;
114
+ }
115
+ if (!agentId) {
116
+ setSelectionError("File upload is unavailable because no agent is associated with this questionnaire.");
117
+ return;
118
+ }
119
+ const nextValidFiles = [];
120
+ let nextError = null;
121
+ for (const file of Array.from(files)) {
122
+ if (file.size > QUESTIONNAIRE_MAX_FILE_SIZE_BYTES) {
123
+ nextError = `File "${file.name}" exceeds the maximum size of ${QUESTIONNAIRE_MAX_FILE_SIZE_MB} MB.`;
124
+ continue;
125
+ }
126
+ nextValidFiles.push(file);
127
+ }
128
+ if (nextValidFiles.length === 0) {
129
+ setSelectionError(nextError);
130
+ return;
131
+ }
132
+ setSelectionError(nextError);
133
+ onChange(allowMultiple
134
+ ? [...selectedFiles, ...nextValidFiles]
135
+ : nextValidFiles.slice(0, 1));
136
+ }, [agentId, allowMultiple, onChange, selectedFiles]);
137
+ const removeSelectedFile = useCallback((fileIndex) => {
138
+ onChange(selectedFiles.filter((_, index) => index !== fileIndex));
139
+ }, [onChange, selectedFiles]);
140
+ return (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { "data-testid": "questionnaire-file-upload", "data-question-id": questionId, className: cn("rounded-lg border-2 border-dashed p-3 transition-colors", isDragOver
141
+ ? "border-indigo-400 bg-indigo-50"
142
+ : "border-gray-200 bg-gray-50/70"), onDragOver: (event) => {
143
+ event.preventDefault();
144
+ event.stopPropagation();
145
+ setIsDragOver(true);
146
+ }, onDragEnter: (event) => {
147
+ event.preventDefault();
148
+ event.stopPropagation();
149
+ setIsDragOver(true);
150
+ }, onDragLeave: (event) => {
151
+ event.preventDefault();
152
+ event.stopPropagation();
153
+ setIsDragOver(false);
154
+ }, onDrop: (event) => {
155
+ event.preventDefault();
156
+ event.stopPropagation();
157
+ setIsDragOver(false);
158
+ handleFiles(event.dataTransfer.files);
159
+ }, children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(FileUp, { className: "mt-0.5 h-4 w-4 shrink-0 text-indigo-500" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-700", children: allowMultiple
160
+ ? "Drop files here or browse to attach"
161
+ : "Drop a file here or browse to attach" }), _jsxs("p", { className: "mt-0.5 text-[11px] text-gray-500", children: ["Files stay local until you submit the questionnaire. Max", " ", QUESTIONNAIRE_MAX_FILE_SIZE_MB, " MB per file.", attachToTaskIfAvailable && taskId
162
+ ? " Submitted files will also be attached to the current task."
163
+ : ""] }), _jsxs("div", { className: "mt-2", children: [_jsx("input", { ref: fileInputRef, "data-testid": "questionnaire-file-input", "data-question-id": questionId, type: "file", className: "hidden", multiple: allowMultiple, accept: QUESTIONNAIRE_FILE_ACCEPT, onChange: (event) => {
164
+ handleFiles(event.target.files);
165
+ event.target.value = "";
166
+ } }), _jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: disabled || !agentId, onClick: () => fileInputRef.current?.click(), className: "h-8 text-xs", children: allowMultiple ? "Browse Files" : "Browse File" })] })] })] }) }), (selectionError || submissionError) && (_jsx("div", { "data-testid": "questionnaire-file-upload-error", "data-question-id": questionId, className: "rounded border border-red-200 bg-red-50 px-2 py-1.5 text-[11px] text-red-700", children: selectionError || submissionError })), selectedFiles.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: selectedFiles.map((file, index) => (_jsxs("span", { "data-testid": "questionnaire-selected-file", "data-question-id": questionId, className: "inline-flex items-center gap-1.5 rounded-full border border-gray-200 bg-white px-2 py-1 text-xs text-gray-700", title: file.name, children: [_jsx(Paperclip, { className: "h-3 w-3 text-gray-500" }), _jsx("span", { className: "max-w-[220px] truncate", children: file.name }), _jsx("button", { type: "button", "data-testid": "questionnaire-remove-selected-file", "data-file-index": index, onClick: () => removeSelectedFile(index), className: "rounded p-0.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600", "aria-label": `Remove ${file.name}`, children: _jsx(Trash2, { className: "h-3 w-3" }) })] }, `${file.name}-${file.size}-${index}`))) }))] }));
167
+ }
168
+ function QuestionnaireItemCollectionField({ questionId, rootItemId, answer, language, onChange, }) {
169
+ const editContext = useEditContext();
170
+ const items = useMemo(() => toItemCollectionItems(answer?.selectedItems, language), [answer?.selectedItems, language]);
171
+ const [selectedInTree, setSelectedInTree] = useState([]);
172
+ const [selectedFromList, setSelectedFromList] = useState([]);
173
+ useEffect(() => {
174
+ setSelectedInTree([]);
175
+ setSelectedFromList([]);
176
+ }, [answer?.selectedItems]);
177
+ const commitItems = useCallback((nextItems) => {
178
+ onChange(toSelectedItems(nextItems));
179
+ }, [onChange]);
180
+ const addToList = useCallback(async (itemsToAdd) => {
181
+ const nextItems = [...items];
182
+ const sourceItems = itemsToAdd ?? selectedInTree;
183
+ const nodesToAdd = sourceItems.filter((node) => !nextItems.some((item) => item.descriptor.id === node.id));
184
+ const nodesNeedingFetch = nodesToAdd.filter((node) => !node.path || !node.name || !node.displayName);
185
+ if (nodesNeedingFetch.length > 0 && editContext?.itemsRepository) {
186
+ try {
187
+ const stubs = await editContext.itemsRepository.getItemsStubs(nodesNeedingFetch.map((node) => ({
188
+ id: node.id,
189
+ language: node.language || language,
190
+ version: node.version || 1,
191
+ })));
192
+ const stubMap = new Map(stubs.map((stub) => [stub.id, stub]));
193
+ nodesToAdd.forEach((node) => {
194
+ const stub = stubMap.get(node.id);
195
+ nextItems.push({
196
+ descriptor: {
197
+ id: node.id,
198
+ language: node.language || language,
199
+ version: node.version || 1,
200
+ name: stub?.name || node.name || "",
201
+ displayName: stub?.displayName || node.displayName || "",
202
+ path: stub?.path || node.path || node.idPath || "",
203
+ },
204
+ includeSubitems: false,
205
+ });
206
+ });
207
+ }
208
+ catch (error) {
209
+ console.error("Failed to fetch item stubs:", error);
210
+ nodesToAdd.forEach((node) => {
211
+ nextItems.push({
212
+ descriptor: {
213
+ id: node.id,
214
+ language: node.language || language,
215
+ version: node.version || 1,
216
+ name: node.name || "",
217
+ displayName: node.displayName || "",
218
+ path: node.path || node.idPath || "",
219
+ },
220
+ includeSubitems: false,
221
+ });
222
+ });
223
+ }
224
+ }
225
+ else {
226
+ nodesToAdd.forEach((node) => {
227
+ nextItems.push({
228
+ descriptor: {
229
+ id: node.id,
230
+ language: node.language || language,
231
+ version: node.version || 1,
232
+ name: node.name || "",
233
+ displayName: node.displayName || "",
234
+ path: node.path || node.idPath || "",
235
+ },
236
+ includeSubitems: false,
237
+ });
238
+ });
239
+ }
240
+ commitItems(nextItems);
241
+ setSelectedInTree([]);
242
+ setSelectedFromList([]);
243
+ }, [
244
+ items,
245
+ selectedInTree,
246
+ editContext?.itemsRepository,
247
+ language,
248
+ commitItems,
249
+ ]);
250
+ const handleAddItem = useCallback(async (item) => {
251
+ const itemId = item.id;
252
+ if (items.some((existing) => existing.descriptor.id === itemId)) {
253
+ return;
254
+ }
255
+ let itemDescriptor = {
256
+ id: itemId,
257
+ language: item.language || language,
258
+ version: item.version ?? 1,
259
+ name: item.name || "",
260
+ displayName: item.displayName || "",
261
+ path: item.path || "",
262
+ };
263
+ if ((!itemDescriptor.path ||
264
+ !itemDescriptor.name ||
265
+ !itemDescriptor.displayName) &&
266
+ editContext?.itemsRepository) {
267
+ try {
268
+ const stubs = await editContext.itemsRepository.getItemsStubs([
269
+ {
270
+ id: itemId,
271
+ language: itemDescriptor.language,
272
+ version: itemDescriptor.version,
273
+ },
274
+ ]);
275
+ if (stubs[0]) {
276
+ itemDescriptor = {
277
+ ...itemDescriptor,
278
+ name: stubs[0].name || itemDescriptor.name,
279
+ displayName: stubs[0].displayName || itemDescriptor.displayName,
280
+ path: stubs[0].path || itemDescriptor.path,
281
+ };
282
+ }
283
+ }
284
+ catch (error) {
285
+ console.error("Failed to fetch item stub:", error);
286
+ }
287
+ }
288
+ commitItems([
289
+ ...items,
290
+ {
291
+ descriptor: itemDescriptor,
292
+ includeSubitems: false,
293
+ },
294
+ ]);
295
+ setSelectedFromList([]);
296
+ }, [items, editContext?.itemsRepository, language, commitItems]);
297
+ const removeFromList = useCallback(() => {
298
+ const idsToRemove = new Set(selectedFromList.map((item) => item.descriptor.id));
299
+ commitItems(items.filter((item) => !idsToRemove.has(item.descriptor.id)));
300
+ setSelectedFromList([]);
301
+ }, [items, selectedFromList, commitItems]);
302
+ const removeItem = useCallback((index) => {
303
+ commitItems(items.filter((_, itemIndex) => itemIndex !== index));
304
+ setSelectedFromList([]);
305
+ }, [items, commitItems]);
306
+ const toggleSubitems = useCallback((index) => {
307
+ commitItems(items.map((item, itemIndex) => itemIndex === index
308
+ ? { ...item, includeSubitems: !item.includeSubitems }
309
+ : item));
310
+ setSelectedFromList([]);
311
+ }, [items, commitItems]);
312
+ return (_jsx(ItemCollectionEditor, { items: items, selectedInTree: selectedInTree, onSelectedInTreeChange: setSelectedInTree, selectedFromList: selectedFromList, onSelectedFromListChange: setSelectedFromList, onAddToList: addToList, onRemoveFromList: removeFromList, onAddItem: handleAddItem, onRemoveItem: removeItem, onToggleSubitems: toggleSubitems, language: language, rootItemIds: rootItemId ? [rootItemId] : undefined, selectedItemsLabel: "Selected Items", emptyMessage: "No items selected", emptyHint: "Browse or search to add items", height: 320, localStorageKey: `questionnaire-item-collection-${questionId}` }));
313
+ }
31
314
  /**
32
315
  * QuestionnaireInline - Inline component for displaying a questionnaire
33
316
  *
34
317
  * This is rendered directly inside the AgentTerminal rather than as a modal dialog.
35
318
  * Displays questions with multiple-choice options and optional freetext input.
36
319
  */
37
- export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Questionnaire", description, parameters, footerActions, }) {
320
+ export function QuestionnaireInline({ requestId, agentId, onClose, onCancel, title = "Questionnaire", description, parameters, footerActions, }) {
38
321
  const editContext = useEditContext();
39
322
  const language = editContext?.currentItemDescriptor?.language || "en";
40
323
  const questionsScrollAreaRef = useRef(null);
324
+ const taskId = parameters.taskId?.trim() || undefined;
41
325
  const [isHighlighted, setIsHighlighted] = useState(false);
42
326
  useEffect(() => {
43
327
  // Flash highlight on mount to grab user's attention
@@ -51,8 +335,14 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
51
335
  }, [parameters.questions.length, requestId, title]);
52
336
  // State for each question's answers: Map<questionId, { selectedOptions, freetext }>
53
337
  const [answers, setAnswers] = useState(() => createInitialAnswers(parameters, requestId ? questionnaireDrafts.get(requestId) : undefined));
338
+ const [pendingFiles, setPendingFiles] = useState(() => createInitialPendingFiles(parameters, requestId ? questionnairePendingFileDrafts.get(requestId) : undefined));
339
+ const [fileUploadErrors, setFileUploadErrors] = useState(() => new Map());
340
+ const [isSubmitting, setIsSubmitting] = useState(false);
54
341
  useEffect(() => {
55
342
  setAnswers(createInitialAnswers(parameters, requestId ? questionnaireDrafts.get(requestId) : undefined));
343
+ setPendingFiles(createInitialPendingFiles(parameters, requestId ? questionnairePendingFileDrafts.get(requestId) : undefined));
344
+ setFileUploadErrors(new Map());
345
+ setIsSubmitting(false);
56
346
  }, [parameters, requestId]);
57
347
  useEffect(() => {
58
348
  const scrollArea = questionsScrollAreaRef.current;
@@ -67,15 +357,16 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
67
357
  }
68
358
  questionnaireDrafts.set(requestId, new Map(answers));
69
359
  }, [answers, requestId]);
360
+ useEffect(() => {
361
+ if (!requestId) {
362
+ return;
363
+ }
364
+ questionnairePendingFileDrafts.set(requestId, new Map(pendingFiles));
365
+ }, [pendingFiles, requestId]);
70
366
  const updateSelectedItems = useCallback((questionId, items) => {
71
367
  setAnswers((prev) => {
72
368
  const newMap = new Map(prev);
73
- const currentAnswer = newMap.get(questionId) || {
74
- questionId,
75
- selectedOptions: [],
76
- selectedItems: [],
77
- freetext: undefined,
78
- };
369
+ const currentAnswer = newMap.get(questionId) || createEmptyAnswer(questionId);
79
370
  newMap.set(questionId, {
80
371
  ...currentAnswer,
81
372
  selectedItems: items,
@@ -86,12 +377,7 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
86
377
  const upsertSelectedItem = useCallback((questionId, item, allowMultiple) => {
87
378
  setAnswers((prev) => {
88
379
  const newMap = new Map(prev);
89
- const currentAnswer = newMap.get(questionId) || {
90
- questionId,
91
- selectedOptions: [],
92
- selectedItems: [],
93
- freetext: undefined,
94
- };
380
+ const currentAnswer = newMap.get(questionId) || createEmptyAnswer(questionId);
95
381
  const existing = currentAnswer.selectedItems ?? [];
96
382
  const next = allowMultiple
97
383
  ? existing.some((i) => i.id === item.id)
@@ -105,16 +391,26 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
105
391
  return newMap;
106
392
  });
107
393
  }, []);
394
+ const updatePendingFiles = useCallback((questionId, files) => {
395
+ setPendingFiles((prev) => {
396
+ const newMap = new Map(prev);
397
+ newMap.set(questionId, files);
398
+ return newMap;
399
+ });
400
+ setFileUploadErrors((prev) => {
401
+ if (!prev.has(questionId)) {
402
+ return prev;
403
+ }
404
+ const newMap = new Map(prev);
405
+ newMap.delete(questionId);
406
+ return newMap;
407
+ });
408
+ }, []);
108
409
  // Toggle option selection for a question
109
410
  const toggleOption = useCallback((questionId, optionId, allowMultiple) => {
110
411
  setAnswers((prev) => {
111
412
  const newMap = new Map(prev);
112
- const currentAnswer = newMap.get(questionId) || {
113
- questionId,
114
- selectedOptions: [],
115
- selectedItems: [],
116
- freetext: undefined,
117
- };
413
+ const currentAnswer = newMap.get(questionId) || createEmptyAnswer(questionId);
118
414
  let newSelectedOptions;
119
415
  if (allowMultiple) {
120
416
  // Multi-select: toggle the option
@@ -145,12 +441,7 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
145
441
  const updateFreetext = useCallback((questionId, text) => {
146
442
  setAnswers((prev) => {
147
443
  const newMap = new Map(prev);
148
- const currentAnswer = newMap.get(questionId) || {
149
- questionId,
150
- selectedOptions: [],
151
- selectedItems: [],
152
- freetext: undefined,
153
- };
444
+ const currentAnswer = newMap.get(questionId) || createEmptyAnswer(questionId);
154
445
  newMap.set(questionId, {
155
446
  ...currentAnswer,
156
447
  freetext: text || undefined,
@@ -165,12 +456,16 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
165
456
  const answer = answers.get(question.id);
166
457
  if (!answer)
167
458
  return false;
168
- if (question.type === "item-picker") {
459
+ if (isItemSelectionType(question.type)) {
169
460
  return (answer.selectedItems?.length ?? 0) > 0;
170
461
  }
462
+ if (isFileUploadType(question.type)) {
463
+ return ((pendingFiles.get(question.id)?.length ?? 0) > 0 ||
464
+ (answer.uploadedDocuments?.length ?? 0) > 0);
465
+ }
171
466
  return (answer.selectedOptions.length > 0 ||
172
467
  (answer.freetext !== undefined && answer.freetext.trim() !== ""));
173
- }, [answers]);
468
+ }, [answers, pendingFiles]);
174
469
  // Check if all required questions have been answered
175
470
  const allRequiredQuestionsAnswered = useMemo(() => {
176
471
  return parameters.questions.every((q) => q.optional || isQuestionAnswered(q));
@@ -181,13 +476,17 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
181
476
  const answer = answers.get(q.id);
182
477
  if (!answer)
183
478
  return false;
184
- if (q.type === "item-picker") {
479
+ if (isItemSelectionType(q.type)) {
185
480
  return (answer.selectedItems?.length ?? 0) > 0;
186
481
  }
482
+ if (isFileUploadType(q.type)) {
483
+ return ((pendingFiles.get(q.id)?.length ?? 0) > 0 ||
484
+ (answer.uploadedDocuments?.length ?? 0) > 0);
485
+ }
187
486
  return (answer.selectedOptions.length > 0 ||
188
487
  (answer.freetext !== undefined && answer.freetext.trim() !== ""));
189
488
  }).length;
190
- }, [parameters.questions, answers]);
489
+ }, [parameters.questions, answers, pendingFiles]);
191
490
  const requiredQuestionCount = useMemo(() => {
192
491
  return parameters.questions.filter((q) => !q.optional).length;
193
492
  }, [parameters.questions]);
@@ -195,20 +494,97 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
195
494
  return parameters.questions.filter((q) => !q.optional && isQuestionAnswered(q)).length;
196
495
  }, [parameters.questions, isQuestionAnswered]);
197
496
  // Handle submit
198
- const handleSubmit = useCallback(() => {
199
- if (requestId) {
200
- questionnaireDrafts.delete(requestId);
497
+ const handleSubmit = useCallback(async () => {
498
+ if (isSubmitting) {
499
+ return;
201
500
  }
202
- const result = {
203
- answers: Array.from(answers.values()).filter((a) => a.selectedOptions.length > 0 ||
204
- (a.selectedItems?.length ?? 0) > 0 ||
205
- (a.freetext !== undefined && a.freetext.trim() !== "")),
206
- };
207
- onClose(result);
208
- }, [answers, onClose, requestId]);
501
+ setIsSubmitting(true);
502
+ setFileUploadErrors(new Map());
503
+ const nextAnswers = [];
504
+ const nextErrors = new Map();
505
+ try {
506
+ for (const question of parameters.questions) {
507
+ const currentAnswer = answers.get(question.id) || createEmptyAnswer(question.id);
508
+ if (!isFileUploadType(question.type)) {
509
+ nextAnswers.push(currentAnswer);
510
+ continue;
511
+ }
512
+ const files = pendingFiles.get(question.id) ?? [];
513
+ if (files.length === 0) {
514
+ nextAnswers.push({
515
+ ...currentAnswer,
516
+ uploadedDocuments: currentAnswer.uploadedDocuments ?? [],
517
+ });
518
+ continue;
519
+ }
520
+ if (!agentId) {
521
+ nextErrors.set(question.id, "File upload is unavailable because no agent is associated with this questionnaire.");
522
+ continue;
523
+ }
524
+ const uploadedDocuments = [];
525
+ for (const file of files) {
526
+ const result = await uploadDocument(agentId, file);
527
+ if (!result.success || !result.document) {
528
+ nextErrors.set(question.id, result.error || `Failed to upload "${file.name}".`);
529
+ break;
530
+ }
531
+ const uploadedDocument = toUploadedDocumentReference(result.document);
532
+ if (!uploadedDocument) {
533
+ nextErrors.set(question.id, `Failed to capture upload metadata for "${file.name}".`);
534
+ break;
535
+ }
536
+ if (question.attachToTaskIfAvailable && taskId) {
537
+ const attachmentResult = await uploadAttachmentFile(taskId, file);
538
+ if (attachmentResult.type !== "success") {
539
+ nextErrors.set(question.id, attachmentResult.details ||
540
+ attachmentResult.summary ||
541
+ `Failed to attach "${file.name}" to the current task.`);
542
+ break;
543
+ }
544
+ }
545
+ uploadedDocuments.push(uploadedDocument);
546
+ }
547
+ if (nextErrors.has(question.id)) {
548
+ continue;
549
+ }
550
+ nextAnswers.push({
551
+ ...currentAnswer,
552
+ uploadedDocuments,
553
+ });
554
+ }
555
+ if (nextErrors.size > 0) {
556
+ setFileUploadErrors(nextErrors);
557
+ return;
558
+ }
559
+ if (requestId) {
560
+ questionnaireDrafts.delete(requestId);
561
+ questionnairePendingFileDrafts.delete(requestId);
562
+ }
563
+ const result = {
564
+ answers: nextAnswers.filter((answer) => answer.selectedOptions.length > 0 ||
565
+ (answer.selectedItems?.length ?? 0) > 0 ||
566
+ (answer.uploadedDocuments?.length ?? 0) > 0 ||
567
+ (answer.freetext !== undefined && answer.freetext.trim() !== "")),
568
+ };
569
+ onClose(result);
570
+ }
571
+ finally {
572
+ setIsSubmitting(false);
573
+ }
574
+ }, [
575
+ agentId,
576
+ answers,
577
+ isSubmitting,
578
+ onClose,
579
+ parameters.questions,
580
+ pendingFiles,
581
+ requestId,
582
+ taskId,
583
+ ]);
209
584
  const handleCancel = useCallback(() => {
210
585
  if (requestId) {
211
586
  questionnaireDrafts.delete(requestId);
587
+ questionnairePendingFileDrafts.delete(requestId);
212
588
  }
213
589
  onCancel();
214
590
  }, [onCancel, requestId]);
@@ -226,9 +602,13 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
226
602
  const answer = answers.get(question.id);
227
603
  const isAnswered = isQuestionAnswered(question);
228
604
  const isItemPicker = question.type === "item-picker";
229
- // Auto-enable freetext if options array is empty (defensive handling), but NOT for item-picker
605
+ const isItemCollection = question.type === "item-collection";
606
+ const isFileUpload = isFileUploadType(question.type);
607
+ const isItemSelection = isItemPicker || isItemCollection;
608
+ const isStructuredQuestion = isItemSelection || isFileUpload;
609
+ // Auto-enable freetext if options array is empty, but not for structured question types.
230
610
  const hasOptions = question.options && question.options.length > 0;
231
- const effectiveAllowFreetext = isItemPicker
611
+ const effectiveAllowFreetext = isStructuredQuestion
232
612
  ? (question.allowFreetext ?? false)
233
613
  : question.allowFreetext || !hasOptions;
234
614
  const effectiveRootItemId = question.rootItemId || question.root;
@@ -236,17 +616,26 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
236
616
  ? "border-green-200 bg-green-50/30"
237
617
  : "border-gray-200 bg-white"}`, children: [_jsxs("div", { className: "mb-2 flex items-start gap-2", children: [_jsx("span", { className: `flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold ${isAnswered
238
618
  ? "bg-green-100 text-green-700"
239
- : "bg-gray-100 text-gray-500"}`, children: index + 1 }), _jsxs("div", { className: "flex-1", children: [_jsx("p", { className: "text-sm font-medium text-gray-800", children: question.prompt }), _jsxs("p", { className: "mt-0.5 text-[10px] text-gray-400", children: [isItemPicker
240
- ? question.allowMultiple
241
- ? "Select one or more items"
242
- : "Select one item"
243
- : !hasOptions
244
- ? "Open-ended question"
245
- : question.allowMultiple
246
- ? "Select one or more options"
247
- : "Select one option", question.optional && " (optional)", effectiveAllowFreetext &&
248
- (isItemPicker || hasOptions) &&
249
- " • Freetext allowed"] })] })] }), isItemPicker && (_jsxs("div", { className: "mt-2 ml-7", children: [_jsx("div", { className: "h-[250px] overflow-hidden rounded border border-gray-200", children: _jsx(TreeListSelector, { language: language, rootItemIds: effectiveRootItemId
619
+ : "bg-gray-100 text-gray-500"}`, children: index + 1 }), _jsxs("div", { className: "flex-1", children: [_jsx("p", { className: "text-sm font-medium text-gray-800", children: question.prompt }), _jsxs("p", { className: "mt-0.5 text-[10px] text-gray-400", children: [isItemCollection
620
+ ? "Select items and choose whether to include subitems"
621
+ : isItemPicker
622
+ ? question.allowMultiple
623
+ ? "Select one or more items"
624
+ : "Select one item"
625
+ : isFileUpload
626
+ ? question.allowMultiple
627
+ ? "Upload one or more files"
628
+ : "Upload a file"
629
+ : !hasOptions
630
+ ? "Open-ended question"
631
+ : question.allowMultiple
632
+ ? "Select one or more options"
633
+ : "Select one option", isFileUpload &&
634
+ question.attachToTaskIfAvailable &&
635
+ taskId &&
636
+ " • Also attaches to task", question.optional && " (optional)", effectiveAllowFreetext &&
637
+ (isItemSelection || hasOptions) &&
638
+ " • Freetext allowed"] })] })] }), isItemCollection && (_jsx("div", { className: "mt-2 ml-7", children: _jsx(QuestionnaireItemCollectionField, { questionId: question.id, rootItemId: effectiveRootItemId, answer: answer, language: language, onChange: (items) => updateSelectedItems(question.id, items) }) })), isItemPicker && (_jsxs("div", { className: "mt-2 ml-7", children: [_jsx("div", { className: "h-[250px] overflow-hidden rounded border border-gray-200", children: _jsx(TreeListSelector, { language: language, rootItemIds: effectiveRootItemId
250
639
  ? [effectiveRootItemId]
251
640
  : undefined, selectedItemIds: (answer?.selectedItems ?? []).map((i) => i.id), selectionMode: question.allowMultiple ? "multiple" : "single", multiSelectRequiresModifier: !question.allowMultiple, onSelectionChange: (items) => {
252
641
  const effectiveItems = question.allowMultiple
@@ -263,7 +652,7 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
263
652
  upsertSelectedItem(question.id, { id, name, path: item.path }, question.allowMultiple ?? false);
264
653
  }, className: "h-full" }) }), (answer?.selectedItems?.length ?? 0) > 0 && (_jsxs("div", { className: "mt-2 flex flex-wrap gap-1", children: [(answer?.selectedItems ?? [])
265
654
  .slice(0, 8)
266
- .map((i) => (_jsx("span", { className: "rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[10px] text-gray-600", title: i.path || i.id, children: i.name || i.path || i.id }, i.id))), (answer?.selectedItems?.length ?? 0) > 8 && (_jsxs("span", { className: "px-1.5 py-0.5 text-[10px] text-gray-400", children: ["+", (answer?.selectedItems?.length ?? 0) - 8, " more"] }))] }))] })), !isItemPicker && hasOptions && (_jsx("div", { className: "ml-7 space-y-1", children: question.options.map((option) => {
655
+ .map((i) => (_jsx("span", { className: "rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[10px] text-gray-600", title: i.path || i.id, children: i.name || i.path || i.id }, i.id))), (answer?.selectedItems?.length ?? 0) > 8 && (_jsxs("span", { className: "px-1.5 py-0.5 text-[10px] text-gray-400", children: ["+", (answer?.selectedItems?.length ?? 0) - 8, " more"] }))] }))] })), isFileUpload && (_jsx("div", { className: "mt-2 ml-7", children: _jsx(QuestionnaireFileUploadField, { questionId: question.id, agentId: agentId, allowMultiple: question.allowMultiple ?? false, selectedFiles: pendingFiles.get(question.id) ?? [], submissionError: fileUploadErrors.get(question.id), disabled: isSubmitting, attachToTaskIfAvailable: question.attachToTaskIfAvailable, taskId: taskId, onChange: (files) => updatePendingFiles(question.id, files) }) })), !isStructuredQuestion && hasOptions && (_jsx("div", { className: "ml-7 space-y-1", children: question.options.map((option) => {
267
656
  const isSelected = answer?.selectedOptions.includes(option.id) ?? false;
268
657
  return (_jsxs("div", { "data-testid": "questionnaire-option", "data-option-id": option.id, "data-selected": isSelected, className: `flex cursor-pointer items-center gap-2 rounded border px-2 py-1.5 transition-colors ${isSelected
269
658
  ? "border-indigo-200 bg-indigo-50"
@@ -275,9 +664,9 @@ export function QuestionnaireInline({ requestId, onClose, onCancel, title = "Que
275
664
  : "Your response" })] }), _jsx(Textarea, { "data-testid": "questionnaire-freetext", value: answer?.freetext ?? "", onChange: (e) => updateFreetext(question.id, e.target.value), placeholder: "Enter your response...", className: "mt-1 min-h-[60px] text-xs" })] }))] }, question.id));
276
665
  }) })) }), _jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-gray-100 bg-gray-50/50 px-4 py-3", children: [footerActions, _jsx(Button, { variant: "outline", size: "sm", onClick: () => {
277
666
  handleCancel();
278
- }, "data-testid": "questionnaire-cancel", className: "h-8 text-xs", children: "Cancel" }), _jsx(Button, { size: "sm", onClick: handleSubmit, disabled: !allRequiredQuestionsAnswered, "data-testid": "questionnaire-submit", className: "h-8 text-xs", children: answeredCount === 0
279
- ? "Submit Answers"
280
- : `Submit ${answeredCount} Answer${answeredCount !== 1 ? "s" : ""}` })] })] }));
667
+ }, "data-testid": "questionnaire-cancel", className: "h-8 text-xs", children: "Cancel" }), _jsx(Button, { size: "sm", onClick: () => {
668
+ void handleSubmit();
669
+ }, disabled: !allRequiredQuestionsAnswered || isSubmitting, "data-testid": "questionnaire-submit", className: "h-8 text-xs", children: isSubmitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-1 h-3 w-3 animate-spin" }), "Submitting..."] })) : answeredCount === 0 ? ("Submit Answers") : (`Submit ${answeredCount} Answer${answeredCount !== 1 ? "s" : ""}`) })] })] }));
281
670
  }
282
671
  export default QuestionnaireInline;
283
672
  //# sourceMappingURL=QuestionnaireInline.js.map