@flamingo-stack/openframe-frontend-core 0.0.751 → 0.0.753

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-FHT7SMA7.cjs → chunk-35BJY5RR.cjs} +47 -47
  3. package/dist/{chunk-ALPRQ3ZX.cjs → chunk-3GG3QAPT.cjs} +15 -15
  4. package/dist/{chunk-34TWK3JE.cjs → chunk-5E6JHOGX.cjs} +3 -3
  5. package/dist/{chunk-OFO75R2O.cjs → chunk-7Z4EQOIX.cjs} +174 -174
  6. package/dist/{chunk-C6LPOYDM.cjs → chunk-AG4HR6Z6.cjs} +8 -8
  7. package/dist/{chunk-XJ3J4UIS.cjs → chunk-B27XV2TC.cjs} +19 -19
  8. package/dist/{chunk-MJFTWHV6.cjs → chunk-C6JHDRK3.cjs} +6 -6
  9. package/dist/{chunk-2YXOD2KL.cjs → chunk-H3RYUTND.cjs} +5 -0
  10. package/dist/{chunk-53I4YHRT.cjs → chunk-ITQZK3UT.cjs} +3 -3
  11. package/dist/{chunk-H52GKJ36.cjs → chunk-J3E5UN2O.cjs} +19 -19
  12. package/dist/{chunk-5WC535NJ.cjs → chunk-JLAYELWS.cjs} +40 -40
  13. package/dist/{chunk-UGCRLRCT.cjs → chunk-NO5UJLLQ.cjs} +2 -5
  14. package/dist/{chunk-5P7WBA2U.cjs → chunk-QXQHCX2R.cjs} +4 -4
  15. package/dist/{chunk-ESRA7NUI.cjs → chunk-R45NJ5ZA.cjs} +90 -90
  16. package/dist/{chunk-CVBHB4OZ.cjs → chunk-RGYLC3US.cjs} +7 -7
  17. package/dist/{chunk-TPH4SRR2.cjs → chunk-SDF5UUTE.cjs} +800 -659
  18. package/dist/{chunk-3TELPOVH.cjs → chunk-T4BBZ5ST.cjs} +2 -2
  19. package/dist/{chunk-CLAJIQ7E.cjs → chunk-UNRNLSG4.cjs} +19 -19
  20. package/dist/{chunk-NOUROZYH.cjs → chunk-UVTJT5BK.cjs} +24 -24
  21. package/dist/{chunk-77IKUBSH.cjs → chunk-W77VPNBJ.cjs} +12 -12
  22. package/dist/{chunk-DGEB5GUZ.cjs → chunk-WBOHLXFC.cjs} +2 -2
  23. package/dist/{chunk-X4LAMJHD.cjs → chunk-WIT36ZTN.cjs} +113 -113
  24. package/dist/{chunk-QOONZMSQ.cjs → chunk-XSOGT6DF.cjs} +28 -28
  25. package/dist/{chunk-LIGDPFDM.cjs → chunk-XW6WSW36.cjs} +771 -771
  26. package/dist/{chunk-4WNBVNGP.cjs → chunk-Y7AZCDLZ.cjs} +20 -20
  27. package/dist/{chunk-SDOCZTST.cjs → chunk-Z6TUOE6L.cjs} +41 -41
  28. package/dist/{chunk-RVLQHSID.cjs → chunk-ZGADZHLD.cjs} +101 -101
  29. package/dist/{chunk-MU7FFN6M.cjs → chunk-ZJ77IOMY.cjs} +83 -83
  30. package/dist/{cloud-tenants-JDPC2DYF.cjs → cloud-tenants-7LTS4DAP.cjs} +11 -11
  31. package/dist/components/case-studies/index.cjs +24 -24
  32. package/dist/components/chat/chat-dialog-menu-items.js +6 -6
  33. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  34. package/dist/components/chat/chat-message-enhanced.js +3 -0
  35. package/dist/components/chat/chat-panel-context.d.ts +3 -0
  36. package/dist/components/chat/chat-panel-context.d.ts.map +1 -1
  37. package/dist/components/chat/embeddable-chat.d.ts +20 -0
  38. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  39. package/dist/components/chat/embeddable-chat.js +78 -8
  40. package/dist/components/chat/hooks/index.d.ts +1 -0
  41. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  42. package/dist/components/chat/hooks/index.js +1 -0
  43. package/dist/components/chat/hooks/use-what-to-ask.d.ts +69 -0
  44. package/dist/components/chat/hooks/use-what-to-ask.d.ts.map +1 -0
  45. package/dist/components/chat/hooks/use-what-to-ask.js +106 -0
  46. package/dist/components/chat/index.cjs +15 -7
  47. package/dist/components/chat/index.d.ts +1 -0
  48. package/dist/components/chat/index.d.ts.map +1 -1
  49. package/dist/components/chat/index.js +1 -0
  50. package/dist/components/chat/mingo-chat-header.d.ts +4 -2
  51. package/dist/components/chat/mingo-chat-header.d.ts.map +1 -1
  52. package/dist/components/chat/mingo-chat-header.js +7 -2
  53. package/dist/components/chat/suggestions-display.d.ts +11 -0
  54. package/dist/components/chat/suggestions-display.d.ts.map +1 -0
  55. package/dist/components/chat/suggestions-display.js +27 -0
  56. package/dist/components/chat/types/message.types.d.ts +27 -1
  57. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  58. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  59. package/dist/components/chat/utils/extract-incomplete-message-state.js +2 -0
  60. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  61. package/dist/components/chat/utils/message-segment-accumulator.js +3 -0
  62. package/dist/components/contact/index.cjs +12 -12
  63. package/dist/components/docs/index.cjs +15 -15
  64. package/dist/components/embeds/index.cjs +12 -12
  65. package/dist/components/faq/index.cjs +13 -13
  66. package/dist/components/features/index.cjs +11 -11
  67. package/dist/components/help-center-pages/index.cjs +112 -112
  68. package/dist/components/index.cjs +69 -61
  69. package/dist/components/loading/loading-region.d.ts.map +1 -1
  70. package/dist/components/loading/loading-region.js +2 -5
  71. package/dist/components/loading/unified-skeleton.d.ts +2 -0
  72. package/dist/components/loading/unified-skeleton.d.ts.map +1 -1
  73. package/dist/components/loading/unified-skeleton.js +5 -0
  74. package/dist/components/meeting-scheduler/index.cjs +72 -72
  75. package/dist/components/navigation/index.cjs +14 -14
  76. package/dist/components/onboarding-guides/index.cjs +14 -14
  77. package/dist/components/product-demo/index.cjs +12 -12
  78. package/dist/components/related-content/index.cjs +7 -7
  79. package/dist/components/tickets/index.cjs +17 -17
  80. package/dist/components/ui/file-manager/index.cjs +30 -30
  81. package/dist/components/ui/index.cjs +19 -11
  82. package/dist/{devices-ADQWD37J.cjs → devices-CGDJAYRB.cjs} +12 -12
  83. package/dist/{embeddable-chat-7YF2JMPS.cjs → embeddable-chat-CMEHSDV7.cjs} +165 -93
  84. package/dist/index.cjs +23 -15
  85. package/dist/{knowledge-TNS64EGK.cjs → knowledge-X5I4QJKV.cjs} +12 -12
  86. package/dist/{logs-754MDDE4.cjs → logs-GDGIRJFI.cjs} +19 -19
  87. package/dist/{policies-Q23DPIH6.cjs → policies-2UOCENDX.cjs} +15 -15
  88. package/dist/{remote-session-ZKP2W3IS.cjs → remote-session-57DGC4X3.cjs} +6 -6
  89. package/dist/{scripts-DCYLK2RZ.cjs → scripts-AS4GEG6L.cjs} +18 -18
  90. package/dist/{software-update-XCMGUIER.cjs → software-update-ADPUFMFY.cjs} +15 -15
  91. package/dist/{tickets-board-BNW4AYFL.cjs → tickets-board-EP6L3BSV.cjs} +7 -7
  92. package/package.json +1 -1
  93. package/src/components/chat/chat-dialog-menu-items.tsx +6 -6
  94. package/src/components/chat/chat-message-enhanced.tsx +3 -0
  95. package/src/components/chat/chat-panel-context.tsx +3 -0
  96. package/src/components/chat/embeddable-chat.tsx +132 -9
  97. package/src/components/chat/hooks/index.ts +1 -0
  98. package/src/components/chat/hooks/use-what-to-ask.ts +203 -0
  99. package/src/components/chat/index.ts +1 -0
  100. package/src/components/chat/mingo-chat-header.tsx +11 -4
  101. package/src/components/chat/suggestions-display.tsx +41 -0
  102. package/src/components/chat/types/message.types.ts +31 -1
  103. package/src/components/chat/utils/extract-incomplete-message-state.ts +2 -0
  104. package/src/components/chat/utils/message-segment-accumulator.ts +4 -0
  105. package/src/components/loading/loading-region.tsx +4 -9
  106. package/src/components/loading/unified-skeleton.tsx +13 -0
@@ -23,6 +23,7 @@ import { EscalationOfferMessage } from './escalation-offer-message';
23
23
  import { remarkCardLinks } from './remark-card-links';
24
24
  import { remarkMentionChips } from './remark-mention-chips';
25
25
  import { remarkStripCitations } from './remark-strip-citations';
26
+ import { SuggestionsDisplay } from './suggestions-display';
26
27
  import { SystemEventMessage } from './system-event-message';
27
28
  import { ThinkingDisplay } from './thinking-display';
28
29
  import { TicketEscalatedMessage } from './ticket-escalated-message';
@@ -647,6 +648,8 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
647
648
  const run = askRuns.get(index);
648
649
  if (!run) return null;
649
650
  return <AskDisplay key={index} cards={run} onSelect={onAskSelect} />;
651
+ } else if (segment.type === 'suggestions') {
652
+ return <SuggestionsDisplay key={index} segment={segment} />;
650
653
  } else if (segment.type === 'tool_execution') {
651
654
  return (
652
655
  <ToolExecutionDisplay
@@ -27,6 +27,9 @@ export interface ChatPanelHandle {
27
27
  /** Close the chat panel. Called by inline cards' nav click handler
28
28
  * ONLY when the resolved navigation is same-tab. */
29
29
  closeChat: () => void;
30
+ /** Send `text` in the panel's open chat (a new chat: the send creates it),
31
+ * leaving the composer's draft alone. Used by `suggestions` chips. */
32
+ sendPrompt?: (text: string) => void;
30
33
  }
31
34
 
32
35
  export const ChatPanelContext = createContext<ChatPanelHandle | null>(null);
@@ -59,7 +59,7 @@ import {
59
59
  } from '../icons-v2-generated';
60
60
  import { Chevron02LeftIcon } from '../icons-v2-generated/arrows/chevron-02-left-icon';
61
61
  import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon';
62
- import { ActionsMenuDropdown } from '../ui/actions-menu';
62
+ import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu';
63
63
  import { Button } from '../ui/button';
64
64
  import { Drawer, DrawerContent } from '../ui/drawer';
65
65
  import { HoverDropdown, type HoverDropdownItem } from '../ui/hover-dropdown';
@@ -91,6 +91,7 @@ import { useEmptyStateConfig } from './hooks/use-empty-state-config';
91
91
  import { fetchSlashCommands, useSlashCommandRegistry, type SlashCommandSummary } from './hooks/use-slash-commands';
92
92
  import type { ChatSource, UseSseChatAdapterOptions } from './hooks/use-sse-chat-adapter';
93
93
  import { useUnifiedChat, type ChatMode, type UseUnifiedChatModes } from './hooks/use-unified-chat';
94
+ import { awaitsDecision, useWhatToAsk, type WhatToAskContent } from './hooks/use-what-to-ask';
94
95
  import { MingoArchiveList } from './mingo-archive-list';
95
96
  import { MingoChatHeader } from './mingo-chat-header';
96
97
  import { ChatDialogModals } from './mingo-chat-modals';
@@ -102,7 +103,7 @@ import { MingoWelcome, type MingoWelcomeProps } from './mingo-welcome';
102
103
  import { NavLinkAnchorViaRuntime } from './nav-link-anchor-via-runtime';
103
104
  import { accentFromIdentityIcon, getAgentAccent } from './quick-action-chip';
104
105
  import { SourceActionButton } from './source-action-button';
105
- import { CHAT_APPEARANCE, type ChatAppearance } from './types/chat.types';
106
+ import { AUTHOR_TYPE, CHAT_APPEARANCE, type ChatAppearance } from './types/chat.types';
106
107
  import type { ChatInputRef, DialogItem, SlashCommandActionId } from './types/component.types';
107
108
  import type { ChatContextItem, ChatContextPickerConfig } from './types/context-item.types';
108
109
  import type { MessageSegment, Message } from './types/message.types';
@@ -497,6 +498,24 @@ export interface EmbeddableChatProps {
497
498
  * guided tour in a conversation of its own) points it there instead.
498
499
  */
499
500
  onWhatToAsk?: () => void;
501
+ /**
502
+ * v2: the "What to ask?" exchange the list's entry shows (when no
503
+ * `onWhatToAsk` takes it over): added at the end of the open conversation,
504
+ * or a new chat opens with it. Local - never sent, never stored. Picking a
505
+ * chip sends its prompt in that chat, like a quick action.
506
+ *
507
+ * A function is called on each click and the exchange appears, whole, once
508
+ * its promise settles (prompts built from live data, e.g. the workspace's
509
+ * devices); clicks while one is pending are ignored. It should not reject:
510
+ * a rejection is logged and shows nothing.
511
+ */
512
+ whatToAsk?: WhatToAskContent | (() => Promise<WhatToAskContent>);
513
+ /**
514
+ * v2: items in the chat's ⋯ menu after its own, past a separator - actions
515
+ * that are not about the chat (e.g. the host's onboarding reset). Shown in
516
+ * every chat that is not archived, a new one included.
517
+ */
518
+ chatMenuExtraItems?: ActionsMenuItem[];
500
519
  }
501
520
 
502
521
  // =============================================================================
@@ -1063,6 +1082,8 @@ function EmbeddableChatInner({
1063
1082
  contextMemory,
1064
1083
  aboveComposer,
1065
1084
  onWhatToAsk,
1085
+ whatToAsk: whatToAskContent,
1086
+ chatMenuExtraItems,
1066
1087
  handleRef,
1067
1088
  }: EmbeddableChatProps & { handleRef?: React.Ref<EmbeddableChatHandle> }) {
1068
1089
  // `shell` 'none' / 'inline' means the consumer hosts us inside their own panel
@@ -1625,8 +1646,14 @@ function EmbeddableChatInner({
1625
1646
  // saved scroll position over the freshly-loaded destination page.
1626
1647
  // Source chips already route through `handleNavigationClose`; inline
1627
1648
  // cards now match. New-tab clicks never invoke `closeChat`.
1649
+ // `sendPrompt` goes through a ref: what it sends with is declared further
1650
+ // down (`sendPromptRef.current` is set next to `handleSend`).
1651
+ const sendPromptRef = useRef<(text: string) => void>(() => {});
1628
1652
  const chatPanelHandle = useMemo<ChatPanelHandle>(
1629
- () => ({ closeChat: handleNavigationClose }),
1653
+ () => ({
1654
+ closeChat: handleNavigationClose,
1655
+ sendPrompt: (text: string) => sendPromptRef.current(text),
1656
+ }),
1630
1657
  [handleNavigationClose],
1631
1658
  );
1632
1659
 
@@ -1711,18 +1738,37 @@ function EmbeddableChatInner({
1711
1738
 
1712
1739
  // Map the host's rows → the lib's Message type. The mapping itself lives in
1713
1740
  // `mapHostMessage`, which is where its pass-through list is pinned by tests.
1714
- const messages: Message[] = useMemo(() => {
1741
+ const hostMessages: Message[] = useMemo(() => {
1715
1742
  const options = { userName: guideUserName, userAvatar: guideUserAvatar, timestampCache };
1716
1743
  const mapped = rawMessages.map(m => mapHostMessage(m, options));
1717
1744
  pruneTimestampCache(timestampCache, new Set(rawMessages.map(m => m.id)));
1718
1745
  return mapped;
1719
1746
  }, [rawMessages, guideUserName, guideUserAvatar, timestampCache]);
1720
1747
 
1748
+ // "What to ask?" exchanges (`whatToAsk`) are the panel's own, shown in the
1749
+ // thread beside the host's messages; a new chat holding one reads as a
1750
+ // conversation until it is sent or left.
1751
+ const whatToAsk = useWhatToAsk({
1752
+ dialogId: activeDialogId,
1753
+ content: typeof whatToAskContent === 'function' ? undefined : whatToAskContent,
1754
+ user: {
1755
+ name: headerUserName ?? 'You',
1756
+ avatar: identityUser?.avatarUrl?.trim() || userAvatarUrl?.trim() || null,
1757
+ authorType: AUTHOR_TYPE.ADMIN,
1758
+ },
1759
+ assistant: { name: 'Mingo', assistantType: 'mingo' },
1760
+ });
1761
+ const noteWhatToAskSend = whatToAsk.noteSend;
1762
+ const messages = whatToAsk.withExchanges(hostMessages);
1763
+ // A new chat whose only messages are those exchanges: no dialog yet.
1764
+ const isWhatToAskDraft = activeDialogId == null && hostMessages.length === 0 && messages.length > 0;
1765
+
1721
1766
  const handleSend = useCallback(
1722
1767
  (text: string) => {
1723
1768
  // Locked: the disabled editor cannot submit, but a queued Enter or a
1724
1769
  // stale ref call still lands here — refused at the seam, not by luck.
1725
1770
  if (composerLock) return;
1771
+ noteWhatToAskSend();
1726
1772
  // Append chat-attachment markdown lines to the user's bubble.
1727
1773
  let augmentedText = text;
1728
1774
  if (readyAttachments.length > 0) {
@@ -1750,7 +1796,7 @@ function EmbeddableChatInner({
1750
1796
  setContextItems([]);
1751
1797
  }
1752
1798
  },
1753
- [composerLock, sendMessage, readyAttachments, viewUrlPrefix, clearAttachments, contextItems],
1799
+ [composerLock, noteWhatToAskSend, sendMessage, readyAttachments, viewUrlPrefix, clearAttachments, contextItems],
1754
1800
  );
1755
1801
 
1756
1802
  /**
@@ -2213,7 +2259,14 @@ function EmbeddableChatInner({
2213
2259
  ? freshConversationTitle
2214
2260
  : 'Current Chats';
2215
2261
  const headerBackAriaLabel = hasConversation ? (isViewingArchived ? 'Back to archive' : 'Back') : 'Back to Mingo';
2216
- const headerOnBack = hasConversation ? handleBack : () => handleActiveModeChange('mingo');
2262
+ const clearWhatToAskDraft = whatToAsk.clearDraft;
2263
+ const headerOnBack = hasConversation
2264
+ ? () => {
2265
+ // Leaving a new chat that only holds "What to ask?" exchanges drops them.
2266
+ if (activeDialogId == null) clearWhatToAskDraft();
2267
+ handleBack();
2268
+ }
2269
+ : () => handleActiveModeChange('mingo');
2217
2270
  const headerOnRestore =
2218
2271
  isViewingArchived && unarchiveDialog && activeDialog ? () => setRestoreTarget(activeDialog) : undefined;
2219
2272
  const headerOnRename =
@@ -2247,8 +2300,75 @@ function EmbeddableChatInner({
2247
2300
  // dedicated reset (not `handleBack`, which would reopen the archive when the
2248
2301
  // open conversation is archived) so it always lands on a fresh chat.
2249
2302
  const handleNewChat = useCallback(() => {
2303
+ clearWhatToAskDraft();
2250
2304
  resetToNewChat();
2251
- }, [resetToNewChat]);
2305
+ }, [clearWhatToAskDraft, resetToNewChat]);
2306
+
2307
+ // The open chat cannot take a message now: the composer is locked, a turn is
2308
+ // running, or it waits on an approval / escalation nobody has answered.
2309
+ const activeDialogStatus = activeDialog && dialogStatusOf ? dialogStatusOf(activeDialog) : undefined;
2310
+ const isChatBlocked =
2311
+ !!composerLock ||
2312
+ chatLoading ||
2313
+ activeDialogStatus === 'working' ||
2314
+ activeDialogStatus === 'approval' ||
2315
+ (pendingApprovals?.length ?? 0) > 0 ||
2316
+ awaitsDecision(hostMessages);
2317
+
2318
+ // "What to ask Mingo?" with `whatToAsk`: the exchange goes at the end of the
2319
+ // open conversation when it can go on right now. Otherwise (none open, an
2320
+ // archived one, one still loading or blocked as above) a new chat opens with
2321
+ // it: adding prompts under a pending approval or a running turn would mix
2322
+ // two conversations. A new chat with nothing sent yet gets another exchange.
2323
+ const showWhatToAsk = (content: WhatToAskContent) => {
2324
+ if (activeDialogId && hasConversation && !isViewingArchived && !isMessagesLoading && !isChatBlocked) {
2325
+ whatToAsk.ask('current', content);
2326
+ return;
2327
+ }
2328
+ const isUnsentNewChat = activeDialogId == null && hostMessages.length === 0 && !isViewingArchived;
2329
+ if (!isUnsentNewChat) {
2330
+ whatToAsk.clearDraft();
2331
+ resetToNewChat();
2332
+ }
2333
+ setComposeOpen(true);
2334
+ whatToAsk.ask('draft', content);
2335
+ };
2336
+
2337
+ // A "What to ask?" chip: its prompt goes out on its own, like a quick action
2338
+ // (whatever is staged in the composer stays staged), into the chat the chip
2339
+ // sits in - not while that chat is blocked. From a new chat that send
2340
+ // creates the dialog the exchange moves to.
2341
+ useEffect(() => {
2342
+ sendPromptRef.current = text => {
2343
+ if (isChatBlocked) return;
2344
+ noteWhatToAskSend();
2345
+ void sendMessage(text).catch((err: unknown) => {
2346
+ console.error('[EmbeddableChat] sendPrompt failed:', err);
2347
+ });
2348
+ };
2349
+ });
2350
+ // An async `whatToAsk` settles renders later: by then the panel may be in
2351
+ // another state, so it is shown through the latest `showWhatToAsk`.
2352
+ const showWhatToAskRef = useRef(showWhatToAsk);
2353
+ useEffect(() => {
2354
+ showWhatToAskRef.current = showWhatToAsk;
2355
+ });
2356
+ const whatToAskPendingRef = useRef(false);
2357
+ const requestWhatToAsk = () => {
2358
+ if (!whatToAskContent) return;
2359
+ if (typeof whatToAskContent !== 'function') {
2360
+ showWhatToAsk(whatToAskContent);
2361
+ return;
2362
+ }
2363
+ if (whatToAskPendingRef.current) return;
2364
+ whatToAskPendingRef.current = true;
2365
+ whatToAskContent()
2366
+ .then(content => showWhatToAskRef.current(content))
2367
+ .catch((err: unknown) => console.error('[EmbeddableChat] whatToAsk failed:', err))
2368
+ .finally(() => {
2369
+ whatToAskPendingRef.current = false;
2370
+ });
2371
+ };
2252
2372
 
2253
2373
  // "Ask Mingo" from an archived chat: the conversation is read-only (the
2254
2374
  // backend refuses writes), so the question starts a new chat instead. It is
@@ -2410,13 +2530,15 @@ function EmbeddableChatInner({
2410
2530
  dialogs,
2411
2531
  activeDialogId: activeDialogId ?? undefined,
2412
2532
  // Beside a new chat (wide only: narrow, the list is its own screen).
2413
- draftTitle: wideMingo && !hasConversation && !archiveOpen ? freshConversationTitle : undefined,
2533
+ // A new chat holding only "What to ask?" exchanges is still that new chat.
2534
+ draftTitle:
2535
+ wideMingo && (!hasConversation || isWhatToAskDraft) && !archiveOpen ? freshConversationTitle : undefined,
2414
2536
  onSelectDialog: (id: string) => leaveArchiveThen(() => handleSelectDialog(id))(),
2415
2537
  onNewChat: leaveArchiveThen(handleNewChat),
2416
2538
  onOpenArchive: headerOnOpenArchive && (archiveOpen ? closeArchive : headerOnOpenArchive),
2417
2539
  archiveActive: archiveOpen,
2418
2540
  // The suggestions a new chat opens on, unless the host has a surface of its own.
2419
- onWhatToAsk: leaveArchiveThen(onWhatToAsk ?? handleNewChat),
2541
+ onWhatToAsk: () => leaveArchiveThen(onWhatToAsk ?? (whatToAskContent ? requestWhatToAsk : handleNewChat))(),
2420
2542
  statusOf: dialogStatusOf,
2421
2543
  onRequestRename: mingoCaps.canRename ? setRenameTarget : undefined,
2422
2544
  onRequestArchive: mingoCaps.canArchive ? setArchiveTarget : undefined,
@@ -2449,6 +2571,7 @@ function EmbeddableChatInner({
2449
2571
  onToggleList={wideMingo ? toggleRailCollapsed : headerShowBack ? headerOnBack : () => setComposeOpen(false)}
2450
2572
  listOpen={splitActive}
2451
2573
  menuItems={headerShowBack && !isViewingArchived ? splitHeaderMenuItems : []}
2574
+ extraMenuItems={isViewingArchived || archiveOpen ? undefined : chatMenuExtraItems}
2452
2575
  onRestore={isViewingArchived ? headerOnRestore : undefined}
2453
2576
  // Only while the list and the chat both fit: narrow, the toggle is
2454
2577
  // already the way back to the list.
@@ -14,6 +14,7 @@ export * from './use-chat-attachment-image-gallery';
14
14
  export * from './use-chat-identity';
15
15
  export * from './use-chat-card-item';
16
16
  export * from './use-close-on-navigation';
17
+ export * from './use-what-to-ask';
17
18
 
18
19
  // useChat + useSSE — ported from hub `hooks/useChat.ts` + `hooks/useSSE.ts`
19
20
  // minus the MockChatService + useChatConfig dependencies (which stay in
@@ -0,0 +1,203 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+ import type { AssistantType, AuthorType } from '../types/chat.types';
5
+ import type { Message, SuggestionGroup } from '../types/message.types';
6
+
7
+ /**
8
+ * The "What to ask?" exchange: the question the user is shown asking, and the
9
+ * assistant's answer, a line of text over grouped prompt chips.
10
+ */
11
+ export interface WhatToAskContent {
12
+ /** The user's side, e.g. "What can I ask you?". */
13
+ question: string;
14
+ /** The answer's text (markdown), above the chips. */
15
+ reply: string;
16
+ groups: SuggestionGroup[];
17
+ /** A line under the chips, e.g. how picking one works. */
18
+ note?: string;
19
+ }
20
+
21
+ /** Who a side of the exchange is shown as. */
22
+ export interface WhatToAskAuthor {
23
+ name?: string;
24
+ avatar?: string | null;
25
+ authorType?: AuthorType;
26
+ assistantType?: AssistantType;
27
+ }
28
+
29
+ export interface UseWhatToAskOptions {
30
+ /** The open conversation; `null` for a new chat that has no dialog yet. */
31
+ dialogId: string | null | undefined;
32
+ /** What an exchange shows unless `ask` is handed its own. */
33
+ content?: WhatToAskContent;
34
+ user?: WhatToAskAuthor;
35
+ assistant?: WhatToAskAuthor;
36
+ /** Where a picked prompt goes (typically: sent). Absent: sent in the surrounding chat panel. */
37
+ onPick?: (prompt: string) => void;
38
+ }
39
+
40
+ export interface WhatToAsk {
41
+ /**
42
+ * Show the exchange: at the end of the open conversation (`'current'`), or
43
+ * in the new chat (`'draft'`), which then reads as a conversation until it
44
+ * is sent or left. `content` overrides the hook's for this exchange (e.g.
45
+ * prompts built from live data); each exchange keeps what it was shown
46
+ * with. No-op without either.
47
+ */
48
+ ask: (target: 'current' | 'draft', content?: WhatToAskContent) => void;
49
+ /** The thread with the exchanges of the open conversation (or the new chat) merged in by time. */
50
+ withExchanges: (messages: Message[]) => Message[];
51
+ /** Call before sending: a new chat's exchanges then move into the dialog the send creates. */
52
+ noteSend: () => void;
53
+ /** Drop the new chat's exchanges (the user started over or left it). */
54
+ clearDraft: () => void;
55
+ }
56
+
57
+ /** The key the new chat's exchanges are held under until it has a dialog. */
58
+ const DRAFT_KEY = '';
59
+
60
+ interface Exchange {
61
+ id: string;
62
+ at: number;
63
+ content: WhatToAskContent;
64
+ }
65
+
66
+ /**
67
+ * Local "What to ask?" exchanges, per conversation: never sent, never stored,
68
+ * gone on reload. Each is a user message and an assistant message carrying a
69
+ * `suggestions` segment, built at render from the exchange's content. A new chat holds its
70
+ * own until a send gives it a dialog (they move there) or the user opens
71
+ * another conversation (they are dropped).
72
+ */
73
+ export function useWhatToAsk({ dialogId, content, user, assistant, onPick }: UseWhatToAskOptions): WhatToAsk {
74
+ const [byDialog, setByDialog] = useState<Record<string, Exchange[]>>({});
75
+ const [draftSent, setDraftSent] = useState(false);
76
+ const key = dialogId ?? DRAFT_KEY;
77
+
78
+ // A new chat that got its dialog: keep its exchanges there if a send made
79
+ // it, drop them if another conversation was opened instead. Adjusted while
80
+ // rendering, so the thread never shows a frame without them.
81
+ const [prevKey, setPrevKey] = useState(key);
82
+ if (prevKey !== key) {
83
+ setPrevKey(key);
84
+ if (prevKey === DRAFT_KEY) {
85
+ setDraftSent(false);
86
+ setByDialog(prev => {
87
+ const { [DRAFT_KEY]: draft, ...rest } = prev;
88
+ if (!draft) return prev;
89
+ return draftSent ? { ...rest, [key]: [...(rest[key] ?? []), ...draft] } : rest;
90
+ });
91
+ }
92
+ }
93
+
94
+ const ask = useCallback(
95
+ (target: 'current' | 'draft', override?: WhatToAskContent) => {
96
+ const shown = override ?? content;
97
+ if (!shown) return;
98
+ const into = target === 'current' && dialogId ? dialogId : DRAFT_KEY;
99
+ const exchange = {
100
+ id: `what-to-ask-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
101
+ at: Date.now(),
102
+ content: shown,
103
+ };
104
+ setByDialog(prev => ({ ...prev, [into]: [...(prev[into] ?? []), exchange] }));
105
+ },
106
+ [content, dialogId],
107
+ );
108
+
109
+ const noteSend = useCallback(() => {
110
+ if (dialogId == null) setDraftSent(true);
111
+ }, [dialogId]);
112
+
113
+ const clearDraft = useCallback(() => {
114
+ setDraftSent(false);
115
+ setByDialog(prev => {
116
+ if (!prev[DRAFT_KEY]) return prev;
117
+ const rest = { ...prev };
118
+ delete rest[DRAFT_KEY];
119
+ return rest;
120
+ });
121
+ }, []);
122
+
123
+ const exchanges = byDialog[key] ?? [];
124
+ const withExchanges = (messages: Message[]): Message[] => {
125
+ if (exchanges.length === 0) return messages;
126
+ const local = exchanges.flatMap(exchange => exchangeMessages(exchange, user, assistant, onPick));
127
+ return mergeByTime(messages, local);
128
+ };
129
+
130
+ return { ask, withExchanges, noteSend, clearDraft };
131
+ }
132
+
133
+ function exchangeMessages(
134
+ { id, at, content }: Exchange,
135
+ user: WhatToAskAuthor | undefined,
136
+ assistant: WhatToAskAuthor | undefined,
137
+ onPick: ((prompt: string) => void) | undefined,
138
+ ): Message[] {
139
+ return [
140
+ {
141
+ id: `${id}-question`,
142
+ role: 'user',
143
+ content: content.question,
144
+ timestamp: new Date(at),
145
+ name: user?.name,
146
+ avatar: user?.avatar,
147
+ authorType: user?.authorType,
148
+ },
149
+ {
150
+ id: `${id}-answer`,
151
+ role: 'assistant',
152
+ content: [
153
+ { type: 'text', text: content.reply },
154
+ { type: 'suggestions', groups: content.groups, note: content.note, onPick },
155
+ ],
156
+ // A millisecond after the question, so the pair keeps its order.
157
+ timestamp: new Date(at + 1),
158
+ name: assistant?.name,
159
+ avatar: assistant?.avatar,
160
+ authorType: assistant?.authorType,
161
+ assistantType: assistant?.assistantType,
162
+ },
163
+ ];
164
+ }
165
+
166
+ /**
167
+ * Whether the thread waits on the user before it can go on: an approval or an
168
+ * escalation offer nobody has answered. "What to ask?" opens a new chat then,
169
+ * rather than adding prompts to a conversation that cannot take them.
170
+ */
171
+ export function awaitsDecision(messages: readonly Message[]): boolean {
172
+ return messages.some(
173
+ message =>
174
+ Array.isArray(message.content) &&
175
+ message.content.some(
176
+ segment =>
177
+ (segment.type === 'approval_request' ||
178
+ segment.type === 'approval_batch' ||
179
+ segment.type === 'escalation_offer') &&
180
+ (segment.status ?? 'pending') === 'pending',
181
+ ),
182
+ );
183
+ }
184
+
185
+ /**
186
+ * Places each local message before the first thread message that is later
187
+ * than it, keeping the thread's own order. A thread message without a time
188
+ * never moves anything past it.
189
+ */
190
+ export function mergeByTime(thread: Message[], local: Message[]): Message[] {
191
+ if (local.length === 0) return thread;
192
+ const out: Message[] = [];
193
+ let next = 0;
194
+ for (const message of thread) {
195
+ const at = message.timestamp?.getTime();
196
+ while (next < local.length && at !== undefined && (local[next].timestamp?.getTime() ?? 0) < at) {
197
+ out.push(local[next]);
198
+ next += 1;
199
+ }
200
+ out.push(message);
201
+ }
202
+ return out.concat(local.slice(next));
203
+ }
@@ -16,6 +16,7 @@ export * from './context-compaction-display';
16
16
  export * from './expand-chevron';
17
17
  export * from './thinking-display';
18
18
  export * from './ask-display';
19
+ export * from './suggestions-display';
19
20
  export * from './error-message-display';
20
21
  export * from './chat-container';
21
22
  export * from './chat-input';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Arrow02RightIcon, Ellipsis01Icon, Refresh01LeftIcon, SidebarIcon, XmarkIcon } from '../icons-v2-generated';
4
- import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu';
4
+ import { ActionsMenuDropdown, type ActionsMenuGroup, type ActionsMenuItem } from '../ui/actions-menu';
5
5
  import { SquareAvatar } from '../ui/square-avatar';
6
6
  import { ChatHeaderIconButton } from './chat-header-icon-button';
7
7
 
@@ -15,8 +15,10 @@ export interface MingoChatHeaderProps {
15
15
  onToggleList?: () => void;
16
16
  /** The list is on screen beside the chat; drives the toggle's state. */
17
17
  listOpen?: boolean;
18
- /** The chat's ⋯ menu. Empty or omitted leaves it out. */
18
+ /** The chat's ⋯ menu. Empty or omitted (with no `extraMenuItems`) leaves it out. */
19
19
  menuItems?: ActionsMenuItem[];
20
+ /** Items under the chat's own, after a separator: actions that are not about this chat. */
21
+ extraMenuItems?: ActionsMenuItem[];
20
22
  /** An archived chat: offers "Unarchive". */
21
23
  onRestore?: () => void;
22
24
  /** Collapse the chat back to the list alone (the panel's minimum). */
@@ -38,10 +40,15 @@ export function MingoChatHeader({
38
40
  onToggleList,
39
41
  listOpen = false,
40
42
  menuItems = [],
43
+ extraMenuItems = [],
41
44
  onRestore,
42
45
  onCollapse,
43
46
  onClose,
44
47
  }: MingoChatHeaderProps) {
48
+ const menuGroups: ActionsMenuGroup[] = [
49
+ ...(menuItems.length > 0 ? [{ items: menuItems, separator: true }] : []),
50
+ ...(extraMenuItems.length > 0 ? [{ items: extraMenuItems }] : []),
51
+ ];
45
52
  return (
46
53
  <div className="flex h-14 w-full shrink-0 overflow-hidden border-b border-ods-border">
47
54
  {onToggleList && (
@@ -77,11 +84,11 @@ export function MingoChatHeader({
77
84
  <Refresh01LeftIcon size={24} />
78
85
  </ChatHeaderIconButton>
79
86
  )}
80
- {menuItems.length > 0 && (
87
+ {menuGroups.length > 0 && (
81
88
  <ActionsMenuDropdown
82
89
  triggerAriaLabel="Chat actions"
83
90
  onCloseAutoFocus={e => e.preventDefault()}
84
- groups={[{ items: menuItems }]}
91
+ groups={menuGroups}
85
92
  customTrigger={
86
93
  <ChatHeaderIconButton aria-label="Chat actions">
87
94
  <Ellipsis01Icon size={24} />
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { useChatPanel } from './chat-panel-context';
4
+ import { QuickActionChipButton } from './quick-action-chip';
5
+ import type { SuggestionsSegment } from './types/message.types';
6
+
7
+ export interface SuggestionsDisplayProps {
8
+ segment: SuggestionsSegment;
9
+ }
10
+
11
+ /**
12
+ * A `suggestions` segment: each group's caption over a wrapping row of
13
+ * sentence chips, then the optional note. A chip hands its prompt to the
14
+ * segment's `onPick`, else sends it in the chat panel it sits in.
15
+ */
16
+ export function SuggestionsDisplay({ segment }: SuggestionsDisplayProps) {
17
+ const panel = useChatPanel();
18
+ const pick = segment.onPick ?? panel?.sendPrompt;
19
+
20
+ return (
21
+ <div className="flex min-w-0 flex-col gap-[var(--spacing-system-sf)]">
22
+ {segment.groups.map(group => (
23
+ <div key={group.label} className="flex min-w-0 flex-col gap-[var(--spacing-system-xs)]">
24
+ <p className="text-ods-text-secondary text-h5">{group.label}</p>
25
+ <div className="flex min-w-0 flex-wrap gap-[var(--spacing-system-xs)]">
26
+ {group.prompts.map(prompt => (
27
+ <QuickActionChipButton
28
+ key={prompt.label}
29
+ variant="question"
30
+ label={prompt.label}
31
+ icon={prompt.icon}
32
+ onSelect={pick ? () => pick(prompt.prompt ?? prompt.label) : undefined}
33
+ />
34
+ ))}
35
+ </div>
36
+ </div>
37
+ ))}
38
+ {segment.note && <p className="text-ods-text-secondary text-h6">{segment.note}</p>}
39
+ </div>
40
+ );
41
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { ChatRef } from '../chat-ref.types';
7
+ import type { QuickActionIconSpec } from '../quick-action-chip';
7
8
  import type { AssistantType, AuthorType, ChatApprovalStatus, MessageOwner } from './chat.types';
8
9
 
9
10
  // ========== Message Type Definitions ==========
@@ -370,6 +371,34 @@ export type ContextCompactionSegment = {
370
371
  summary?: string;
371
372
  };
372
373
 
374
+ /** One prompt a `suggestions` block offers: the chip shows `label`, and
375
+ * picking it hands `prompt` (absent: the label) to be sent. `icon` is a
376
+ * quick-action icon (a spec or a node). */
377
+ export type SuggestionPrompt = {
378
+ label: string;
379
+ prompt?: string;
380
+ icon?: ReactNode | QuickActionIconSpec;
381
+ };
382
+
383
+ /** A titled row of prompts in a `suggestions` block (e.g. "Diagnose"). */
384
+ export type SuggestionGroup = {
385
+ label: string;
386
+ prompts: SuggestionPrompt[];
387
+ };
388
+
389
+ /**
390
+ * Prompts the assistant offers as chips, grouped under captions, with an
391
+ * optional line under them. Never on the wire: a host builds it locally (the
392
+ * "What to ask?" answer, see `useWhatToAsk`). Picking a chip calls `onPick`;
393
+ * without one, inside an `EmbeddableChat` the prompt is sent in its open chat.
394
+ */
395
+ export type SuggestionsSegment = {
396
+ type: 'suggestions';
397
+ groups: SuggestionGroup[];
398
+ note?: string;
399
+ onPick?: (prompt: string) => void;
400
+ };
401
+
373
402
  export type MessageSegment =
374
403
  | TextSegment
375
404
  | ThinkingSegment
@@ -381,7 +410,8 @@ export type MessageSegment =
381
410
  | TicketEscalatedSegment
382
411
  | TicketEventSegment
383
412
  | ErrorSegment
384
- | ContextCompactionSegment;
413
+ | ContextCompactionSegment
414
+ | SuggestionsSegment;
385
415
 
386
416
  export type MessageContent = string | MessageSegment[];
387
417
 
@@ -134,12 +134,14 @@ export function extractIncompleteMessageState(
134
134
  // outside MESSAGE_START/END — there is no agent turn to continue.
135
135
  // - `error` ended the turn; resuming it would re-open a bubble the
136
136
  // backend has already abandoned.
137
+ // - `suggestions` is built locally by the host and never comes from a turn.
137
138
  case 'text':
138
139
  case 'thinking':
139
140
  case 'ask':
140
141
  case 'ticket_escalated':
141
142
  case 'ticket_event':
142
143
  case 'error':
144
+ case 'suggestions':
143
145
  break;
144
146
 
145
147
  default: {
@@ -707,6 +707,10 @@ export class MessageSegmentAccumulator {
707
707
  this.completeContextCompaction(segment.summary);
708
708
  }
709
709
  break;
710
+ case 'suggestions':
711
+ // Built locally by the host, never streamed: kept as it is.
712
+ this.segments.push(segment);
713
+ break;
710
714
  }
711
715
  }
712
716
  return this.getSegments();