@xinghunm/ai-chat 0.3.0 → 1.0.0

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/dist/index.js CHANGED
@@ -78,10 +78,18 @@ var DEFAULT_AI_CHAT_LABELS = {
78
78
  attachmentLimitNotice: "Images exceeded the limit. Only the first 10 images were kept.",
79
79
  userRoleLabel: "User",
80
80
  assistantRoleLabel: "Assistant",
81
+ expandMessageAriaLabel: "Expand message",
82
+ collapseMessageAriaLabel: "Collapse message",
83
+ expandComposerAriaLabel: "Expand composer",
84
+ collapseComposerAriaLabel: "Collapse composer",
81
85
  stoppedResponse: "Response stopped",
82
86
  assistantStreamingAriaLabel: "assistant streaming",
83
87
  networkError: "Network request failed. Please try again.",
84
- genericError: "Something went wrong. Please try again."
88
+ genericError: "Something went wrong. Please try again.",
89
+ questionnaireSubmitting: "Submitting...",
90
+ questionnaireSubmitted: "Selection submitted. Waiting for the plan to continue...",
91
+ questionnaireValidationPrefix: "Please complete:",
92
+ questionnaireSubmitFailed: "Failed to submit. Please try again."
85
93
  };
86
94
 
87
95
  // src/store/chat-store.ts
@@ -97,6 +105,24 @@ var resolveSessionTitleFromMessage = (message) => {
97
105
  }
98
106
  return DEFAULT_CHAT_SESSION_TITLE;
99
107
  };
108
+ var mergeStreamingBlocks = (existingBlocks, incomingBlocks) => {
109
+ const nextBlocks = [...existingBlocks ?? []];
110
+ incomingBlocks.forEach((incomingBlock) => {
111
+ if (incomingBlock.type !== "questionnaire") {
112
+ nextBlocks.push(incomingBlock);
113
+ return;
114
+ }
115
+ const existingIndex = nextBlocks.findIndex(
116
+ (block) => block.type === "questionnaire" && block.questionnaire.questionnaireId === incomingBlock.questionnaire.questionnaireId
117
+ );
118
+ if (existingIndex === -1) {
119
+ nextBlocks.push(incomingBlock);
120
+ return;
121
+ }
122
+ nextBlocks[existingIndex] = incomingBlock;
123
+ });
124
+ return nextBlocks;
125
+ };
100
126
  var finalizeStreamingMessage = (state, sessionId, status, clearError = false) => {
101
127
  const message = state.streamingMessageBySession[sessionId];
102
128
  const hasRenderableMessagePayload = Boolean(
@@ -286,10 +312,15 @@ var createChatStore = (initialState) => (0, import_vanilla.createStore)((set, ge
286
312
  const target = state.streamingMessageBySession[sessionId];
287
313
  if (!target)
288
314
  return;
315
+ const nextBlocks = patch.blocks !== void 0 ? mergeStreamingBlocks(target.blocks, patch.blocks) : target.blocks;
289
316
  set({
290
317
  streamingMessageBySession: {
291
318
  ...state.streamingMessageBySession,
292
- [sessionId]: { ...target, ...patch }
319
+ [sessionId]: {
320
+ ...target,
321
+ ...patch,
322
+ ...patch.blocks !== void 0 ? { blocks: nextBlocks } : {}
323
+ }
293
324
  }
294
325
  });
295
326
  },
@@ -509,9 +540,27 @@ var DEFAULT_CHAT_TRANSPORT_ENDPOINTS = {
509
540
  completions: "/chat/completions",
510
541
  terminate: "/chat/terminate"
511
542
  };
543
+ var createToolExecutionHeaders = (policy) => {
544
+ if (!policy?.enabled) {
545
+ return {};
546
+ }
547
+ return {
548
+ "X-Tool-Approval-Required": String(Boolean(policy.approvalRequired)),
549
+ ...typeof policy.approvalTimeoutSec === "number" ? { "X-Tool-Approval-Timeout": String(policy.approvalTimeoutSec) } : {}
550
+ };
551
+ };
552
+ var createModeDefaultHeaders = (mode) => {
553
+ if (mode === "ask" || mode === "plan") {
554
+ return {
555
+ "X-Tool-Approval-Required": "false"
556
+ };
557
+ }
558
+ return {};
559
+ };
512
560
  var createDefaultChatTransport = ({
513
561
  apiBaseUrl,
514
562
  authToken,
563
+ toolExecutionPolicy,
515
564
  streamHeaders,
516
565
  transformStreamPacket,
517
566
  endpoints,
@@ -522,6 +571,10 @@ var createDefaultChatTransport = ({
522
571
  ...DEFAULT_CHAT_TRANSPORT_ENDPOINTS,
523
572
  ...endpoints
524
573
  };
574
+ const resolvedStreamHeaders = {
575
+ ...createToolExecutionHeaders(toolExecutionPolicy),
576
+ ...streamHeaders
577
+ };
525
578
  return {
526
579
  getModels: () => getChatModels(client, resolvedEndpoints.models),
527
580
  startStream: async ({
@@ -535,12 +588,16 @@ var createDefaultChatTransport = ({
535
588
  onDone,
536
589
  onError
537
590
  }) => {
591
+ const requestHeaders = {
592
+ ...createModeDefaultHeaders(mode),
593
+ ...resolvedStreamHeaders
594
+ };
538
595
  await startChatStream({
539
596
  apiBaseUrl,
540
597
  endpointPath: resolvedEndpoints.completions,
541
598
  sessionId,
542
599
  authToken,
543
- requestHeaders: streamHeaders,
600
+ requestHeaders,
544
601
  model,
545
602
  mode,
546
603
  content,
@@ -569,6 +626,8 @@ var AiChatProvider = (props) => {
569
626
  defaultMode,
570
627
  labels,
571
628
  renderMessageBlock,
629
+ handleQuestionnaireSubmit,
630
+ handleConfirmationSubmit,
572
631
  messageRenderOrder,
573
632
  enableImageAttachments = true,
574
633
  children
@@ -624,6 +683,8 @@ var AiChatProvider = (props) => {
624
683
  sendRef,
625
684
  retryRef,
626
685
  renderMessageBlock,
686
+ handleQuestionnaireSubmit,
687
+ handleConfirmationSubmit,
627
688
  messageRenderOrder,
628
689
  transformStreamPacket: defaultTransformStreamPacket,
629
690
  enableImageAttachments
@@ -634,6 +695,8 @@ var AiChatProvider = (props) => {
634
695
  defaultAuthToken,
635
696
  defaultTransformStreamPacket,
636
697
  enableImageAttachments,
698
+ handleConfirmationSubmit,
699
+ handleQuestionnaireSubmit,
637
700
  labels,
638
701
  messageRenderOrder,
639
702
  renderMessageBlock,
@@ -943,7 +1006,7 @@ var getTimelineBlockKey = (block, index) => {
943
1006
  case "confirmation_card":
944
1007
  return `${index}:confirmation_card:${block.proposal.proposalId}`;
945
1008
  case "result_summary":
946
- return `${index}:result_summary:${block.summary.taskId}:${block.summary.status}`;
1009
+ return `${index}:result_summary:${block.summary.summaryId}:${block.summary.status}`;
947
1010
  case "questionnaire":
948
1011
  return `${index}:questionnaire:${block.questionnaire.questionnaireId}`;
949
1012
  case "custom":
@@ -1069,6 +1132,83 @@ var buildAnchoredTimelineSegments = ({
1069
1132
  };
1070
1133
 
1071
1134
  // src/components/chat-thread/hooks/use-timeline-block-anchors.ts
1135
+ var createTimelineAnchorState = ({
1136
+ messageId,
1137
+ currentBlockKeys
1138
+ }) => ({
1139
+ messageId,
1140
+ previousBlockKeys: currentBlockKeys,
1141
+ timelineBlockAnchors: {},
1142
+ visibleTimelineBlockKeys: {}
1143
+ });
1144
+ var timelineAnchorReducer = (state, action) => {
1145
+ switch (action.type) {
1146
+ case "reset-message":
1147
+ if (state.messageId === action.messageId) {
1148
+ return state;
1149
+ }
1150
+ return createTimelineAnchorState(action);
1151
+ case "sync-anchors": {
1152
+ const previousBlockKeys = new Set(state.previousBlockKeys);
1153
+ const nextAnchors = action.currentBlockKeys.reduce(
1154
+ (acc, blockKey) => {
1155
+ const existingAnchor = state.timelineBlockAnchors[blockKey];
1156
+ if (existingAnchor !== void 0) {
1157
+ acc[blockKey] = existingAnchor;
1158
+ return acc;
1159
+ }
1160
+ if (!previousBlockKeys.has(blockKey)) {
1161
+ acc[blockKey] = action.timelineTextStreamLength;
1162
+ }
1163
+ return acc;
1164
+ },
1165
+ {}
1166
+ );
1167
+ const hasAnchorChanged = Object.keys(nextAnchors).length !== Object.keys(state.timelineBlockAnchors).length || Object.entries(nextAnchors).some(
1168
+ ([blockKey, anchor]) => state.timelineBlockAnchors[blockKey] !== anchor
1169
+ );
1170
+ const hasPreviousKeysChanged = action.currentBlockKeys.length !== state.previousBlockKeys.length || action.currentBlockKeys.some(
1171
+ (blockKey, index) => state.previousBlockKeys[index] !== blockKey
1172
+ );
1173
+ if (!hasAnchorChanged && !hasPreviousKeysChanged) {
1174
+ return state;
1175
+ }
1176
+ return {
1177
+ ...state,
1178
+ previousBlockKeys: action.currentBlockKeys,
1179
+ timelineBlockAnchors: hasAnchorChanged ? nextAnchors : state.timelineBlockAnchors
1180
+ };
1181
+ }
1182
+ case "sync-visible": {
1183
+ const nextVisibleBlockKeys = action.currentBlockKeys.reduce(
1184
+ (acc, blockKey) => {
1185
+ if (state.visibleTimelineBlockKeys[blockKey]) {
1186
+ acc[blockKey] = true;
1187
+ return acc;
1188
+ }
1189
+ const anchor = action.effectiveTimelineBlockAnchors[blockKey];
1190
+ if (anchor !== void 0 && anchor <= action.displayedTimelineTextLength) {
1191
+ acc[blockKey] = true;
1192
+ }
1193
+ return acc;
1194
+ },
1195
+ {}
1196
+ );
1197
+ const hasVisibleBlockChanged = Object.keys(nextVisibleBlockKeys).length !== Object.keys(state.visibleTimelineBlockKeys).length || Object.keys(nextVisibleBlockKeys).some(
1198
+ (blockKey) => !state.visibleTimelineBlockKeys[blockKey]
1199
+ );
1200
+ if (!hasVisibleBlockChanged) {
1201
+ return state;
1202
+ }
1203
+ return {
1204
+ ...state,
1205
+ visibleTimelineBlockKeys: nextVisibleBlockKeys
1206
+ };
1207
+ }
1208
+ default:
1209
+ return state;
1210
+ }
1211
+ };
1072
1212
  var useTimelineBlockAnchors = ({
1073
1213
  blocks,
1074
1214
  displayedTimelineTextLength,
@@ -1076,8 +1216,6 @@ var useTimelineBlockAnchors = ({
1076
1216
  message,
1077
1217
  messageRenderOrder
1078
1218
  }) => {
1079
- const [timelineBlockAnchors, setTimelineBlockAnchors] = (0, import_react6.useState)({});
1080
- const [visibleTimelineBlockKeys, setVisibleTimelineBlockKeys] = (0, import_react6.useState)({});
1081
1219
  const currentTimelineBlockKeys = (0, import_react6.useMemo)(
1082
1220
  () => blocks.map((block, index) => getTimelineBlockKey(block, index)).filter((blockKey) => Boolean(blockKey)),
1083
1221
  [blocks]
@@ -1086,20 +1224,22 @@ var useTimelineBlockAnchors = ({
1086
1224
  () => getTimelineDisplayUnitCount(getTimelineTextStream(message.content, blocks)),
1087
1225
  [blocks, message.content]
1088
1226
  );
1089
- const previousTimelineStateRef = (0, import_react6.useRef)({
1090
- messageId: message.id,
1091
- blockKeys: currentTimelineBlockKeys,
1092
- textLength: timelineTextStreamLength
1093
- });
1227
+ const [state, dispatch] = (0, import_react6.useReducer)(
1228
+ timelineAnchorReducer,
1229
+ {
1230
+ messageId: message.id,
1231
+ currentBlockKeys: currentTimelineBlockKeys
1232
+ },
1233
+ createTimelineAnchorState
1234
+ );
1094
1235
  const effectiveTimelineBlockAnchors = (0, import_react6.useMemo)(() => {
1095
1236
  if (messageRenderOrder !== "timeline" || !isAssistantStreaming) {
1096
- return timelineBlockAnchors;
1237
+ return state.timelineBlockAnchors;
1097
1238
  }
1098
- const previousTimelineState = previousTimelineStateRef.current;
1099
- const previousBlockKeys = new Set(previousTimelineState.blockKeys);
1239
+ const previousBlockKeys = new Set(state.previousBlockKeys);
1100
1240
  return currentTimelineBlockKeys.reduce(
1101
1241
  (acc, blockKey) => {
1102
- const existingAnchor = timelineBlockAnchors[blockKey];
1242
+ const existingAnchor = state.timelineBlockAnchors[blockKey];
1103
1243
  if (existingAnchor !== void 0) {
1104
1244
  acc[blockKey] = existingAnchor;
1105
1245
  return acc;
@@ -1109,125 +1249,70 @@ var useTimelineBlockAnchors = ({
1109
1249
  }
1110
1250
  return acc;
1111
1251
  },
1112
- { ...timelineBlockAnchors }
1252
+ { ...state.timelineBlockAnchors }
1113
1253
  );
1114
1254
  }, [
1115
1255
  currentTimelineBlockKeys,
1116
1256
  isAssistantStreaming,
1117
1257
  messageRenderOrder,
1118
- timelineBlockAnchors,
1258
+ state.previousBlockKeys,
1259
+ state.timelineBlockAnchors,
1119
1260
  timelineTextStreamLength
1120
1261
  ]);
1121
1262
  (0, import_react6.useEffect)(() => {
1122
- const previousTimelineState = previousTimelineStateRef.current;
1123
- if (previousTimelineState.messageId !== message.id) {
1124
- if (Object.keys(timelineBlockAnchors).length > 0) {
1125
- setTimelineBlockAnchors({});
1126
- }
1127
- if (Object.keys(visibleTimelineBlockKeys).length > 0) {
1128
- setVisibleTimelineBlockKeys({});
1129
- }
1130
- previousTimelineStateRef.current = {
1131
- messageId: message.id,
1132
- blockKeys: currentTimelineBlockKeys,
1133
- textLength: timelineTextStreamLength
1134
- };
1263
+ dispatch({
1264
+ type: "reset-message",
1265
+ messageId: message.id,
1266
+ currentBlockKeys: currentTimelineBlockKeys
1267
+ });
1268
+ }, [currentTimelineBlockKeys, message.id]);
1269
+ (0, import_react6.useEffect)(() => {
1270
+ if (messageRenderOrder !== "timeline" || !isAssistantStreaming) {
1135
1271
  return;
1136
1272
  }
1137
- if (messageRenderOrder === "timeline" && isAssistantStreaming) {
1138
- const previousBlockKeys = new Set(previousTimelineState.blockKeys);
1139
- const nextAnchors = currentTimelineBlockKeys.reduce(
1140
- (acc, blockKey) => {
1141
- const existingAnchor = timelineBlockAnchors[blockKey];
1142
- if (existingAnchor !== void 0) {
1143
- acc[blockKey] = existingAnchor;
1144
- return acc;
1145
- }
1146
- if (!previousBlockKeys.has(blockKey)) {
1147
- acc[blockKey] = timelineTextStreamLength;
1148
- }
1149
- return acc;
1150
- },
1151
- {}
1152
- );
1153
- const hasAnchorChanged = Object.keys(nextAnchors).length !== Object.keys(timelineBlockAnchors).length || Object.entries(nextAnchors).some(
1154
- ([blockKey, anchor]) => timelineBlockAnchors[blockKey] !== anchor
1155
- );
1156
- if (hasAnchorChanged) {
1157
- setTimelineBlockAnchors(nextAnchors);
1158
- }
1159
- } else if (messageRenderOrder !== "timeline" && Object.keys(timelineBlockAnchors).length > 0) {
1160
- setTimelineBlockAnchors({});
1161
- }
1162
- previousTimelineStateRef.current = {
1163
- messageId: message.id,
1164
- blockKeys: currentTimelineBlockKeys,
1165
- textLength: timelineTextStreamLength
1166
- };
1167
- }, [
1168
- currentTimelineBlockKeys,
1169
- isAssistantStreaming,
1170
- message.id,
1171
- message.content,
1172
- messageRenderOrder,
1173
- timelineBlockAnchors,
1174
- timelineTextStreamLength,
1175
- visibleTimelineBlockKeys
1176
- ]);
1273
+ dispatch({
1274
+ type: "sync-anchors",
1275
+ currentBlockKeys: currentTimelineBlockKeys,
1276
+ timelineTextStreamLength
1277
+ });
1278
+ }, [currentTimelineBlockKeys, isAssistantStreaming, messageRenderOrder, timelineTextStreamLength]);
1177
1279
  (0, import_react6.useEffect)(() => {
1178
1280
  if (messageRenderOrder !== "timeline") {
1179
- if (Object.keys(visibleTimelineBlockKeys).length > 0) {
1180
- setVisibleTimelineBlockKeys({});
1181
- }
1182
1281
  return;
1183
1282
  }
1184
- const nextVisibleBlockKeys = currentTimelineBlockKeys.reduce(
1185
- (acc, blockKey) => {
1186
- if (visibleTimelineBlockKeys[blockKey]) {
1187
- acc[blockKey] = true;
1188
- return acc;
1189
- }
1190
- const anchor = effectiveTimelineBlockAnchors[blockKey];
1191
- if (anchor !== void 0 && anchor <= displayedTimelineTextLength) {
1192
- acc[blockKey] = true;
1193
- }
1194
- return acc;
1195
- },
1196
- {}
1197
- );
1198
- const hasVisibleBlockChanged = Object.keys(nextVisibleBlockKeys).length !== Object.keys(visibleTimelineBlockKeys).length || Object.keys(nextVisibleBlockKeys).some((blockKey) => !visibleTimelineBlockKeys[blockKey]);
1199
- if (hasVisibleBlockChanged) {
1200
- setVisibleTimelineBlockKeys(nextVisibleBlockKeys);
1201
- }
1283
+ dispatch({
1284
+ type: "sync-visible",
1285
+ currentBlockKeys: currentTimelineBlockKeys,
1286
+ effectiveTimelineBlockAnchors,
1287
+ displayedTimelineTextLength
1288
+ });
1202
1289
  }, [
1203
1290
  currentTimelineBlockKeys,
1204
1291
  displayedTimelineTextLength,
1205
1292
  effectiveTimelineBlockAnchors,
1206
- messageRenderOrder,
1207
- timelineBlockAnchors,
1208
- visibleTimelineBlockKeys
1293
+ messageRenderOrder
1209
1294
  ]);
1210
1295
  return {
1211
- timelineBlockAnchors: effectiveTimelineBlockAnchors,
1212
- visibleTimelineBlockKeys
1296
+ timelineBlockAnchors: messageRenderOrder === "timeline" ? effectiveTimelineBlockAnchors : {},
1297
+ visibleTimelineBlockKeys: messageRenderOrder === "timeline" ? state.visibleTimelineBlockKeys : {}
1213
1298
  };
1214
1299
  };
1215
1300
 
1216
- // src/components/chat-thread/components/pde-ai-execution-confirmation-card.tsx
1301
+ // src/components/chat-thread/components/execution-confirmation-card.tsx
1217
1302
  var import_styled = __toESM(require("@emotion/styled"));
1218
1303
  var import_jsx_runtime2 = require("@emotion/react/jsx-runtime");
1219
- var PDEAIExecutionConfirmationCard = ({
1304
+ var ExecutionConfirmationCard = ({
1220
1305
  proposal,
1221
1306
  interactive = false,
1222
1307
  onApply,
1223
1308
  onConfirm,
1224
1309
  onRevise
1225
1310
  }) => {
1226
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Card, { "data-testid": "pde-ai-execution-confirmation-card", children: [
1311
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Card, { "data-testid": "execution-confirmation-card", children: [
1227
1312
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Header, { children: [
1228
1313
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Eyebrow, { children: "Execution Proposal" }),
1229
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Title, { children: proposal.equationName }),
1230
- proposal.solverName ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Subtitle, { children: proposal.solverName }) : null
1314
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Title, { children: proposal.resourceName }),
1315
+ proposal.executorName ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Subtitle, { children: proposal.executorName }) : null
1231
1316
  ] }),
1232
1317
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SummaryList, { children: proposal.parameterSummary.map((item) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(SummaryItem, { children: [
1233
1318
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SummaryLabel, { children: item.label }),
@@ -1235,21 +1320,13 @@ var PDEAIExecutionConfirmationCard = ({
1235
1320
  ] }, `${item.fieldPath ?? item.label}-${item.value}`)) }),
1236
1321
  proposal.warnings?.length ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Warnings, { children: proposal.warnings.map((warning) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Warning, { children: warning }, warning)) }) : null,
1237
1322
  interactive ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Actions, { children: [
1238
- onApply && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ActionButton, { type: "button", "data-testid": "pde-ai-confirmation-apply", onClick: onApply, children: "Apply to Parameters" }),
1239
- onConfirm && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1240
- ActionButton,
1241
- {
1242
- type: "button",
1243
- "data-testid": "pde-ai-confirmation-confirm",
1244
- onClick: onConfirm,
1245
- children: "Confirm Execution"
1246
- }
1247
- ),
1323
+ onApply && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ActionButton, { type: "button", "data-testid": "confirmation-apply", onClick: onApply, children: "Apply to Parameters" }),
1324
+ onConfirm && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ActionButton, { type: "button", "data-testid": "confirmation-confirm", onClick: onConfirm, children: "Confirm Execution" }),
1248
1325
  onRevise && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1249
1326
  SecondaryActionButton,
1250
1327
  {
1251
1328
  type: "button",
1252
- "data-testid": "pde-ai-confirmation-revise",
1329
+ "data-testid": "confirmation-revise",
1253
1330
  onClick: onRevise,
1254
1331
  children: "Revise Plan"
1255
1332
  }
@@ -1346,10 +1423,10 @@ var SecondaryActionButton = (0, import_styled.default)(ActionButton)`
1346
1423
  border: 1px solid rgba(255, 255, 255, 0.14);
1347
1424
  `;
1348
1425
 
1349
- // src/components/chat-thread/components/pde-ai-notice-card.tsx
1426
+ // src/components/chat-thread/components/notice-card.tsx
1350
1427
  var import_styled2 = __toESM(require("@emotion/styled"));
1351
1428
  var import_jsx_runtime3 = require("@emotion/react/jsx-runtime");
1352
- var PDEAINoticeCard = ({ text, tone }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Card2, { "data-testid": "pde-ai-notice-card", "data-tone": tone, children: text });
1429
+ var NoticeCard = ({ text, tone }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Card2, { "data-testid": "notice-card", "data-tone": tone, children: text });
1353
1430
  var Card2 = import_styled2.default.div`
1354
1431
  padding: 12px 14px;
1355
1432
  border-radius: 16px;
@@ -1375,10 +1452,10 @@ var Card2 = import_styled2.default.div`
1375
1452
  }
1376
1453
  `;
1377
1454
 
1378
- // src/components/chat-thread/components/pde-ai-parameter-summary-card.tsx
1455
+ // src/components/chat-thread/components/parameter-summary-card.tsx
1379
1456
  var import_styled3 = __toESM(require("@emotion/styled"));
1380
1457
  var import_jsx_runtime4 = require("@emotion/react/jsx-runtime");
1381
- var PDEAIParameterSummaryCard = ({ items }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Card3, { "data-testid": "pde-ai-parameter-summary-card", children: [
1458
+ var ParameterSummaryCard = ({ items }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Card3, { "data-testid": "parameter-summary-card", children: [
1382
1459
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Title2, { children: "Parameter Summary" }),
1383
1460
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(List, { children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(ListItem, { children: [
1384
1461
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { children: item.label }),
@@ -1422,11 +1499,17 @@ var Value = import_styled3.default.span`
1422
1499
  text-align: right;
1423
1500
  `;
1424
1501
 
1425
- // src/components/chat-thread/components/pde-ai-questionnaire-card.tsx
1502
+ // src/components/chat-thread/components/questionnaire-card.tsx
1426
1503
  var import_react7 = require("react");
1427
1504
  var import_styled4 = __toESM(require("@emotion/styled"));
1428
1505
  var import_jsx_runtime5 = require("@emotion/react/jsx-runtime");
1429
1506
  var OTHER_OPTION_VALUE = "__other__";
1507
+ var DEFAULT_QUESTIONNAIRE_CARD_LABELS = {
1508
+ submitting: "Submitting...",
1509
+ submitted: "Selection submitted. Waiting for the plan to continue...",
1510
+ validationPrefix: "Please complete:",
1511
+ submitFailed: "Failed to submit. Please try again."
1512
+ };
1430
1513
  var createInitialAnswers = (questionnaire) => ({
1431
1514
  ...questionnaire.answers ?? {}
1432
1515
  });
@@ -1520,26 +1603,40 @@ var normalizeQuestionAnswer = (question, answer) => {
1520
1603
  return answer;
1521
1604
  }
1522
1605
  };
1523
- var PDEAIQuestionnaireCardInner = ({
1606
+ var QuestionnaireCardInner = ({
1524
1607
  questionnaire,
1525
1608
  interactive = false,
1526
- onSubmit
1609
+ onSubmit,
1610
+ labels
1527
1611
  }) => {
1528
1612
  const [answers, setAnswers] = (0, import_react7.useState)(
1529
1613
  () => createInitialAnswers(questionnaire)
1530
1614
  );
1531
1615
  const [errorMessage, setErrorMessage] = (0, import_react7.useState)(null);
1532
- const handleSubmit = () => {
1616
+ const [isSubmitting, setIsSubmitting] = (0, import_react7.useState)(false);
1617
+ const [isSubmitted, setIsSubmitted] = (0, import_react7.useState)(false);
1618
+ const resolvedLabels = {
1619
+ ...DEFAULT_QUESTIONNAIRE_CARD_LABELS,
1620
+ ...labels
1621
+ };
1622
+ const hasExternalFailureStatus = questionnaire.status === "expired" || questionnaire.status === "failed";
1623
+ const visibleErrorMessage = questionnaire.statusMessage ?? errorMessage;
1624
+ const isInteractionLocked = !interactive || isSubmitting || isSubmitted || hasExternalFailureStatus;
1625
+ const handleSubmit = async () => {
1626
+ if (isSubmitting || isSubmitted) {
1627
+ return;
1628
+ }
1533
1629
  const missingQuestions = questionnaire.questions.filter(
1534
1630
  (question) => question.required && isMissingRequiredAnswer(question, answers)
1535
1631
  );
1536
1632
  if (missingQuestions.length > 0) {
1537
1633
  setErrorMessage(
1538
- `Please complete: ${missingQuestions.map((question) => question.label).join(", ")}`
1634
+ `${resolvedLabels.validationPrefix} ${missingQuestions.map((question) => question.label).join(", ")}`
1539
1635
  );
1540
1636
  return;
1541
1637
  }
1542
1638
  setErrorMessage(null);
1639
+ setIsSubmitting(true);
1543
1640
  const normalizedAnswers = Object.fromEntries(
1544
1641
  questionnaire.questions.flatMap((question) => {
1545
1642
  const value = normalizeQuestionAnswer(question, answers[question.id]);
@@ -1556,11 +1653,18 @@ var PDEAIQuestionnaireCardInner = ({
1556
1653
  return [`- ${question.label}: ${formatQuestionAnswer(question, value)}`];
1557
1654
  })
1558
1655
  ];
1559
- onSubmit?.({
1560
- questionnaireId: questionnaire.questionnaireId,
1561
- answers: normalizedAnswers,
1562
- content: contentLines.join("\n")
1563
- });
1656
+ try {
1657
+ await onSubmit?.({
1658
+ questionnaireId: questionnaire.questionnaireId,
1659
+ answers: normalizedAnswers,
1660
+ content: contentLines.join("\n")
1661
+ });
1662
+ setIsSubmitted(true);
1663
+ } catch (error) {
1664
+ setErrorMessage(error instanceof Error ? error.message : resolvedLabels.submitFailed);
1665
+ } finally {
1666
+ setIsSubmitting(false);
1667
+ }
1564
1668
  };
1565
1669
  const renderQuestion = (question) => {
1566
1670
  const renderOptionChoice = ({
@@ -1575,13 +1679,13 @@ var PDEAIQuestionnaireCardInner = ({
1575
1679
  OptionChoiceItem,
1576
1680
  {
1577
1681
  role: "button",
1578
- tabIndex: interactive ? 0 : -1,
1682
+ tabIndex: isInteractionLocked ? -1 : 0,
1579
1683
  "aria-pressed": isSelected,
1580
1684
  "data-selected": isSelected,
1581
1685
  "data-tone": tone,
1582
- "data-testid": `pde-ai-question-option-${questionId}-${index}`,
1686
+ "data-testid": `question-option-${questionId}-${index}`,
1583
1687
  onClick: (event) => {
1584
- if (!interactive) {
1688
+ if (isInteractionLocked) {
1585
1689
  return;
1586
1690
  }
1587
1691
  if (event.target instanceof HTMLElement && event.target.closest("input")) {
@@ -1590,7 +1694,7 @@ var PDEAIQuestionnaireCardInner = ({
1590
1694
  onClick();
1591
1695
  },
1592
1696
  onKeyDown: (event) => {
1593
- if (!interactive) {
1697
+ if (isInteractionLocked) {
1594
1698
  return;
1595
1699
  }
1596
1700
  if (event.key !== "Enter" && event.key !== " ") {
@@ -1649,11 +1753,11 @@ var PDEAIQuestionnaireCardInner = ({
1649
1753
  inlineInput: singleSelectDraft.selectedValue === OTHER_OPTION_VALUE ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1650
1754
  InlineOtherInput,
1651
1755
  {
1652
- "data-testid": `pde-ai-question-input-${question.id}`,
1756
+ "data-testid": `question-input-${question.id}`,
1653
1757
  type: "text",
1654
1758
  value: singleSelectDraft.otherValue,
1655
1759
  placeholder: "Other",
1656
- readOnly: !interactive,
1760
+ readOnly: isInteractionLocked,
1657
1761
  onClick: (event) => {
1658
1762
  event.stopPropagation();
1659
1763
  },
@@ -1674,11 +1778,11 @@ var PDEAIQuestionnaireCardInner = ({
1674
1778
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(QuestionBody, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1675
1779
  TextInput,
1676
1780
  {
1677
- "data-testid": `pde-ai-question-input-${question.id}`,
1781
+ "data-testid": `question-input-${question.id}`,
1678
1782
  type: "text",
1679
1783
  value: getTextInputValue(answers[question.id]),
1680
1784
  placeholder: question.placeholder,
1681
- readOnly: !interactive,
1785
+ readOnly: isInteractionLocked,
1682
1786
  onChange: (event) => {
1683
1787
  setAnswers((current) => updateAnswerValue(current, question.id, event.target.value));
1684
1788
  }
@@ -1689,11 +1793,11 @@ var PDEAIQuestionnaireCardInner = ({
1689
1793
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1690
1794
  TextInput,
1691
1795
  {
1692
- "data-testid": `pde-ai-question-input-${question.id}`,
1796
+ "data-testid": `question-input-${question.id}`,
1693
1797
  type: "number",
1694
1798
  value: getNumberInputValue(answers[question.id]),
1695
1799
  placeholder: question.placeholder,
1696
- readOnly: !interactive,
1800
+ readOnly: isInteractionLocked,
1697
1801
  onChange: (event) => {
1698
1802
  setAnswers(
1699
1803
  (current) => updateAnswerValue(
@@ -1728,7 +1832,7 @@ var PDEAIQuestionnaireCardInner = ({
1728
1832
  return null;
1729
1833
  }
1730
1834
  };
1731
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Card4, { "data-testid": "pde-ai-questionnaire-card", children: [
1835
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Card4, { "data-testid": "questionnaire-card", children: [
1732
1836
  questionnaire.title ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Title3, { children: questionnaire.title }) : null,
1733
1837
  questionnaire.description ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Description, { children: questionnaire.description }) : null,
1734
1838
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(QuestionList, { children: questionnaire.questions.map((question) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(QuestionCard, { children: [
@@ -1738,24 +1842,28 @@ var PDEAIQuestionnaireCardInner = ({
1738
1842
  ] }),
1739
1843
  renderQuestion(question)
1740
1844
  ] }, question.id)) }),
1741
- errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ErrorMessage, { "data-testid": "pde-ai-questionnaire-error", children: errorMessage }) : null,
1742
- interactive ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1845
+ visibleErrorMessage ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ErrorMessage, { "data-testid": "questionnaire-error", children: visibleErrorMessage }) : null,
1846
+ isSubmitted ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SuccessMessage, { "data-testid": "questionnaire-success", children: resolvedLabels.submitted }) : interactive && !hasExternalFailureStatus ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1743
1847
  SubmitButton,
1744
1848
  {
1745
1849
  type: "button",
1746
- "data-testid": "pde-ai-questionnaire-submit",
1747
- onClick: handleSubmit,
1748
- children: questionnaire.submitLabel ?? "Submit"
1850
+ "data-testid": "questionnaire-submit",
1851
+ disabled: isInteractionLocked,
1852
+ onClick: () => {
1853
+ void handleSubmit();
1854
+ },
1855
+ children: isSubmitting ? resolvedLabels.submitting : questionnaire.submitLabel ?? "Submit"
1749
1856
  }
1750
1857
  ) : null
1751
1858
  ] });
1752
1859
  };
1753
1860
  var getQuestionnaireStateKey = (questionnaire) => JSON.stringify([
1754
1861
  questionnaire.questionnaireId,
1755
- questionnaire.answers ?? {},
1756
- questionnaire.questions
1862
+ questionnaire.questions,
1863
+ questionnaire.status,
1864
+ questionnaire.statusMessage
1757
1865
  ]);
1758
- var PDEAIQuestionnaireCard = (props) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PDEAIQuestionnaireCardInner, { ...props }, getQuestionnaireStateKey(props.questionnaire));
1866
+ var QuestionnaireCard = (props) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(QuestionnaireCardInner, { ...props }, getQuestionnaireStateKey(props.questionnaire));
1759
1867
  var Card4 = import_styled4.default.section`
1760
1868
  display: grid;
1761
1869
  gap: 14px;
@@ -1903,6 +2011,10 @@ var ErrorMessage = import_styled4.default.div`
1903
2011
  color: rgba(255, 145, 145, 0.96);
1904
2012
  font-size: 12px;
1905
2013
  `;
2014
+ var SuccessMessage = import_styled4.default.div`
2015
+ color: rgba(164, 255, 210, 0.96);
2016
+ font-size: 12px;
2017
+ `;
1906
2018
  var SubmitButton = import_styled4.default.button`
1907
2019
  justify-self: flex-start;
1908
2020
  border: none;
@@ -1913,12 +2025,17 @@ var SubmitButton = import_styled4.default.button`
1913
2025
  font-weight: 700;
1914
2026
  padding: 10px 14px;
1915
2027
  cursor: pointer;
2028
+
2029
+ &:disabled {
2030
+ cursor: default;
2031
+ opacity: 0.72;
2032
+ }
1916
2033
  `;
1917
2034
 
1918
- // src/components/chat-thread/components/pde-ai-result-summary-card.tsx
2035
+ // src/components/chat-thread/components/result-summary-card.tsx
1919
2036
  var import_styled5 = __toESM(require("@emotion/styled"));
1920
2037
  var import_jsx_runtime6 = require("@emotion/react/jsx-runtime");
1921
- var PDEAIResultSummaryCard = ({ summary }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Card5, { "data-testid": "pde-ai-result-summary-card", "data-status": summary.status, children: [
2038
+ var ResultSummaryCard = ({ summary }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Card5, { "data-testid": "result-summary-card", "data-status": summary.status, children: [
1922
2039
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Eyebrow2, { children: summary.status }),
1923
2040
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Headline, { children: summary.headline }),
1924
2041
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Details, { children: summary.details.map((detail) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Detail, { children: detail }, detail)) })
@@ -2014,6 +2131,7 @@ var ImageViewer = ({ src, alt, onClose }) => {
2014
2131
  var import_jsx_runtime8 = require("@emotion/react/jsx-runtime");
2015
2132
  var MARKDOWN_REMARK_PLUGINS = [import_remark_gfm.default, import_remark_math.default];
2016
2133
  var MARKDOWN_REHYPE_PLUGINS = [import_rehype_katex.default];
2134
+ var USER_MESSAGE_COLLAPSE_HEIGHT_PX = 120;
2017
2135
  var MARKDOWN_COMPONENTS = {
2018
2136
  table: ({ node: _node, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TableWrapper, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("table", { ...props }) })
2019
2137
  };
@@ -2026,10 +2144,101 @@ var renderMarkdownContent = (content) => /* @__PURE__ */ (0, import_jsx_runtime8
2026
2144
  children: content
2027
2145
  }
2028
2146
  );
2147
+ var renderPlainTextContent = (content) => content;
2148
+ var CollapseIcon = ({ expanded }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2149
+ "svg",
2150
+ {
2151
+ "aria-hidden": "true",
2152
+ width: "16",
2153
+ height: "16",
2154
+ viewBox: "0 0 16 16",
2155
+ fill: "none",
2156
+ xmlns: "http://www.w3.org/2000/svg",
2157
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2158
+ "path",
2159
+ {
2160
+ d: "M4 6l4 4 4-4",
2161
+ stroke: "currentColor",
2162
+ strokeWidth: "1.6",
2163
+ strokeLinecap: "round",
2164
+ strokeLinejoin: "round",
2165
+ transform: expanded ? "rotate(180 8 8)" : void 0
2166
+ }
2167
+ )
2168
+ }
2169
+ );
2170
+ var useUserMessageCollapse = ({
2171
+ blocks,
2172
+ displayedBlocks,
2173
+ displayedContent,
2174
+ enabled,
2175
+ freshContent,
2176
+ settledContent
2177
+ }) => {
2178
+ const [isCollapsible, setIsCollapsible] = (0, import_react9.useState)(false);
2179
+ const [isExpanded, setIsExpanded] = (0, import_react9.useState)(false);
2180
+ const [bodyStackElement, setBodyStackElement] = (0, import_react9.useState)(null);
2181
+ const syncCollapseState = (0, import_react9.useCallback)(
2182
+ (element) => {
2183
+ const nextCollapsible = enabled && (element?.scrollHeight ?? 0) > USER_MESSAGE_COLLAPSE_HEIGHT_PX;
2184
+ setIsCollapsible(nextCollapsible);
2185
+ if (!nextCollapsible) {
2186
+ setIsExpanded(false);
2187
+ }
2188
+ },
2189
+ [enabled]
2190
+ );
2191
+ const bodyStackRef = (0, import_react9.useCallback)(
2192
+ (node) => {
2193
+ setBodyStackElement(node);
2194
+ syncCollapseState(node);
2195
+ },
2196
+ [syncCollapseState]
2197
+ );
2198
+ (0, import_react9.useLayoutEffect)(() => {
2199
+ if (!bodyStackElement) {
2200
+ return;
2201
+ }
2202
+ const frameId = requestAnimationFrame(() => {
2203
+ syncCollapseState(bodyStackElement);
2204
+ });
2205
+ return () => {
2206
+ cancelAnimationFrame(frameId);
2207
+ };
2208
+ }, [
2209
+ blocks,
2210
+ bodyStackElement,
2211
+ displayedBlocks,
2212
+ displayedContent,
2213
+ enabled,
2214
+ freshContent,
2215
+ settledContent,
2216
+ syncCollapseState
2217
+ ]);
2218
+ (0, import_react9.useLayoutEffect)(() => {
2219
+ if (!bodyStackElement || !enabled || typeof ResizeObserver === "undefined") {
2220
+ return;
2221
+ }
2222
+ const observer = new ResizeObserver(() => {
2223
+ syncCollapseState(bodyStackElement);
2224
+ });
2225
+ observer.observe(bodyStackElement);
2226
+ return () => {
2227
+ observer.disconnect();
2228
+ };
2229
+ }, [bodyStackElement, enabled, syncCollapseState]);
2230
+ return {
2231
+ bodyStackRef,
2232
+ isCollapsed: isCollapsible && !isExpanded,
2233
+ isCollapsible,
2234
+ isExpanded,
2235
+ toggleExpanded: () => setIsExpanded((current) => !current)
2236
+ };
2237
+ };
2029
2238
  var createExecutionConfirmationContent = (proposal) => [
2030
2239
  "Execution confirmed",
2031
- `- Equation: ${proposal.equationName}`,
2032
- ...proposal.solverName ? [`- Solver: ${proposal.solverName}`] : [],
2240
+ `- Equation: ${proposal.resourceName}`,
2241
+ ...proposal.executorName ? [`- Solver: ${proposal.executorName}`] : [],
2033
2242
  `- Proposal ID: ${proposal.proposalId}`
2034
2243
  ].join("\n");
2035
2244
  var areChatAttachmentsEqual = (previousAttachments, nextAttachments) => {
@@ -2051,10 +2260,10 @@ var areParameterSummaryItemsEqual = (previousItems, nextItems) => previousItems.
2051
2260
  const nextItem = nextItems[index];
2052
2261
  return item.label === nextItem?.label && item.value === nextItem.value && item.fieldPath === nextItem.fieldPath;
2053
2262
  });
2054
- var areExecutionProposalsEqual = (previousProposal, nextProposal) => previousProposal.proposalId === nextProposal.proposalId && previousProposal.equationKey === nextProposal.equationKey && previousProposal.equationName === nextProposal.equationName && previousProposal.solverName === nextProposal.solverName && previousProposal.requiresConfirmation === nextProposal.requiresConfirmation && areParameterSummaryItemsEqual(previousProposal.parameterSummary, nextProposal.parameterSummary) && (previousProposal.warnings ?? []).length === (nextProposal.warnings ?? []).length && (previousProposal.warnings ?? []).every(
2263
+ var areExecutionProposalsEqual = (previousProposal, nextProposal) => previousProposal.proposalId === nextProposal.proposalId && previousProposal.resourceKey === nextProposal.resourceKey && previousProposal.resourceName === nextProposal.resourceName && previousProposal.executorName === nextProposal.executorName && previousProposal.requiresConfirmation === nextProposal.requiresConfirmation && areParameterSummaryItemsEqual(previousProposal.parameterSummary, nextProposal.parameterSummary) && (previousProposal.warnings ?? []).length === (nextProposal.warnings ?? []).length && (previousProposal.warnings ?? []).every(
2055
2264
  (warning, index) => warning === nextProposal.warnings?.[index]
2056
2265
  );
2057
- var areResultSummariesEqual = (previousSummary, nextSummary) => previousSummary.taskId === nextSummary.taskId && previousSummary.status === nextSummary.status && previousSummary.headline === nextSummary.headline && previousSummary.details.length === nextSummary.details.length && previousSummary.details.every((detail, index) => detail === nextSummary.details[index]);
2266
+ var areResultSummariesEqual = (previousSummary, nextSummary) => previousSummary.summaryId === nextSummary.summaryId && previousSummary.status === nextSummary.status && previousSummary.headline === nextSummary.headline && previousSummary.details.length === nextSummary.details.length && previousSummary.details.every((detail, index) => detail === nextSummary.details[index]);
2058
2267
  var areStringArraysEqual = (previousValues, nextValues) => previousValues.length === nextValues.length && previousValues.every((value, index) => value === nextValues[index]);
2059
2268
  var areQuestionAnswersEqual = (previousAnswer, nextAnswer) => {
2060
2269
  if (Array.isArray(previousAnswer) || Array.isArray(nextAnswer)) {
@@ -2095,7 +2304,7 @@ var arePlanQuestionsEqual = (previousQuestion, nextQuestion) => {
2095
2304
  return false;
2096
2305
  }
2097
2306
  };
2098
- var areQuestionnairesEqual = (previousQuestionnaire, nextQuestionnaire) => previousQuestionnaire.questionnaireId === nextQuestionnaire.questionnaireId && previousQuestionnaire.title === nextQuestionnaire.title && previousQuestionnaire.description === nextQuestionnaire.description && previousQuestionnaire.submitLabel === nextQuestionnaire.submitLabel && previousQuestionnaire.questions.length === nextQuestionnaire.questions.length && previousQuestionnaire.questions.every(
2307
+ var areQuestionnairesEqual = (previousQuestionnaire, nextQuestionnaire) => previousQuestionnaire.questionnaireId === nextQuestionnaire.questionnaireId && previousQuestionnaire.title === nextQuestionnaire.title && previousQuestionnaire.description === nextQuestionnaire.description && previousQuestionnaire.submitLabel === nextQuestionnaire.submitLabel && previousQuestionnaire.status === nextQuestionnaire.status && previousQuestionnaire.statusMessage === nextQuestionnaire.statusMessage && previousQuestionnaire.questions.length === nextQuestionnaire.questions.length && previousQuestionnaire.questions.every(
2099
2308
  (question, index) => arePlanQuestionsEqual(question, nextQuestionnaire.questions[index])
2100
2309
  ) && areQuestionAnswerMapsEqual(previousQuestionnaire.answers, nextQuestionnaire.answers);
2101
2310
  var areMessageBlocksEqual = (previousBlocks, nextBlocks) => {
@@ -2176,6 +2385,8 @@ var ChatMessageItemView = ({
2176
2385
  const canSubmitConfirmation = isPlanMode && typeof onConfirmationSubmit === "function";
2177
2386
  const canSubmitQuestionnaire = isPlanMode && typeof onQuestionnaireSubmit === "function";
2178
2387
  const shouldShowStreamingCaret = isAssistantStreaming && (!shouldRenderStructuredBlocks || hasTextContent);
2388
+ const isUserMessage = message.role === "user";
2389
+ const messageRenderMode = isUserMessage ? "plain-text" : "markdown";
2179
2390
  const timelineConsumedText = messageRenderOrder === "timeline" ? getTimelineConsumedText(blocks) : "";
2180
2391
  const hasConsumedTimelineText = timelineConsumedText.length > 0 && displayedContent.startsWith(timelineConsumedText);
2181
2392
  const timelineDisplayedContent = hasConsumedTimelineText ? displayedContent.slice(timelineConsumedText.length) : displayedContent;
@@ -2192,6 +2403,21 @@ var ChatMessageItemView = ({
2192
2403
  message,
2193
2404
  messageRenderOrder
2194
2405
  });
2406
+ const {
2407
+ bodyStackRef,
2408
+ isCollapsed: isUserMessageCollapsed,
2409
+ isCollapsible: isUserMessageCollapsible,
2410
+ isExpanded: isUserMessageExpanded,
2411
+ toggleExpanded: toggleUserMessageExpanded
2412
+ } = useUserMessageCollapse({
2413
+ blocks,
2414
+ displayedBlocks,
2415
+ displayedContent,
2416
+ enabled: isUserMessage,
2417
+ freshContent,
2418
+ settledContent
2419
+ });
2420
+ const renderMessageContent = (content) => messageRenderMode === "plain-text" ? renderPlainTextContent(content) : renderMarkdownContent(content);
2195
2421
  const renderChatMessageBlock = (block, index) => {
2196
2422
  switch (block.type) {
2197
2423
  case "markdown":
@@ -2200,17 +2426,18 @@ var ChatMessageItemView = ({
2200
2426
  {
2201
2427
  "data-testid": `chat-message-block-${index}`,
2202
2428
  "data-block-tone": "settled",
2203
- children: renderMarkdownContent(block.text)
2429
+ "data-render-mode": messageRenderMode,
2430
+ children: renderMessageContent(block.text)
2204
2431
  },
2205
2432
  `markdown-${index}`
2206
2433
  );
2207
2434
  case "notice":
2208
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react9.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PDEAINoticeCard, { text: block.text, tone: block.tone }) }, `notice-${index}`);
2435
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react9.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(NoticeCard, { text: block.text, tone: block.tone }) }, `notice-${index}`);
2209
2436
  case "parameter_summary":
2210
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react9.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PDEAIParameterSummaryCard, { items: block.items }) }, `parameter-summary-${index}`);
2437
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react9.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ParameterSummaryCard, { items: block.items }) }, `parameter-summary-${index}`);
2211
2438
  case "confirmation_card":
2212
2439
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react9.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2213
- PDEAIExecutionConfirmationCard,
2440
+ ExecutionConfirmationCard,
2214
2441
  {
2215
2442
  proposal: block.proposal,
2216
2443
  interactive: isPlanMode,
@@ -2222,13 +2449,19 @@ var ChatMessageItemView = ({
2222
2449
  }
2223
2450
  ) }, `confirmation-card-${index}`);
2224
2451
  case "result_summary":
2225
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react9.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PDEAIResultSummaryCard, { summary: block.summary }) }, `result-summary-${index}`);
2452
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react9.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ResultSummaryCard, { summary: block.summary }) }, `result-summary-${index}`);
2226
2453
  case "questionnaire":
2227
2454
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react9.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2228
- PDEAIQuestionnaireCard,
2455
+ QuestionnaireCard,
2229
2456
  {
2230
2457
  questionnaire: block.questionnaire,
2231
2458
  interactive: canSubmitQuestionnaire,
2459
+ labels: {
2460
+ submitting: labels.questionnaireSubmitting,
2461
+ submitted: labels.questionnaireSubmitted,
2462
+ validationPrefix: labels.questionnaireValidationPrefix,
2463
+ submitFailed: labels.questionnaireSubmitFailed
2464
+ },
2232
2465
  onSubmit: canSubmitQuestionnaire ? (submission) => onQuestionnaireSubmit({
2233
2466
  ...submission,
2234
2467
  sourceMessageId: message.id
@@ -2261,14 +2494,31 @@ var ChatMessageItemView = ({
2261
2494
  "data-testid": block.tone === "fresh" ? "chat-message-fresh-block" : "chat-message-settled-block",
2262
2495
  "data-block-tone": block.tone,
2263
2496
  "data-block-index": index,
2264
- children: renderMarkdownContent(block.content)
2497
+ "data-render-mode": messageRenderMode,
2498
+ children: renderMessageContent(block.content)
2265
2499
  },
2266
2500
  `${block.tone}-${index}`
2267
2501
  )),
2268
- !textBlocks.some((block) => block.content) && !settledText && !freshText && Boolean(textContent) ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ContentBlock, { "data-testid": "chat-message-settled-block", "data-block-tone": "settled", children: renderMarkdownContent(textContent) }) : null
2502
+ !textBlocks.some((block) => block.content) && !settledText && !freshText && Boolean(textContent) ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2503
+ ContentBlock,
2504
+ {
2505
+ "data-testid": "chat-message-settled-block",
2506
+ "data-block-tone": "settled",
2507
+ "data-render-mode": messageRenderMode,
2508
+ children: renderMessageContent(textContent)
2509
+ }
2510
+ ) : null
2269
2511
  ] });
2270
2512
  };
2271
- const renderStaticTextSegment = (content) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ContentBlock, { "data-testid": "chat-message-settled-block", "data-block-tone": "settled", children: renderMarkdownContent(content) });
2513
+ const renderStaticTextSegment = (content) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2514
+ ContentBlock,
2515
+ {
2516
+ "data-testid": "chat-message-settled-block",
2517
+ "data-block-tone": "settled",
2518
+ "data-render-mode": messageRenderMode,
2519
+ children: renderMessageContent(content)
2520
+ }
2521
+ );
2272
2522
  const bodySegments = (() => {
2273
2523
  if (!shouldRenderStructuredBlocks && hasTextContent) {
2274
2524
  return [{ type: "text" }];
@@ -2326,21 +2576,40 @@ var ChatMessageItemView = ({
2326
2576
  }
2327
2577
  ) : null,
2328
2578
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Role, { children: message.role === "user" ? labels.userRoleLabel : labels.assistantRoleLabel }),
2579
+ isUserMessageCollapsible ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2580
+ CollapseToggle,
2581
+ {
2582
+ type: "button",
2583
+ "data-testid": "chat-message-collapse-toggle",
2584
+ "aria-label": isUserMessageExpanded ? labels.collapseMessageAriaLabel : labels.expandMessageAriaLabel,
2585
+ "aria-expanded": isUserMessageExpanded,
2586
+ onClick: toggleUserMessageExpanded,
2587
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CollapseIcon, { expanded: isUserMessageExpanded })
2588
+ }
2589
+ ) : null,
2329
2590
  isStoppedAssistant ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(StatusTag, { "data-testid": "chat-message-stopped-tag", children: labels.stoppedResponse }) : null
2330
2591
  ] }),
2331
2592
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Content, { "data-testid": "chat-message-content", children: [
2332
- shouldRenderStructuredBlocks || hasTextContent ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ContentStack, { "data-testid": "chat-message-body-stack", children: bodySegments.map((segment, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2333
- ContentSegment,
2593
+ shouldRenderStructuredBlocks || hasTextContent ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2594
+ ContentStack,
2334
2595
  {
2335
- "data-testid": "chat-message-content-segment",
2336
- children: segment.type === "block" ? renderChatMessageBlock(segment.block, segment.index) : segment.type === "text" ? segment.content !== void 0 ? segment.useTimelineSegmentation ? renderTextContent({
2337
- content: segment.content,
2338
- displayedBlocks: segment.displayedBlocks,
2339
- useTimelineSegmentation: true
2340
- }) : renderStaticTextSegment(segment.content) : renderTextContent() : renderStaticTextSegment(segment.content)
2341
- },
2342
- segment.type === "text" ? `text-${index}` : segment.type === "markdown" ? `markdown-${index}` : `${segment.block.type}-${segment.index}`
2343
- )) }) : null,
2596
+ ref: bodyStackRef,
2597
+ "data-testid": "chat-message-body-stack",
2598
+ "data-collapsed": isUserMessageCollapsed,
2599
+ children: bodySegments.map((segment, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2600
+ ContentSegment,
2601
+ {
2602
+ "data-testid": "chat-message-content-segment",
2603
+ children: segment.type === "block" ? renderChatMessageBlock(segment.block, segment.index) : segment.type === "text" ? segment.content !== void 0 ? segment.useTimelineSegmentation ? renderTextContent({
2604
+ content: segment.content,
2605
+ displayedBlocks: segment.displayedBlocks,
2606
+ useTimelineSegmentation: true
2607
+ }) : renderStaticTextSegment(segment.content) : renderTextContent() : renderStaticTextSegment(segment.content)
2608
+ },
2609
+ segment.type === "text" ? `text-${index}` : segment.type === "markdown" ? `markdown-${index}` : `${segment.block.type}-${segment.index}`
2610
+ ))
2611
+ }
2612
+ ) : null,
2344
2613
  attachments.length ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AttachmentGrid, { "data-testid": "chat-message-attachment-grid", children: attachments.map((attachment) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2345
2614
  AttachmentButton,
2346
2615
  {
@@ -2425,6 +2694,29 @@ var StatusTag = import_styled7.default.span`
2425
2694
  letter-spacing: 0.02em;
2426
2695
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
2427
2696
  `;
2697
+ var CollapseToggle = import_styled7.default.button`
2698
+ margin-left: auto;
2699
+ width: 28px;
2700
+ height: 28px;
2701
+ display: inline-flex;
2702
+ align-items: center;
2703
+ justify-content: center;
2704
+ border: 1px solid rgba(255, 255, 255, 0.12);
2705
+ border-radius: 999px;
2706
+ background: rgba(255, 255, 255, 0.06);
2707
+ color: rgba(255, 255, 255, 0.72);
2708
+ cursor: pointer;
2709
+ transition:
2710
+ background 160ms ease-out,
2711
+ border-color 160ms ease-out,
2712
+ color 160ms ease-out;
2713
+
2714
+ &:hover {
2715
+ background: rgba(255, 255, 255, 0.1);
2716
+ border-color: rgba(255, 255, 255, 0.18);
2717
+ color: rgba(255, 255, 255, 0.92);
2718
+ }
2719
+ `;
2428
2720
  var Content = import_styled7.default.div`
2429
2721
  color: rgba(255, 255, 255, 0.92);
2430
2722
  line-height: 1.6;
@@ -2469,6 +2761,11 @@ var ContentStack = import_styled7.default.div`
2469
2761
  display: flex;
2470
2762
  flex-direction: column;
2471
2763
  gap: 16px;
2764
+
2765
+ &[data-collapsed='true'] {
2766
+ max-height: ${USER_MESSAGE_COLLAPSE_HEIGHT_PX}px;
2767
+ overflow: hidden;
2768
+ }
2472
2769
  `;
2473
2770
  var ContentSegment = import_styled7.default.div`
2474
2771
  min-width: 0;
@@ -2482,6 +2779,11 @@ var ContentBlock = import_styled7.default.div`
2482
2779
  margin-top: 16px;
2483
2780
  }
2484
2781
 
2782
+ &[data-render-mode='plain-text'] {
2783
+ white-space: pre-wrap;
2784
+ overflow-wrap: anywhere;
2785
+ }
2786
+
2485
2787
  &[data-block-tone='fresh'] {
2486
2788
  opacity: 0.85;
2487
2789
  filter: brightness(0.82) saturate(0.88);
@@ -2889,7 +3191,14 @@ var ChatThread = () => {
2889
3191
  const error = useChatStore((s) => s.errorBySession[s.activeSessionId ?? ""]);
2890
3192
  const updateQA = useChatStore((s) => s.updateQuestionnaireAnswers);
2891
3193
  const clearSessionError = useChatStore((s) => s.clearSessionError);
2892
- const { sendRef, retryRef, renderMessageBlock, labels } = useChatContext();
3194
+ const {
3195
+ sendRef,
3196
+ retryRef,
3197
+ renderMessageBlock,
3198
+ handleQuestionnaireSubmit: customQuestionnaireSubmit,
3199
+ handleConfirmationSubmit: customConfirmationSubmit,
3200
+ labels
3201
+ } = useChatContext();
2893
3202
  const handleRetry = (0, import_react11.useCallback)(() => {
2894
3203
  if (!activeSessionId)
2895
3204
  return;
@@ -2897,7 +3206,25 @@ var ChatThread = () => {
2897
3206
  void retryRef.current();
2898
3207
  }, [activeSessionId, clearSessionError, retryRef]);
2899
3208
  const handleQuestionnaireSubmit = (0, import_react11.useCallback)(
2900
- (submission) => {
3209
+ async (submission) => {
3210
+ if (customQuestionnaireSubmit) {
3211
+ const handled = await customQuestionnaireSubmit(submission, {
3212
+ sessionId: activeSessionId ?? void 0,
3213
+ mode: activeSessionMode
3214
+ });
3215
+ if (handled !== false) {
3216
+ if (activeSessionId && submission.sourceMessageId) {
3217
+ updateQA(
3218
+ activeSessionId,
3219
+ submission.sourceMessageId,
3220
+ submission.questionnaireId,
3221
+ submission.answers
3222
+ );
3223
+ }
3224
+ return;
3225
+ }
3226
+ }
3227
+ await sendRef.current(submission.content);
2901
3228
  if (activeSessionId && submission.sourceMessageId) {
2902
3229
  updateQA(
2903
3230
  activeSessionId,
@@ -2906,15 +3233,23 @@ var ChatThread = () => {
2906
3233
  submission.answers
2907
3234
  );
2908
3235
  }
2909
- void sendRef.current(submission.content);
2910
3236
  },
2911
- [activeSessionId, updateQA, sendRef]
3237
+ [activeSessionId, activeSessionMode, updateQA, sendRef, customQuestionnaireSubmit]
2912
3238
  );
2913
- const handleConfirmationSubmit = (0, import_react11.useCallback)(
2914
- (submission) => {
2915
- void sendRef.current(submission.content);
3239
+ const handleConfirmation = (0, import_react11.useCallback)(
3240
+ async (submission) => {
3241
+ if (customConfirmationSubmit) {
3242
+ const handled = await customConfirmationSubmit(submission, {
3243
+ sessionId: activeSessionId ?? void 0,
3244
+ mode: activeSessionMode
3245
+ });
3246
+ if (handled !== false) {
3247
+ return;
3248
+ }
3249
+ }
3250
+ await sendRef.current(submission.content);
2916
3251
  },
2917
- [sendRef]
3252
+ [activeSessionId, activeSessionMode, sendRef, customConfirmationSubmit]
2918
3253
  );
2919
3254
  if (!hasSessions || messages.length === 0 && !streamingMessage) {
2920
3255
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatThreadEmptyState, {});
@@ -2928,7 +3263,7 @@ var ChatThread = () => {
2928
3263
  error,
2929
3264
  retryButtonLabel: labels.retryButton,
2930
3265
  onRetry: handleRetry,
2931
- onConfirmationSubmit: handleConfirmationSubmit,
3266
+ onConfirmationSubmit: handleConfirmation,
2932
3267
  onQuestionnaireSubmit: handleQuestionnaireSubmit,
2933
3268
  renderMessageBlock
2934
3269
  }
@@ -2942,6 +3277,7 @@ var Container = import_styled9.default.div`
2942
3277
  min-height: 0;
2943
3278
  overflow: auto;
2944
3279
  padding: 24px;
3280
+ margin-bottom: 24px;
2945
3281
  overscroll-behavior: contain;
2946
3282
 
2947
3283
  &::-webkit-scrollbar {
@@ -4021,6 +4357,27 @@ var StopSpinner = import_styled13.default.span`
4021
4357
 
4022
4358
  // src/components/chat-composer/index.tsx
4023
4359
  var import_jsx_runtime15 = require("@emotion/react/jsx-runtime");
4360
+ var CHAT_COMPOSER_LINE_HEIGHT_PX = 20;
4361
+ var CHAT_COMPOSER_MAX_ROWS = 7;
4362
+ var CHAT_COMPOSER_PADDING_TOP_PX = 8;
4363
+ var CHAT_COMPOSER_PADDING_BOTTOM_PX = 12;
4364
+ var CHAT_COMPOSER_PADDING_BLOCK_PX = CHAT_COMPOSER_PADDING_TOP_PX + CHAT_COMPOSER_PADDING_BOTTOM_PX;
4365
+ var CHAT_COMPOSER_MIN_ROWS = 4;
4366
+ var CHAT_COMPOSER_EXPANDED_MAX_ROWS = 60;
4367
+ var CHAT_COMPOSER_EXPANDED_MAX_VIEWPORT_RATIO = 0.7;
4368
+ var CHAT_COMPOSER_EXPANDED_RESERVED_SPACE_PX = 96;
4369
+ var CHAT_COMPOSER_MAX_HEIGHT_PX = CHAT_COMPOSER_MAX_ROWS * CHAT_COMPOSER_LINE_HEIGHT_PX + CHAT_COMPOSER_PADDING_BLOCK_PX;
4370
+ var CHAT_COMPOSER_EXPANDED_ROWS_HEIGHT_PX = CHAT_COMPOSER_EXPANDED_MAX_ROWS * CHAT_COMPOSER_LINE_HEIGHT_PX + CHAT_COMPOSER_PADDING_BLOCK_PX;
4371
+ var getExpandedComposerHeightPx = () => {
4372
+ if (typeof window === "undefined") {
4373
+ return CHAT_COMPOSER_EXPANDED_ROWS_HEIGHT_PX;
4374
+ }
4375
+ const viewportLimitedHeight = window.innerHeight * CHAT_COMPOSER_EXPANDED_MAX_VIEWPORT_RATIO - CHAT_COMPOSER_EXPANDED_RESERVED_SPACE_PX;
4376
+ return Math.max(
4377
+ CHAT_COMPOSER_MAX_HEIGHT_PX,
4378
+ Math.floor(Math.min(CHAT_COMPOSER_EXPANDED_ROWS_HEIGHT_PX, viewportLimitedHeight))
4379
+ );
4380
+ };
4024
4381
  var PlusIcon = () => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4025
4382
  "svg",
4026
4383
  {
@@ -4033,6 +4390,36 @@ var PlusIcon = () => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4033
4390
  children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("path", { d: "M8 3v10M3 8h10", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round" })
4034
4391
  }
4035
4392
  );
4393
+ var ComposerExpandIcon = ({ expanded }) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4394
+ "svg",
4395
+ {
4396
+ "aria-hidden": "true",
4397
+ width: "16",
4398
+ height: "16",
4399
+ viewBox: "0 0 16 16",
4400
+ fill: "none",
4401
+ xmlns: "http://www.w3.org/2000/svg",
4402
+ children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4403
+ "path",
4404
+ {
4405
+ d: "M14 6h-4V2M10 6l4-4M2 10h4v4M6 10l-4 4",
4406
+ stroke: "currentColor",
4407
+ strokeWidth: "1.5",
4408
+ strokeLinecap: "round",
4409
+ strokeLinejoin: "round"
4410
+ }
4411
+ ) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4412
+ "path",
4413
+ {
4414
+ d: "M10 2h4v4M14 2L9 7M6 14H2v-4M2 14l5-5",
4415
+ stroke: "currentColor",
4416
+ strokeWidth: "1.5",
4417
+ strokeLinecap: "round",
4418
+ strokeLinejoin: "round"
4419
+ }
4420
+ )
4421
+ }
4422
+ );
4036
4423
  var ChatComposerView = ({
4037
4424
  value,
4038
4425
  placeholder,
@@ -4049,6 +4436,8 @@ var ChatComposerView = ({
4049
4436
  isStopping,
4050
4437
  enableImageAttachments,
4051
4438
  modeLabels,
4439
+ expandComposerAriaLabel,
4440
+ collapseComposerAriaLabel,
4052
4441
  onValueChange,
4053
4442
  onPickImages,
4054
4443
  onPasteImages,
@@ -4060,6 +4449,9 @@ var ChatComposerView = ({
4060
4449
  onSend
4061
4450
  }) => {
4062
4451
  const imageInputRef = (0, import_react15.useRef)(null);
4452
+ const inputRef = (0, import_react15.useRef)(null);
4453
+ const [isComposerExpandable, setIsComposerExpandable] = (0, import_react15.useState)(false);
4454
+ const [isComposerExpanded, setIsComposerExpanded] = (0, import_react15.useState)(false);
4063
4455
  const canSend = canSendChatMessage({
4064
4456
  value,
4065
4457
  attachmentCount: attachments.length,
@@ -4067,6 +4459,26 @@ var ChatComposerView = ({
4067
4459
  isModelsError,
4068
4460
  hasModels
4069
4461
  });
4462
+ (0, import_react15.useLayoutEffect)(() => {
4463
+ const element = inputRef.current;
4464
+ if (!element) {
4465
+ return;
4466
+ }
4467
+ if (!isComposerExpanded) {
4468
+ element.style.height = "0px";
4469
+ }
4470
+ const scrollHeight = element.scrollHeight;
4471
+ const nextExpandable = scrollHeight > CHAT_COMPOSER_MAX_HEIGHT_PX;
4472
+ const expandedHeight = getExpandedComposerHeightPx();
4473
+ const nextHeight = isComposerExpanded ? expandedHeight : Math.min(scrollHeight, CHAT_COMPOSER_MAX_HEIGHT_PX);
4474
+ setIsComposerExpandable(nextExpandable);
4475
+ element.style.height = `${nextHeight}px`;
4476
+ element.style.overflowY = !isComposerExpanded && scrollHeight > CHAT_COMPOSER_MAX_HEIGHT_PX ? "auto" : "hidden";
4477
+ }, [isComposerExpanded, value]);
4478
+ const handleSend = async () => {
4479
+ setIsComposerExpanded(false);
4480
+ await onSend();
4481
+ };
4070
4482
  const handleKeyDown = (event) => {
4071
4483
  if (shouldStopChatComposer({ key: event.key, shiftKey: event.shiftKey, isStreaming })) {
4072
4484
  event.preventDefault();
@@ -4076,7 +4488,7 @@ var ChatComposerView = ({
4076
4488
  if (!shouldSubmitChatComposer({ key: event.key, shiftKey: event.shiftKey, canSend }))
4077
4489
  return;
4078
4490
  event.preventDefault();
4079
- void onSend();
4491
+ void handleSend();
4080
4492
  };
4081
4493
  const handlePickImages = (event) => {
4082
4494
  if (event.target.files?.length)
@@ -4111,60 +4523,77 @@ var ChatComposerView = ({
4111
4523
  }
4112
4524
  ),
4113
4525
  attachmentNotice === "limit_reached" ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(AttachmentNotice, { "data-testid": "chat-composer-attachment-notice", children: attachmentLimitNotice }) : null,
4114
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4115
- Input,
4116
- {
4117
- "data-testid": "chat-composer-input",
4118
- value,
4119
- onChange: (event) => onValueChange(event.target.value),
4120
- onKeyDown: handleKeyDown,
4121
- onPaste: enableImageAttachments ? handlePaste : void 0,
4122
- placeholder
4123
- }
4124
- ),
4125
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Footer, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(Actions2, { "data-testid": "chat-composer-actions", children: [
4126
- enableImageAttachments ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4127
- AttachButton,
4526
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(InputArea, { "data-testid": "chat-composer-input-area", children: [
4527
+ isComposerExpanded || isComposerExpandable ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4528
+ ComposerExpandButton,
4128
4529
  {
4129
4530
  type: "button",
4130
- "data-testid": "chat-composer-attach-image",
4131
- "aria-label": "Attach image",
4132
- onClick: () => imageInputRef.current?.click(),
4133
- children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PlusIcon, {})
4531
+ "data-testid": "chat-composer-expand-toggle",
4532
+ "aria-label": isComposerExpanded ? collapseComposerAriaLabel : expandComposerAriaLabel,
4533
+ "aria-expanded": isComposerExpanded,
4534
+ onClick: () => setIsComposerExpanded((current) => !current),
4535
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ComposerExpandIcon, { expanded: isComposerExpanded })
4134
4536
  }
4135
4537
  ) : null,
4136
4538
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4137
- ChatModeControl,
4138
- {
4139
- value: selectedMode,
4140
- disabled: isStreaming,
4141
- labels: modeLabels,
4142
- onChange: onSelectedModeChange
4143
- }
4144
- ),
4145
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4146
- ChatModelControl,
4539
+ Input,
4147
4540
  {
4148
- selectedModel,
4149
- availableModels,
4150
- isModelsLoading,
4151
- isModelsError,
4152
- hasModels,
4153
- onSelectedModelChange,
4154
- onReloadModels
4541
+ ref: inputRef,
4542
+ "data-testid": "chat-composer-input",
4543
+ "data-expanded": isComposerExpanded,
4544
+ value,
4545
+ onChange: (event) => onValueChange(event.target.value),
4546
+ onKeyDown: handleKeyDown,
4547
+ onPaste: enableImageAttachments ? handlePaste : void 0,
4548
+ placeholder
4155
4549
  }
4156
- ),
4157
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4158
- ChatSendActions,
4550
+ )
4551
+ ] }),
4552
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(Footer, { children: [
4553
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(LeadingActions, { "data-testid": "chat-composer-leading-actions", children: enableImageAttachments ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4554
+ AttachButton,
4159
4555
  {
4160
- canSend,
4161
- isStreaming,
4162
- isStopping,
4163
- onStop,
4164
- onSend
4556
+ type: "button",
4557
+ "data-testid": "chat-composer-attach-image",
4558
+ "aria-label": "Attach image",
4559
+ onClick: () => imageInputRef.current?.click(),
4560
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PlusIcon, {})
4165
4561
  }
4166
- )
4167
- ] }) })
4562
+ ) : null }),
4563
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(TrailingActions, { "data-testid": "chat-composer-trailing-actions", children: [
4564
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4565
+ ChatModeControl,
4566
+ {
4567
+ value: selectedMode,
4568
+ disabled: isStreaming,
4569
+ labels: modeLabels,
4570
+ onChange: onSelectedModeChange
4571
+ }
4572
+ ),
4573
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4574
+ ChatModelControl,
4575
+ {
4576
+ selectedModel,
4577
+ availableModels,
4578
+ isModelsLoading,
4579
+ isModelsError,
4580
+ hasModels,
4581
+ onSelectedModelChange,
4582
+ onReloadModels
4583
+ }
4584
+ ),
4585
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4586
+ ChatSendActions,
4587
+ {
4588
+ canSend,
4589
+ isStreaming,
4590
+ isStopping,
4591
+ onStop,
4592
+ onSend: handleSend
4593
+ }
4594
+ )
4595
+ ] })
4596
+ ] })
4168
4597
  ] }) });
4169
4598
  };
4170
4599
  var ChatComposer = () => {
@@ -4200,6 +4629,8 @@ var ChatComposer = () => {
4200
4629
  isStopping: state.isStopping,
4201
4630
  enableImageAttachments,
4202
4631
  modeLabels,
4632
+ expandComposerAriaLabel: labels.expandComposerAriaLabel,
4633
+ collapseComposerAriaLabel: labels.collapseComposerAriaLabel,
4203
4634
  onValueChange: actions.setValue,
4204
4635
  onPickImages: actions.pickImages,
4205
4636
  onPasteImages: actions.pasteImages,
@@ -4216,6 +4647,16 @@ var Container2 = import_styled14.default.div`
4216
4647
  padding: 0 16px 16px;
4217
4648
  `;
4218
4649
  var Surface = import_styled14.default.div`
4650
+ display: grid;
4651
+ grid-template-columns: minmax(0, 1fr);
4652
+ grid-template-areas:
4653
+ 'attachments'
4654
+ 'notice'
4655
+ 'input'
4656
+ 'footer';
4657
+ width: 100%;
4658
+ max-width: 760px;
4659
+ margin: 0 auto;
4219
4660
  background: var(--border-color);
4220
4661
  border-radius: 20px;
4221
4662
  border: 1px solid var(--border-hover);
@@ -4225,6 +4666,7 @@ var Surface = import_styled14.default.div`
4225
4666
  backdrop-filter: blur(10px);
4226
4667
  `;
4227
4668
  var AttachmentNotice = import_styled14.default.div`
4669
+ grid-area: notice;
4228
4670
  margin: 10px 12px 0;
4229
4671
  padding: 8px 10px;
4230
4672
  border-radius: 10px;
@@ -4234,19 +4676,45 @@ var AttachmentNotice = import_styled14.default.div`
4234
4676
  font-size: 12px;
4235
4677
  line-height: 1.4;
4236
4678
  `;
4679
+ var InputArea = import_styled14.default.div`
4680
+ grid-area: input;
4681
+ position: relative;
4682
+ `;
4237
4683
  var Input = import_styled14.default.textarea`
4684
+ --textarea-line-height: ${CHAT_COMPOSER_LINE_HEIGHT_PX}px;
4685
+ --textarea-min-rows: ${CHAT_COMPOSER_MIN_ROWS};
4686
+ --textarea-max-rows: ${CHAT_COMPOSER_MAX_ROWS};
4687
+ --textarea-expanded-max-rows: ${CHAT_COMPOSER_EXPANDED_MAX_ROWS};
4688
+ --textarea-padding-top: ${CHAT_COMPOSER_PADDING_TOP_PX}px;
4689
+ --textarea-padding-bottom: ${CHAT_COMPOSER_PADDING_BOTTOM_PX}px;
4690
+ --textarea-padding-block: calc(var(--textarea-padding-top) + var(--textarea-padding-bottom));
4691
+ --textarea-max-height: calc(
4692
+ var(--textarea-max-rows) * var(--textarea-line-height) + var(--textarea-padding-block)
4693
+ );
4694
+ --textarea-expanded-max-height: min(
4695
+ calc(
4696
+ var(--textarea-expanded-max-rows) * var(--textarea-line-height) +
4697
+ var(--textarea-padding-block)
4698
+ ),
4699
+ calc(70vh - ${CHAT_COMPOSER_EXPANDED_RESERVED_SPACE_PX}px)
4700
+ );
4238
4701
  width: 100%;
4239
- min-height: 96px;
4702
+ min-height: calc(
4703
+ var(--textarea-min-rows) * var(--textarea-line-height) + var(--textarea-padding-block)
4704
+ );
4705
+ max-height: var(--textarea-max-height);
4706
+ box-sizing: border-box;
4240
4707
  resize: none;
4241
4708
  appearance: none;
4242
4709
  border: 0;
4243
4710
  outline: 0;
4244
4711
  background: transparent;
4245
- padding: 8px 12px 12px 12px;
4712
+ padding: var(--textarea-padding-top) 44px var(--textarea-padding-bottom) 12px;
4246
4713
  font-weight: 400;
4247
4714
  font-size: 14px;
4248
4715
  color: var(--text-primary);
4249
- line-height: 20px;
4716
+ line-height: var(--textarea-line-height);
4717
+ overflow-y: hidden;
4250
4718
 
4251
4719
  &::placeholder {
4252
4720
  color: var(--text-secondary);
@@ -4255,19 +4723,50 @@ var Input = import_styled14.default.textarea`
4255
4723
  &::-webkit-resizer {
4256
4724
  display: none;
4257
4725
  }
4726
+
4727
+ &[data-expanded='true'] {
4728
+ max-height: var(--textarea-expanded-max-height);
4729
+ }
4730
+ `;
4731
+ var ComposerExpandButton = import_styled14.default.button`
4732
+ position: absolute;
4733
+ top: 8px;
4734
+ right: 10px;
4735
+ width: 28px;
4736
+ height: 28px;
4737
+ display: grid;
4738
+ place-items: center;
4739
+ border: none;
4740
+ border-radius: 999px;
4741
+ background: transparent;
4742
+ color: rgba(255, 255, 255, 0.72);
4743
+ cursor: pointer;
4744
+ z-index: 1;
4745
+
4746
+ &:hover {
4747
+ background: rgba(255, 255, 255, 0.08);
4748
+ color: rgba(255, 255, 255, 0.92);
4749
+ }
4258
4750
  `;
4259
4751
  var Footer = import_styled14.default.div`
4260
- display: flex;
4752
+ grid-area: footer;
4753
+ display: grid;
4754
+ grid-template-columns: minmax(0, 1fr) auto;
4261
4755
  align-items: flex-end;
4262
- justify-content: stretch;
4263
4756
  gap: 16px;
4264
4757
  padding: 0 14px 14px;
4265
4758
  `;
4266
- var Actions2 = import_styled14.default.div`
4759
+ var LeadingActions = import_styled14.default.div`
4760
+ display: flex;
4761
+ align-items: center;
4762
+ justify-content: flex-start;
4763
+ gap: 8px;
4764
+ min-width: 0;
4765
+ `;
4766
+ var TrailingActions = import_styled14.default.div`
4267
4767
  display: flex;
4268
4768
  align-items: center;
4269
4769
  flex-wrap: wrap;
4270
- width: 100%;
4271
4770
  min-width: 0;
4272
4771
  justify-content: flex-end;
4273
4772
  gap: 8px;