@flamingo-stack/openframe-frontend-core 0.0.752 → 0.0.754
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/build/export-map.json +1 -1
- package/dist/{chunk-OXEAN3EQ.cjs → chunk-35BJY5RR.cjs} +44 -44
- package/dist/{chunk-ZYRRRKSZ.cjs → chunk-5E6JHOGX.cjs} +3 -3
- package/dist/{chunk-35RU25PJ.cjs → chunk-7Z4EQOIX.cjs} +44 -44
- package/dist/{chunk-4PEG4JTM.cjs → chunk-B27XV2TC.cjs} +6 -6
- package/dist/{chunk-4OTCQEP3.cjs → chunk-BLZIUELR.cjs} +11 -11
- package/dist/{chunk-KBJSZFBZ.cjs → chunk-BQQ3JGEO.cjs} +3 -3
- package/dist/{chunk-RDH63XXW.cjs → chunk-DW5OKTUP.cjs} +34 -34
- package/dist/{chunk-KUVYVAS2.cjs → chunk-EDSFLB7J.cjs} +19 -17
- package/dist/{chunk-WQCLN27R.cjs → chunk-FAFFWYEH.cjs} +4 -4
- package/dist/{chunk-P4ODFQGA.cjs → chunk-GQYCVZJI.cjs} +4 -4
- package/dist/{chunk-Y6PVZRYL.cjs → chunk-IVAAQSOU.cjs} +12 -12
- package/dist/{chunk-EUQJ7QHW.cjs → chunk-JXACH52M.cjs} +4 -4
- package/dist/{chunk-JI6NRWCC.cjs → chunk-MOEILCEX.cjs} +13 -13
- package/dist/{chunk-IPVJAML5.cjs → chunk-OGKV5NOP.cjs} +69 -69
- package/dist/{chunk-OXCGQV6B.cjs → chunk-RTQN35EP.cjs} +153 -146
- package/dist/{chunk-EM4OEWCM.cjs → chunk-SDF5UUTE.cjs} +749 -608
- package/dist/{chunk-7OEVVMAU.cjs → chunk-T4BBZ5ST.cjs} +2 -2
- package/dist/{chunk-XADJG35G.cjs → chunk-TBDVF4SN.cjs} +31 -31
- package/dist/{chunk-G44V3UGE.cjs → chunk-TPJTFN7O.cjs} +81 -81
- package/dist/{chunk-J5G4CF2V.cjs → chunk-U4I3QR2A.cjs} +16 -16
- package/dist/{chunk-U2AE4UG7.cjs → chunk-UNRNLSG4.cjs} +2 -2
- package/dist/{chunk-ZVOXBSDP.cjs → chunk-WBOHLXFC.cjs} +2 -2
- package/dist/{chunk-VD3XBUQR.cjs → chunk-YSFOYBYC.cjs} +158 -158
- package/dist/{chunk-7WEGXNNS.cjs → chunk-ZNV5FE4T.cjs} +30 -30
- package/dist/{cloud-tenants-WAOMZZQ2.cjs → cloud-tenants-5OTPO7FL.cjs} +7 -7
- package/dist/components/announcement-bar.d.ts +1 -1
- package/dist/components/announcement-bar.d.ts.map +1 -1
- package/dist/components/announcement-bar.js +3 -1
- package/dist/components/case-studies/index.cjs +20 -20
- package/dist/components/chat/chat-dialog-menu-items.js +6 -6
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.js +3 -0
- package/dist/components/chat/chat-panel-context.d.ts +3 -0
- package/dist/components/chat/chat-panel-context.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.d.ts +20 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.js +78 -8
- package/dist/components/chat/hooks/index.d.ts +1 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.js +1 -0
- package/dist/components/chat/hooks/use-what-to-ask.d.ts +69 -0
- package/dist/components/chat/hooks/use-what-to-ask.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-what-to-ask.js +106 -0
- package/dist/components/chat/index.cjs +13 -5
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +1 -0
- package/dist/components/chat/mingo-chat-header.d.ts +4 -2
- package/dist/components/chat/mingo-chat-header.d.ts.map +1 -1
- package/dist/components/chat/mingo-chat-header.js +7 -2
- package/dist/components/chat/suggestions-display.d.ts +11 -0
- package/dist/components/chat/suggestions-display.d.ts.map +1 -0
- package/dist/components/chat/suggestions-display.js +27 -0
- package/dist/components/chat/types/message.types.d.ts +27 -1
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.js +2 -0
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.js +3 -0
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/docs/index.cjs +11 -11
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/faq/index.cjs +9 -9
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/help-center-pages/index.cjs +84 -84
- package/dist/components/index.cjs +66 -56
- package/dist/components/meeting-scheduler/index.cjs +31 -31
- package/dist/components/navigation/index.cjs +10 -10
- package/dist/components/onboarding-guides/index.cjs +10 -10
- package/dist/components/product-demo/index.cjs +7 -7
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/tickets/index.cjs +13 -13
- package/dist/components/ui/index.cjs +15 -7
- package/dist/{devices-VFEPXW2N.cjs → devices-MBGMD7BO.cjs} +8 -8
- package/dist/{embeddable-chat-BYQBABDV.cjs → embeddable-chat-CMEHSDV7.cjs} +163 -91
- package/dist/index.cjs +23 -11
- package/dist/{knowledge-4TE7AWZX.cjs → knowledge-LO5RK6PM.cjs} +8 -8
- package/dist/{logs-3B2P3JXN.cjs → logs-QRGJSZ4G.cjs} +15 -15
- package/dist/{policies-DVI6V22X.cjs → policies-OIQDO6F5.cjs} +11 -11
- package/dist/{scripts-JJ2BVYQ5.cjs → scripts-XULUNPDV.cjs} +12 -12
- package/dist/{software-update-BN23RATE.cjs → software-update-SRF55434.cjs} +10 -10
- package/dist/types/announcement.cjs +2 -0
- package/dist/types/announcement.d.ts +21 -0
- package/dist/types/announcement.d.ts.map +1 -1
- package/dist/types/announcement.js +2 -0
- package/dist/types/index.cjs +2 -0
- package/dist/utils/announcement-surface.d.ts +8 -0
- package/dist/utils/announcement-surface.d.ts.map +1 -0
- package/dist/utils/announcement-surface.js +7 -0
- package/dist/utils/index.cjs +7 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/package.json +1 -1
- package/src/components/announcement-bar.tsx +9 -1
- package/src/components/chat/chat-dialog-menu-items.tsx +6 -6
- package/src/components/chat/chat-message-enhanced.tsx +3 -0
- package/src/components/chat/chat-panel-context.tsx +3 -0
- package/src/components/chat/embeddable-chat.tsx +132 -9
- package/src/components/chat/hooks/index.ts +1 -0
- package/src/components/chat/hooks/use-what-to-ask.ts +203 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/mingo-chat-header.tsx +11 -4
- package/src/components/chat/suggestions-display.tsx +41 -0
- package/src/components/chat/types/message.types.ts +31 -1
- package/src/components/chat/utils/extract-incomplete-message-state.ts +2 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +4 -0
- package/src/types/announcement.ts +22 -0
- package/src/utils/announcement-surface.ts +14 -0
- package/src/utils/index.ts +2 -0
|
@@ -22,18 +22,18 @@ export function chatDialogMenuItems({
|
|
|
22
22
|
onArchive,
|
|
23
23
|
}: ChatDialogMenuActions): ActionsMenuItem[] {
|
|
24
24
|
const items: (ActionsMenuItem | undefined)[] = [
|
|
25
|
-
onCopyLink && {
|
|
26
|
-
id: 'copy-link',
|
|
27
|
-
label: 'Copy Chat Link',
|
|
28
|
-
icon: <Link01HorizontalIcon className={chatMenuIconClassName} />,
|
|
29
|
-
onClick: onCopyLink,
|
|
30
|
-
},
|
|
31
25
|
onRename && {
|
|
32
26
|
id: 'rename',
|
|
33
27
|
label: 'Rename Chat',
|
|
34
28
|
icon: <PenEditIcon className={chatMenuIconClassName} />,
|
|
35
29
|
onClick: onRename,
|
|
36
30
|
},
|
|
31
|
+
onCopyLink && {
|
|
32
|
+
id: 'copy-link',
|
|
33
|
+
label: 'Copy Chat Link',
|
|
34
|
+
icon: <Link01HorizontalIcon className={chatMenuIconClassName} />,
|
|
35
|
+
onClick: onCopyLink,
|
|
36
|
+
},
|
|
37
37
|
onCompact && {
|
|
38
38
|
id: 'compact',
|
|
39
39
|
label: 'Compact Chat Memory',
|
|
@@ -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
|
-
() => ({
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
{
|
|
87
|
+
{menuGroups.length > 0 && (
|
|
81
88
|
<ActionsMenuDropdown
|
|
82
89
|
triggerAriaLabel="Chat actions"
|
|
83
90
|
onCloseAutoFocus={e => e.preventDefault()}
|
|
84
|
-
groups={
|
|
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: {
|