@jini-ai/chat 0.6.6 → 0.6.7
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.
- package/CHANGELOG.md +4 -0
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +184 -97
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +31 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.js +54 -32
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.js +47 -31
- package/dist/react/components/MessageRow.usage.js +16 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +14 -0
- package/dist/react/features/chat-pane/model-options.js +45 -25
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +152 -36
- package/dist/react/features/chat-pane/types.d.ts +17 -8
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.js +19 -17
- package/dist/server/durable-runs/recover.js +132 -80
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +12 -10
|
@@ -10,25 +10,49 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
* scroll-anchoring state lives in `useMessageListAutoScroll`
|
|
11
11
|
* (`MessageList.hooks.ts`); see that file for why it is presentational.
|
|
12
12
|
*/
|
|
13
|
-
import { memo } from 'react';
|
|
13
|
+
import { memo, useMemo } from 'react';
|
|
14
|
+
import { projectAcceptedInputMessages } from '../../core/accepted-input.js';
|
|
14
15
|
import { isTerminalRunStatus } from '../../core/index.js';
|
|
15
16
|
import { useT } from '../hooks/context.js';
|
|
16
17
|
import { useMessageListAutoScroll, useConversationUsage } from './MessageList.hooks.js';
|
|
17
18
|
import { MessageRow } from './MessageRow.js';
|
|
19
|
+
import { SentAttachmentButton } from './SentAttachment.js';
|
|
20
|
+
import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
|
|
21
|
+
import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
|
|
22
|
+
function renderMessageRow({ message, isLast, isStreaming, usageSummary }, { activeQuestionFormMessageId, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, ...rowProps } = {}) {
|
|
23
|
+
const runStreaming = isLast && isStreaming && !isTerminalRunStatus({ status: message.runStatus });
|
|
24
|
+
return (_jsx(MessageRow, { message: message, ...rowProps, ...(usageSummary === undefined ? {} : { usageSummary }), runStreaming: runStreaming, runSucceeded: message.runStatus === 'succeeded', questionFormInteractive: message.id === activeQuestionFormMessageId, ...(questionFormSubmittedAnswersByMessageId?.[message.id] !== undefined ? { questionFormSubmittedAnswers: questionFormSubmittedAnswersByMessageId[message.id] } : {}), ...(onQuestionFormSubmit !== undefined ? { onQuestionFormSubmit: (text, answers) => onQuestionFormSubmit(message.id, text, answers) } : {}) }, message.id));
|
|
25
|
+
}
|
|
18
26
|
/**
|
|
19
27
|
* Memoized: the pane re-renders on every composer keystroke, and re-rendering a long transcript
|
|
20
28
|
* (hundreds of tool rows, MCP-UI cards, markdown) per key made typing lag (owner report
|
|
21
|
-
* 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `
|
|
29
|
+
* 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `pendingPrompts` and
|
|
22
30
|
* `projectFileNames` for exactly this.
|
|
23
31
|
*/
|
|
24
|
-
export const MessageList = memo(function MessageList({ messages, isStreaming = false, scrollIntent = false, onScrolled,
|
|
25
|
-
const
|
|
32
|
+
export const MessageList = memo(function MessageList({ messages: sourceMessages, isStreaming = false, scrollIntent = false, onScrolled, pendingPrompt, pendingPrompts, ...rowProps }) {
|
|
33
|
+
const messages = useMemo(() => projectAcceptedInputMessages({ messages: sourceMessages }), [sourceMessages]);
|
|
26
34
|
const usageByMessage = useConversationUsage({ messages });
|
|
27
|
-
const { containerRef, onScroll } = useMessageListAutoScroll({ messages, scrollIntent }, onScrolled !== undefined ? { onScrolled } : {});
|
|
28
|
-
return (_jsxs("div", { className: "jini-message-list", ref: containerRef, onScroll: onScroll, "data-agent-element": "chat-transcript", "data-agent-role": "list", "data-agent-label": "The conversation transcript, oldest message first", children: [messages.map((message) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const { containerRef, onScroll, awayFromLatest, jumpToLatest } = useMessageListAutoScroll({ messages, scrollIntent }, onScrolled !== undefined ? { onScrolled } : {});
|
|
36
|
+
return (_jsxs("div", { className: "jini-message-list", ref: containerRef, onScroll: onScroll, "data-agent-element": "chat-transcript", "data-agent-role": "list", "data-agent-label": "The conversation transcript, oldest message first", children: [messages.map((message) => renderMessageRow({
|
|
37
|
+
message,
|
|
38
|
+
isLast: message.id === messages[messages.length - 1]?.id,
|
|
39
|
+
isStreaming,
|
|
40
|
+
usageSummary: usageByMessage.get(message.id),
|
|
41
|
+
}, rowProps)), (pendingPrompts ?? (pendingPrompt ? [pendingPrompt] : [])).map((prompt, index) => (_jsx(QueuedMessage, { prompt: prompt, renderAttachment: rowProps.renderAttachment }, prompt.id ?? index))), awayFromLatest && _jsx(JumpToLatest, { onJump: jumpToLatest })] }));
|
|
33
42
|
});
|
|
43
|
+
/**
|
|
44
|
+
* Sticky, zero-height last child of the scroll container, so the button floats centred just above
|
|
45
|
+
* the composer without a wrapper element changing the pane's flex layout.
|
|
46
|
+
*/
|
|
47
|
+
function JumpToLatest({ onJump }) {
|
|
48
|
+
const t = useT();
|
|
49
|
+
return (_jsx("div", { className: "jini-message-list__jump", children: _jsxs("button", { type: "button", className: "jini-message-list__jump-btn", onClick: onJump, children: [_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 5v14" }), _jsx("path", { d: "m19 12-7 7-7-7" })] }), t('Jump to latest')] }) }));
|
|
50
|
+
}
|
|
51
|
+
/** One cancelable queue receipt; attachment rendering reuses the sent-message preview owner. */
|
|
52
|
+
function QueuedMessage({ prompt, renderAttachment }) {
|
|
53
|
+
const t = useT();
|
|
54
|
+
const attachments = prompt.attachments ?? [];
|
|
55
|
+
const gallery = useAttachmentGallery({ attachments });
|
|
56
|
+
return (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { children: t('Queued — sends when this reply finishes') }), _jsx("div", { className: "jini-chat-pane__queued-text", children: prompt.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open, renderAttachment: renderAttachment }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: () => prompt.onCancel(), title: t('Cancel queued message'), children: t('Cancel') }) })] }));
|
|
57
|
+
}
|
|
34
58
|
//# sourceMappingURL=MessageList.js.map
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { projectAcceptedInputEvents } from '../../core/accepted-input.js';
|
|
3
|
+
import { MidRunUserMessage, MID_RUN_USER_MESSAGE_EXT_EVENT_NAME } from './MidRunUserMessage.js';
|
|
2
4
|
import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
|
|
3
5
|
import { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
|
|
4
6
|
/**
|
|
@@ -51,7 +53,7 @@ import { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary }
|
|
|
51
53
|
* not appear until the whole message is done, not merely until its current
|
|
52
54
|
* chunk stopped arriving.
|
|
53
55
|
*/
|
|
54
|
-
import React, { memo } from 'react';
|
|
56
|
+
import React, { memo, useMemo } from 'react';
|
|
55
57
|
import { isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
|
|
56
58
|
import { useToolTimeline } from '../hooks/useToolTimeline.js';
|
|
57
59
|
import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
|
|
@@ -164,24 +166,31 @@ const NO_ATTACHMENTS = [];
|
|
|
164
166
|
* re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
|
|
165
167
|
* are replaced, never mutated, so identity is a sound change signal.
|
|
166
168
|
*/
|
|
167
|
-
export const MessageRow = memo(function MessageRow({
|
|
169
|
+
export const MessageRow = memo(function MessageRow({ showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, runStreaming = false, runSucceeded = false, questionFormInteractive = false, ...props }) {
|
|
170
|
+
const message = useMemo(() => props.message.events === undefined ? props.message : {
|
|
171
|
+
...props.message, events: projectAcceptedInputEvents({ events: props.message.events }),
|
|
172
|
+
}, [props.message]);
|
|
173
|
+
if (message.role === 'user')
|
|
174
|
+
return _jsx(UserMessageRow, { ...props, message: message });
|
|
175
|
+
return _jsx(AssistantMessageRow, { ...props, message: message, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSucceeded, questionFormInteractive });
|
|
176
|
+
});
|
|
177
|
+
function UserMessageRow({ message, renderAttachment }) {
|
|
168
178
|
const t = useT();
|
|
169
|
-
const registry = useExtEventRendererRegistry();
|
|
170
|
-
const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
|
|
171
|
-
const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
|
|
172
|
-
const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => registry.slot({ name, data }) });
|
|
173
|
-
const answeredCards = useAnsweredCards({ slotOf: registry.slot });
|
|
174
179
|
// Which attachment chip (if any) this row's preview modal is open for. Row-local, not
|
|
175
180
|
// module-level: two attachments in the same message can never be "open" at once, but two
|
|
176
181
|
// different messages opening their own attachments independently is fine and expected.
|
|
177
182
|
// Previous/next steps through this message's images only.
|
|
178
183
|
const gallery = useAttachmentGallery({ attachments: message.attachments ?? NO_ATTACHMENTS });
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [message.attachments && message.attachments.length > 0 ? (_jsx("div", { className: "jini-message-attachments", children: message.attachments.map((a) => (_jsx(SentAttachmentButton, { attachment: a, onOpen: gallery.open, renderAttachment: renderAttachment }, a.path))) })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), gallery.opened ? (_jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })) : null] }));
|
|
185
|
+
}
|
|
186
|
+
function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
|
|
187
|
+
const registry = useExtEventRendererRegistry();
|
|
188
|
+
const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
|
|
189
|
+
const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
|
|
190
|
+
const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => registry.slot({ name, data }) });
|
|
191
|
+
const answeredCards = useAnsweredCards({ slotOf: registry.slot, events: message.events ?? [], groups: extGroups, runStreaming, runStatus: message.runStatus });
|
|
182
192
|
const terminalNotice = terminalMessageNotice({ message }, {});
|
|
183
193
|
const visibleContent = stripArtifact({ content: message.content });
|
|
184
|
-
const segments = splitOnQuestionForms({ input: visibleContent });
|
|
185
194
|
const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows, registry }, { toolCallDisplay, includeThinking: showThinking });
|
|
186
195
|
const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
|
|
187
196
|
const runInProgress = isRunInProgress(message.runStatus);
|
|
@@ -196,18 +205,24 @@ export const MessageRow = memo(function MessageRow({ message, usageSummary, show
|
|
|
196
205
|
const flatToolRows = toolRowsForDisplay({ rows: timeline.rows, mode: toolCallDisplay });
|
|
197
206
|
const renderExtGroup = (group) => {
|
|
198
207
|
const renderer = registry.get({ name: group.name });
|
|
208
|
+
if (!renderer && group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME)
|
|
209
|
+
return _jsx(MidRunUserMessage, { events: group.events });
|
|
199
210
|
if (!renderer)
|
|
200
211
|
return null;
|
|
212
|
+
const answerText = answeredCards.answerText(group.slot);
|
|
201
213
|
const node = renderer({
|
|
202
214
|
name: group.name,
|
|
203
215
|
events: group.events,
|
|
204
|
-
runStreaming,
|
|
216
|
+
runStreaming: answeredCards.runStreaming,
|
|
205
217
|
runSucceeded,
|
|
206
218
|
awaitingAnswer,
|
|
207
219
|
runId: message.runId,
|
|
208
220
|
...(group.call === undefined ? {} : { call: group.call }),
|
|
209
221
|
answered: answeredCards.isAnswered(group.slot),
|
|
210
222
|
onAnswered: () => answeredCards.markAnswered(group.slot),
|
|
223
|
+
onAnswerText: (text) => answeredCards.markAnswered(group.slot, text),
|
|
224
|
+
...(answeredCards.isRecorded(group.slot) ? { answerRecorded: true } : {}),
|
|
225
|
+
...(answerText === undefined ? {} : { answerText }),
|
|
211
226
|
});
|
|
212
227
|
if (!node)
|
|
213
228
|
return null;
|
|
@@ -218,28 +233,29 @@ export const MessageRow = memo(function MessageRow({ message, usageSummary, show
|
|
|
218
233
|
_jsx(ExtEventErrorBoundary, { name: group.name, children: _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
|
|
219
234
|
};
|
|
220
235
|
const renderTextSegments = (text, keyPrefix) => splitOnQuestionForms({ input: text }).map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, `${keyPrefix}-${i}`)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, `${keyPrefix}-${i}`)));
|
|
236
|
+
const renderBlock = (block) => {
|
|
237
|
+
if (block.kind === 'text') {
|
|
238
|
+
return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
|
|
239
|
+
}
|
|
240
|
+
if (block.kind === 'reply-end') {
|
|
241
|
+
const reply = usage.replies[block.index];
|
|
242
|
+
return reply && showUsageLine ? _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal }, block.key) : null;
|
|
243
|
+
}
|
|
244
|
+
if (block.kind === 'tools')
|
|
245
|
+
return renderTools(block.rows, block.key);
|
|
246
|
+
if (block.kind === 'thinking')
|
|
247
|
+
return _jsx(ThinkingBlock, { text: block.text }, block.key);
|
|
248
|
+
const group = extGroups.find((g) => g.slot === block.slot);
|
|
249
|
+
if (!group)
|
|
250
|
+
return null;
|
|
251
|
+
return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
|
|
252
|
+
};
|
|
221
253
|
return (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress }, { showTotal: showUsageTotal })
|
|
222
254
|
? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
|
|
223
255
|
: 'jini-message jini-message-assistant', "data-message-id": message.id, "data-run-status": message.runStatus, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A reply from the assistant", children: [message.agentName ? _jsx("div", { className: "jini-message-agent", children: message.agentName }) : null, blocks
|
|
224
|
-
? blocks.map(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
if (block.kind === 'reply-end') {
|
|
229
|
-
const reply = usage.replies[block.index];
|
|
230
|
-
return reply && showUsageLine ? _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal }, block.key) : null;
|
|
231
|
-
}
|
|
232
|
-
if (block.kind === 'tools')
|
|
233
|
-
return renderTools(block.rows, block.key);
|
|
234
|
-
if (block.kind === 'thinking')
|
|
235
|
-
return _jsx(ThinkingBlock, { text: block.text }, block.key);
|
|
236
|
-
const group = extGroups.find((g) => g.slot === block.slot);
|
|
237
|
-
if (!group)
|
|
238
|
-
return null;
|
|
239
|
-
return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
|
|
240
|
-
})
|
|
241
|
-
: (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))), flatToolRows.length > 0 ? renderTools(flatToolRows) : null, extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })), _jsx(AssistantMessageFooter, { message, usageEvent: footerUsage, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }));
|
|
242
|
-
});
|
|
256
|
+
? blocks.map(renderBlock)
|
|
257
|
+
: (_jsxs(_Fragment, { children: [renderTextSegments(visibleContent, 'text'), flatToolRows.length > 0 ? renderTools(flatToolRows) : null, extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })), _jsx(AssistantMessageFooter, { message, usageEvent: footerUsage, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }));
|
|
258
|
+
}
|
|
243
259
|
/**
|
|
244
260
|
* The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
|
|
245
261
|
* old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
|
|
@@ -33,19 +33,24 @@ export function usageLineMeetsActions({ footer, terminalNotice, pending, runInPr
|
|
|
33
33
|
* the current turn's cost is unknown. `showTotal: false` turns the total off. */
|
|
34
34
|
export function useUsageSummary({ usage }, { showTotal = true } = {}) {
|
|
35
35
|
const t = useT();
|
|
36
|
-
const parts = []
|
|
37
|
-
|
|
38
|
-
const seconds = Math.round(usage.durationMs / 1000);
|
|
39
|
-
parts.push(seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`);
|
|
40
|
-
}
|
|
41
|
-
if (usage.costUsd !== undefined) {
|
|
42
|
-
parts.push(formatUsageCost({ costUsd: usage.costUsd, ...(showTotal ? { totalCostUsd: usage.totalCostUsd ?? usage.costUsd } : {}) }));
|
|
43
|
-
}
|
|
44
|
-
else if (showTotal && usage.totalCostUsd !== undefined) {
|
|
45
|
-
parts.push(`total ${formatUsageCost({ costUsd: usage.totalCostUsd })}`);
|
|
46
|
-
}
|
|
36
|
+
const parts = [usageDuration({ durationMs: usage.durationMs }), usageCost({ usage }, { showTotal })]
|
|
37
|
+
.filter((part) => part !== undefined);
|
|
47
38
|
const text = parts.join(' · ');
|
|
48
39
|
const title = usage.outputTokens === undefined ? undefined : t('{n} out', { n: usage.outputTokens });
|
|
49
40
|
return { text, title, ariaLabel: title === undefined ? undefined : `${t('Done')} · ${text} · ${title}` };
|
|
50
41
|
}
|
|
42
|
+
function usageDuration({ durationMs }, _options = {}) {
|
|
43
|
+
if (durationMs === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
const seconds = Math.round(durationMs / 1000);
|
|
46
|
+
return seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
|
|
47
|
+
}
|
|
48
|
+
function usageCost({ usage }, { showTotal = true } = {}) {
|
|
49
|
+
if (usage.costUsd !== undefined) {
|
|
50
|
+
return formatUsageCost({ costUsd: usage.costUsd, ...(showTotal ? { totalCostUsd: usage.totalCostUsd ?? usage.costUsd } : {}) });
|
|
51
|
+
}
|
|
52
|
+
if (showTotal && usage.totalCostUsd !== undefined)
|
|
53
|
+
return `total ${formatUsageCost({ costUsd: usage.totalCostUsd })}`;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
51
56
|
//# sourceMappingURL=MessageRow.usage.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* @module MidRunUserMessage
|
|
4
4
|
*
|
|
@@ -10,6 +10,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
* It is an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
|
|
11
11
|
* into the turn it was already running — there is no separate reply to hang off it.
|
|
12
12
|
*/
|
|
13
|
+
import { acceptedInputEvent } from '../../core/accepted-input.js';
|
|
14
|
+
import { SentAttachmentButton } from './SentAttachment.js';
|
|
15
|
+
import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
|
|
16
|
+
import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
|
|
13
17
|
import { MID_RUN_USER_MESSAGE_EVENT_NAME } from '../../core/turn-usage.js';
|
|
14
18
|
import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
|
|
15
19
|
/** The ext-event name the daemon's mid-run `user_message` run event arrives under. One source with
|
|
@@ -25,10 +29,19 @@ export function midRunUserMessageSlotKey({ data }) {
|
|
|
25
29
|
}
|
|
26
30
|
/** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
|
|
27
31
|
export function MidRunUserMessage({ events }) {
|
|
28
|
-
const
|
|
32
|
+
const data = events.at(-1);
|
|
33
|
+
const text = field(data, 'text');
|
|
29
34
|
if (text === undefined)
|
|
30
35
|
return null;
|
|
31
|
-
|
|
36
|
+
const receipt = acceptedInputEvent({ event: { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME, data } });
|
|
37
|
+
// MOVE: A single live delivery may contain several queued inputs; display their original receipts.
|
|
38
|
+
const inputs = receipt?.data.inputs ?? [{ id: field(data, 'id') ?? 'mid-run', text }];
|
|
39
|
+
return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input }, input.id)) });
|
|
40
|
+
}
|
|
41
|
+
function MidRunInput({ input }) {
|
|
42
|
+
const attachments = input.attachments ?? [];
|
|
43
|
+
const gallery = useAttachmentGallery({ attachments });
|
|
44
|
+
return (_jsxs("div", { className: "jini-message jini-message-user jini-message-user--mid-run", "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [_jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
32
45
|
}
|
|
33
46
|
/**
|
|
34
47
|
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { CREDENTIAL_CARD_GUIDANCE, SECRET_REDACTED_NOTICE } from '../../core/user-text-redaction.js';
|
|
3
3
|
import { useUserTextGuard } from '../hooks/useUserTextGuard.js';
|
|
4
4
|
/**
|
|
@@ -21,34 +21,46 @@ export const QuestionForm = forwardRef(function QuestionForm({ form: incomingFor
|
|
|
21
21
|
const t = useT();
|
|
22
22
|
const form = useMemo(() => withoutSecretQuestionDefaults({ form: incomingForm }), [incomingForm]);
|
|
23
23
|
const guard = useUserTextGuard({ ...(redactUserText ? { redactUserText } : {}), ...(onSecretRedacted ? { onSecretRedacted } : {}) });
|
|
24
|
+
const locked = !interactive || !onSubmit || submittedAnswers !== undefined;
|
|
25
|
+
const state = useQuestionAnswers({ form, submittedAnswers, draftAnswers, locked }, { onDraftChange, onAnswerChange });
|
|
26
|
+
const currentAnswers = submittedAnswers ? redactQuestionAnswers({ form, answers: submittedAnswers }).answers : state.answers;
|
|
27
|
+
const ready = answersReady(form, currentAnswers);
|
|
28
|
+
function handleSubmit() {
|
|
29
|
+
if (locked || !onSubmit)
|
|
30
|
+
return;
|
|
31
|
+
if (!ready)
|
|
32
|
+
return;
|
|
33
|
+
const safe = redactQuestionAnswers({ form, answers: state.answers }, { ...(redactUserText ? { redactUserText } : {}), onSecretRedacted: guard.notifyRedaction });
|
|
34
|
+
state.setAnswers(safe.answers);
|
|
35
|
+
const files = collectFileSubmissions(form, state.fileAnswers);
|
|
36
|
+
if (files.length > 0)
|
|
37
|
+
onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers, files);
|
|
38
|
+
else
|
|
39
|
+
onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers);
|
|
40
|
+
}
|
|
41
|
+
function handleSkipAll() {
|
|
42
|
+
if (locked || !onSubmit)
|
|
43
|
+
return;
|
|
44
|
+
const empty = {};
|
|
45
|
+
onSubmit(formatFormAnswers({ form: form, answers: empty }), empty);
|
|
46
|
+
}
|
|
47
|
+
useImperativeHandle(ref, () => ({ submit: handleSubmit, skipAll: handleSkipAll }));
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
onReadyChange?.(!locked && ready);
|
|
50
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
+
}, [onReadyChange, locked, ready]);
|
|
52
|
+
return (_jsxs("div", { className: `question-form${locked ? ' question-form-locked' : ''}`, "data-form-id": form.id, children: [_jsx(QuestionFormHead, { form: form, locked: locked }), _jsx("div", { className: "question-form-body", children: form.questions.map((q) => (_jsx(QuestionField, { q: q, value: currentAnswers[q.id], formId: form.id, locked: locked, t: t, ...state.handlers }, q.id))) }), guard.secretRedacted ? _jsx("div", { role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, hideInternalSubmit ? null : _jsx(QuestionFormFoot, { locked: locked, answered: submittedAnswers !== undefined, ready: ready, submitLabel: form.submitLabel, onSubmit: handleSubmit })] }));
|
|
53
|
+
});
|
|
54
|
+
/** Owns the editable answers plus the picked files; every edit is a no-op once the form is locked. */
|
|
55
|
+
function useQuestionAnswers({ form, submittedAnswers, draftAnswers, locked }, { onDraftChange, onAnswerChange } = {}) {
|
|
24
56
|
const initial = useMemo(() => buildInitialState(form, submittedAnswers, draftAnswers), [form, submittedAnswers, draftAnswers]);
|
|
25
57
|
const [answers, setAnswers] = useState(initial);
|
|
26
58
|
const [fileAnswers, setFileAnswers] = useState({});
|
|
27
|
-
const locked = !interactive || !onSubmit || submittedAnswers !== undefined;
|
|
28
|
-
const currentAnswers = submittedAnswers ? redactQuestionAnswers({ form, answers: submittedAnswers }).answers : answers;
|
|
29
59
|
useEffect(() => {
|
|
30
60
|
setFileAnswers({});
|
|
31
61
|
}, [form.id]);
|
|
32
62
|
useEffect(() => {
|
|
33
|
-
setAnswers((prev) =>
|
|
34
|
-
let changed = false;
|
|
35
|
-
const next = { ...prev };
|
|
36
|
-
for (const q of form.questions) {
|
|
37
|
-
if (next[q.id] !== undefined)
|
|
38
|
-
continue;
|
|
39
|
-
changed = true;
|
|
40
|
-
if (submittedAnswers && submittedAnswers[q.id] !== undefined) {
|
|
41
|
-
next[q.id] = canonicalizeQuestionValue(q, submittedAnswers[q.id]);
|
|
42
|
-
}
|
|
43
|
-
else if (q.defaultValue !== undefined) {
|
|
44
|
-
next[q.id] = canonicalizeQuestionValue(q, q.defaultValue);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
next[q.id] = emptyQuestionValue(q);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return changed ? next : prev;
|
|
51
|
-
});
|
|
63
|
+
setAnswers((prev) => backfillAnswers(prev, form, submittedAnswers));
|
|
52
64
|
}, [form, submittedAnswers]);
|
|
53
65
|
function update(id, value) {
|
|
54
66
|
if (locked)
|
|
@@ -78,60 +90,111 @@ export const QuestionForm = forwardRef(function QuestionForm({ form: incomingFor
|
|
|
78
90
|
const fixed = current.filter((entry) => questionValueIsKnown(q, entry));
|
|
79
91
|
update(q.id, [...fixed, ...splitCustomEntries(raw)]);
|
|
80
92
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const safe = redactQuestionAnswers({ form, answers }, { ...(redactUserText ? { redactUserText } : {}), onSecretRedacted: guard.notifyRedaction });
|
|
87
|
-
setAnswers(safe.answers);
|
|
88
|
-
const files = collectFileSubmissions(form, fileAnswers);
|
|
89
|
-
if (files.length > 0)
|
|
90
|
-
onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers, files);
|
|
91
|
-
else
|
|
92
|
-
onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers);
|
|
93
|
-
}
|
|
94
|
-
function handleSkipAll() {
|
|
95
|
-
if (locked || !onSubmit)
|
|
96
|
-
return;
|
|
97
|
-
const empty = {};
|
|
98
|
-
onSubmit(formatFormAnswers({ form: form, answers: empty }), empty);
|
|
93
|
+
function selectFiles(q, list) {
|
|
94
|
+
const files = Array.from(list ?? []);
|
|
95
|
+
const names = files.map((file) => file.name);
|
|
96
|
+
setFileAnswers((current) => ({ ...current, [q.id]: files }));
|
|
97
|
+
update(q.id, q.multiple ? names : (names[0] ?? ''));
|
|
99
98
|
}
|
|
99
|
+
return { answers, setAnswers, fileAnswers, handlers: { update, toggleCheckbox, updateCheckboxCustom, selectFiles } };
|
|
100
|
+
}
|
|
101
|
+
function answersReady(form, answers) {
|
|
100
102
|
const withinSelectionLimits = form.questions.every((q) => {
|
|
101
103
|
if (q.type !== 'checkbox' || q.maxSelections === undefined)
|
|
102
104
|
return true;
|
|
103
|
-
const v =
|
|
105
|
+
const v = answers[q.id];
|
|
104
106
|
return !Array.isArray(v) || v.length <= q.maxSelections;
|
|
105
107
|
});
|
|
106
108
|
const requiredAnswered = form.questions.every((q) => {
|
|
107
109
|
if (q.required !== true)
|
|
108
110
|
return true;
|
|
109
|
-
const v =
|
|
111
|
+
const v = answers[q.id];
|
|
110
112
|
if (Array.isArray(v))
|
|
111
113
|
return v.length > 0;
|
|
112
114
|
return typeof v === 'string' && v.trim().length > 0;
|
|
113
115
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
116
|
+
return withinSelectionLimits && requiredAnswered;
|
|
117
|
+
}
|
|
118
|
+
function QuestionFormHead({ form, locked }) {
|
|
119
|
+
const t = useT();
|
|
120
|
+
return (_jsxs("div", { className: "question-form-head", children: [_jsx("span", { className: "question-form-icon", "aria-hidden": true, children: "?" }), _jsxs("div", { className: "question-form-titles", children: [_jsx("div", { className: "question-form-title", children: form.title }), form.description ? _jsx("div", { className: "question-form-desc", children: form.description }) : null] }), locked ? _jsx("span", { className: "question-form-pill", children: t('Answered') }) : null] }));
|
|
121
|
+
}
|
|
122
|
+
function QuestionFormFoot({ locked, answered, ready, submitLabel, onSubmit }) {
|
|
123
|
+
const t = useT();
|
|
124
|
+
return (_jsxs("div", { className: "question-form-foot", children: [locked ? _jsx("span", { className: "qf-locked-note", children: answered ? t('You answered this') : t('A newer message answered this') }) : _jsx("span", { className: "qf-hint", children: t('Answer above, then continue') }), !locked ? (_jsx("button", { type: "button", className: "primary", onClick: onSubmit, disabled: !ready, title: ready ? t('Submit your answers') : t('Answer the required questions first'), children: submitLabel ?? t('Continue') })) : null] }));
|
|
125
|
+
}
|
|
126
|
+
function QuestionField(ctx) {
|
|
127
|
+
const { q, t } = ctx;
|
|
128
|
+
const renderControl = QUESTION_CONTROLS[q.type];
|
|
129
|
+
return (_jsxs("div", { className: "qf-field", children: [_jsxs("label", { className: "qf-label", children: [_jsx("span", { children: q.label }), q.required ? (_jsx("span", { className: "qf-required", "aria-label": t('required'), children: "*" })) : null] }), q.help ? _jsx("div", { className: "qf-help", children: q.help }) : null, isSecretQuestion({ question: q }) ? _jsx("div", { className: "qf-help", children: t(CREDENTIAL_CARD_GUIDANCE) }) : null, renderControl ? renderControl(ctx) : null] }));
|
|
130
|
+
}
|
|
131
|
+
function stringValue(value) {
|
|
132
|
+
return typeof value === 'string' ? value : '';
|
|
133
|
+
}
|
|
134
|
+
/** A range with no usable answer yet (e.g. mid-stream) shows its minimum, matching `emptyQuestionValue`. */
|
|
135
|
+
function rangeValue(q, value) {
|
|
136
|
+
return typeof value === 'string' && value.trim() ? value : String(q.min ?? 0);
|
|
137
|
+
}
|
|
138
|
+
function customChoice({ q, t, locked }, value, onChange) {
|
|
139
|
+
if (!shouldRenderCustomChoice(q))
|
|
140
|
+
return null;
|
|
141
|
+
return _jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: value, placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: onChange });
|
|
142
|
+
}
|
|
143
|
+
function singleCustomChoice(ctx) {
|
|
144
|
+
return customChoice(ctx, customSingleValue(ctx.q, ctx.value), (next) => ctx.update(ctx.q.id, next));
|
|
145
|
+
}
|
|
146
|
+
function RadioControl(ctx) {
|
|
147
|
+
const { q, value, formId, locked, update } = ctx;
|
|
148
|
+
if (!q.options)
|
|
149
|
+
return null;
|
|
150
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "qf-options", children: q.options.map((opt) => (_jsxs("label", { className: `qf-chip${value === opt.value ? ' qf-chip-on' : ''}`, title: opt.description, children: [_jsx("input", { type: "radio", name: `${formId}-${q.id}`, value: opt.value, checked: value === opt.value, disabled: locked, "aria-label": opt.label, onChange: () => update(q.id, opt.value) }), _jsx(OptionCopy, { option: opt })] }, opt.value))) }), singleCustomChoice(ctx)] }));
|
|
151
|
+
}
|
|
152
|
+
function CheckboxControl(ctx) {
|
|
153
|
+
const { q, value, locked, toggleCheckbox, updateCheckboxCustom } = ctx;
|
|
154
|
+
if (!q.options)
|
|
155
|
+
return null;
|
|
156
|
+
const arr = Array.isArray(value) ? value : [];
|
|
157
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "qf-options", children: q.options.map((opt) => {
|
|
158
|
+
const on = arr.includes(opt.value);
|
|
159
|
+
const maxed = q.maxSelections !== undefined && !on && arr.length >= q.maxSelections;
|
|
160
|
+
return (_jsxs("label", { title: opt.description, className: `qf-chip${on ? ' qf-chip-on' : ''}${maxed ? ' qf-chip-disabled' : ''}`, children: [_jsx("input", { type: "checkbox", value: opt.value, checked: on, disabled: locked || maxed, "aria-label": opt.label, onChange: () => toggleCheckbox(q.id, opt.value, q.maxSelections) }), _jsx(OptionCopy, { option: opt })] }, opt.value));
|
|
161
|
+
}) }), customChoice(ctx, customCheckboxValue(q, value), (next) => updateCheckboxCustom(q, next))] }));
|
|
162
|
+
}
|
|
163
|
+
function SelectControl(ctx) {
|
|
164
|
+
const { q, value, locked, t, update } = ctx;
|
|
165
|
+
if (!q.options)
|
|
166
|
+
return null;
|
|
167
|
+
return (_jsxs(_Fragment, { children: [_jsxs("select", { className: "qf-select", value: typeof value === 'string' && questionValueIsKnown(q, value) ? value : '', disabled: locked, onChange: (e) => update(q.id, e.target.value), children: [_jsx("option", { value: "", disabled: true, children: q.placeholder ?? t('Choose one') }), q.options.map((opt) => (_jsx("option", { value: opt.value, title: opt.description, children: opt.label }, opt.value)))] }), singleCustomChoice(ctx)] }));
|
|
168
|
+
}
|
|
169
|
+
function DirectionCardsControl(ctx) {
|
|
170
|
+
const { q, value, formId, locked, update } = ctx;
|
|
171
|
+
if (!q.cards || q.cards.length === 0)
|
|
172
|
+
return null;
|
|
173
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "qf-direction-cards", children: q.cards.map((card) => (_jsx(DirectionCardView, { card: card, formId: formId, questionId: q.id, selected: value === card.id || value === card.label, disabled: locked, onSelect: () => update(q.id, card.id) }, card.id))) }), singleCustomChoice(ctx)] }));
|
|
174
|
+
}
|
|
175
|
+
/** date/time/datetime-local and url/email/tel share one plain input whose `type` is the question type. */
|
|
176
|
+
function TypedInputControl({ q, value, locked, update }) {
|
|
177
|
+
return _jsx("input", { type: q.type, className: "qf-input", value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) });
|
|
178
|
+
}
|
|
179
|
+
const QUESTION_CONTROLS = {
|
|
180
|
+
radio: RadioControl,
|
|
181
|
+
checkbox: CheckboxControl,
|
|
182
|
+
select: SelectControl,
|
|
183
|
+
'direction-cards': DirectionCardsControl,
|
|
184
|
+
text: ({ q, value, locked, update }) => _jsx("input", { ...questionTextInputProps({ question: q }), className: "qf-input", value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
|
|
185
|
+
number: ({ q, value, locked, update }) => _jsx("input", { type: "number", className: "qf-input", value: stringValue(value), placeholder: q.placeholder, min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
|
|
186
|
+
range: ({ q, value, locked, update }) => (_jsxs("div", { className: "qf-range-wrap", children: [_jsx("input", { type: "range", className: "qf-range", value: rangeValue(q, value), min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }), _jsx("output", { className: "qf-range-value", children: rangeValue(q, value) })] })),
|
|
187
|
+
date: TypedInputControl,
|
|
188
|
+
time: TypedInputControl,
|
|
189
|
+
'datetime-local': TypedInputControl,
|
|
190
|
+
url: TypedInputControl,
|
|
191
|
+
email: TypedInputControl,
|
|
192
|
+
tel: TypedInputControl,
|
|
193
|
+
color: ({ q, value, locked, update }) => _jsx("input", { type: "color", className: "qf-color", value: normalizeColorInputValue(value), disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
|
|
194
|
+
file: ({ q, value, locked, selectFiles }) => (_jsxs("div", { className: "qf-file-wrap", children: [_jsx("input", { type: "file", className: "qf-file", multiple: q.multiple, accept: q.accept, disabled: locked, onChange: (e) => selectFiles(q, e.target.files) }), fileValueLabel(value) ? _jsx("div", { className: "qf-file-summary", children: fileValueLabel(value) }) : null] })),
|
|
195
|
+
switch: ({ q, value, locked, update }) => (_jsxs("label", { className: "qf-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: value === 'true', disabled: locked, onChange: (e) => update(q.id, e.target.checked ? 'true' : 'false') }), _jsx("span", { "aria-hidden": true })] })),
|
|
196
|
+
textarea: ({ q, value, locked, update }) => _jsx("textarea", { ...questionTextareaProps({ question: q }), className: "qf-textarea", value: stringValue(value), placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) }),
|
|
197
|
+
};
|
|
135
198
|
function OptionCopy({ option }) {
|
|
136
199
|
return (_jsxs("span", { className: "qf-chip-copy", children: [_jsx("span", { children: option.label }), option.description ? _jsx("span", { className: "qf-chip-desc", children: option.description }) : null] }));
|
|
137
200
|
}
|
|
@@ -145,22 +208,31 @@ function DirectionCardView({ card, formId, questionId, selected, disabled, onSel
|
|
|
145
208
|
}
|
|
146
209
|
function buildInitialState(form, submitted, draft) {
|
|
147
210
|
const out = {};
|
|
211
|
+
for (const q of form.questions)
|
|
212
|
+
out[q.id] = initialQuestionValue(q, submitted, draft);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
/** Submitted beats draft beats default beats empty; file drafts are never restored (the File objects are gone). */
|
|
216
|
+
function initialQuestionValue(q, submitted, draft) {
|
|
217
|
+
if (submitted?.[q.id] !== undefined)
|
|
218
|
+
return canonicalizeQuestionValue(q, submitted[q.id]);
|
|
219
|
+
if (draft?.[q.id] !== undefined && q.type !== 'file')
|
|
220
|
+
return canonicalizeQuestionValue(q, draft[q.id]);
|
|
221
|
+
if (q.defaultValue !== undefined)
|
|
222
|
+
return canonicalizeQuestionValue(q, q.defaultValue);
|
|
223
|
+
return emptyQuestionValue(q);
|
|
224
|
+
}
|
|
225
|
+
/** Questions that stream in after mount get their initial value; answers already set are kept. */
|
|
226
|
+
function backfillAnswers(prev, form, submitted) {
|
|
227
|
+
let changed = false;
|
|
228
|
+
const next = { ...prev };
|
|
148
229
|
for (const q of form.questions) {
|
|
149
|
-
if (
|
|
150
|
-
out[q.id] = canonicalizeQuestionValue(q, submitted[q.id]);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (draft && draft[q.id] !== undefined && q.type !== 'file') {
|
|
154
|
-
out[q.id] = canonicalizeQuestionValue(q, draft[q.id]);
|
|
230
|
+
if (next[q.id] !== undefined)
|
|
155
231
|
continue;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
out[q.id] = canonicalizeQuestionValue(q, q.defaultValue);
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
out[q.id] = emptyQuestionValue(q);
|
|
232
|
+
changed = true;
|
|
233
|
+
next[q.id] = initialQuestionValue(q, submitted, undefined);
|
|
162
234
|
}
|
|
163
|
-
return
|
|
235
|
+
return changed ? next : prev;
|
|
164
236
|
}
|
|
165
237
|
function draftSafeAnswers(form, answers) {
|
|
166
238
|
const fileQuestionIds = new Set(form.questions.filter((q) => q.type === 'file').map((q) => q.id));
|
|
@@ -20,7 +20,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
import { useRef, useState } from 'react';
|
|
21
21
|
import { useT } from '../hooks/context.js';
|
|
22
22
|
import { QuestionForm } from './QuestionForm.js';
|
|
23
|
-
export function QuestionsPanel({ form, interactive, submitDisabled
|
|
23
|
+
export function QuestionsPanel({ form, interactive, submitDisabled, submittedAnswers, generating = false, onSubmit, redactUserText, onSecretRedacted }) {
|
|
24
24
|
const t = useT();
|
|
25
25
|
const formRef = useRef(null);
|
|
26
26
|
const [ready, setReady] = useState(false);
|