@steerable/agent-ui 0.2.3 → 0.2.4
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/cards/ActionSegmentCard.d.ts +18 -0
- package/dist/cards/ActionSegmentCard.d.ts.map +1 -0
- package/dist/cards/ActionSegmentCard.js +26 -0
- package/dist/cards/ActionSegmentCard.js.map +1 -0
- package/dist/cards/AnalysisDocumentCard.d.ts +33 -0
- package/dist/cards/AnalysisDocumentCard.d.ts.map +1 -0
- package/dist/cards/AnalysisDocumentCard.js +11 -0
- package/dist/cards/AnalysisDocumentCard.js.map +1 -0
- package/dist/cards/AskUserQuestionsCard.d.ts +43 -0
- package/dist/cards/AskUserQuestionsCard.d.ts.map +1 -0
- package/dist/cards/AskUserQuestionsCard.js +171 -0
- package/dist/cards/AskUserQuestionsCard.js.map +1 -0
- package/dist/cards/CoverageReportCard.d.ts +41 -0
- package/dist/cards/CoverageReportCard.d.ts.map +1 -0
- package/dist/cards/CoverageReportCard.js +55 -0
- package/dist/cards/CoverageReportCard.js.map +1 -0
- package/dist/cards/OrchestrationPlanCard.d.ts +60 -0
- package/dist/cards/OrchestrationPlanCard.d.ts.map +1 -0
- package/dist/cards/OrchestrationPlanCard.js +103 -0
- package/dist/cards/OrchestrationPlanCard.js.map +1 -0
- package/dist/cards/PlanSelectorCard.d.ts +40 -0
- package/dist/cards/PlanSelectorCard.d.ts.map +1 -0
- package/dist/cards/PlanSelectorCard.js +106 -0
- package/dist/cards/PlanSelectorCard.js.map +1 -0
- package/dist/cards/PlanStepsCard.d.ts +18 -0
- package/dist/cards/PlanStepsCard.d.ts.map +1 -0
- package/dist/cards/PlanStepsCard.js +25 -0
- package/dist/cards/PlanStepsCard.js.map +1 -0
- package/dist/cards/QuizCard.d.ts +44 -0
- package/dist/cards/QuizCard.d.ts.map +1 -0
- package/dist/cards/QuizCard.js +113 -0
- package/dist/cards/QuizCard.js.map +1 -0
- package/dist/cards/ResearchPlanCard.d.ts +23 -0
- package/dist/cards/ResearchPlanCard.d.ts.map +1 -0
- package/dist/cards/ResearchPlanCard.js +50 -0
- package/dist/cards/ResearchPlanCard.js.map +1 -0
- package/dist/cards/SearchSourcesCard.d.ts +29 -0
- package/dist/cards/SearchSourcesCard.d.ts.map +1 -0
- package/dist/cards/SearchSourcesCard.js +83 -0
- package/dist/cards/SearchSourcesCard.js.map +1 -0
- package/dist/cards/SuggestedRepliesCard.d.ts +18 -0
- package/dist/cards/SuggestedRepliesCard.d.ts.map +1 -0
- package/dist/cards/SuggestedRepliesCard.js +14 -0
- package/dist/cards/SuggestedRepliesCard.js.map +1 -0
- package/dist/cards/SummaryMessageCard.d.ts +40 -0
- package/dist/cards/SummaryMessageCard.d.ts.map +1 -0
- package/dist/cards/SummaryMessageCard.js +56 -0
- package/dist/cards/SummaryMessageCard.js.map +1 -0
- package/dist/cards/ThinkingProcessCard.d.ts +46 -0
- package/dist/cards/ThinkingProcessCard.d.ts.map +1 -0
- package/dist/cards/ThinkingProcessCard.js +79 -0
- package/dist/cards/ThinkingProcessCard.js.map +1 -0
- package/dist/cards/ToolExecutionCard.d.ts +21 -0
- package/dist/cards/ToolExecutionCard.d.ts.map +1 -0
- package/dist/cards/ToolExecutionCard.js +65 -0
- package/dist/cards/ToolExecutionCard.js.map +1 -0
- package/dist/cards/icons.d.ts +38 -0
- package/dist/cards/icons.d.ts.map +1 -0
- package/dist/cards/icons.js +87 -0
- package/dist/cards/icons.js.map +1 -0
- package/dist/cards/index.d.ts +38 -0
- package/dist/cards/index.d.ts.map +1 -0
- package/dist/cards/index.js +24 -0
- package/dist/cards/index.js.map +1 -0
- package/dist/cards/types.d.ts +24 -0
- package/dist/cards/types.d.ts.map +1 -0
- package/dist/cards/types.js +7 -0
- package/dist/cards/types.js.map +1 -0
- package/dist/components/AgentSelector.d.ts +23 -0
- package/dist/components/AgentSelector.d.ts.map +1 -0
- package/dist/components/AgentSelector.js +63 -0
- package/dist/components/AgentSelector.js.map +1 -0
- package/dist/components/AgentSelector.stories.d.ts +12 -0
- package/dist/components/AgentSelector.stories.d.ts.map +1 -0
- package/dist/components/AgentSelector.stories.js +95 -0
- package/dist/components/AgentSelector.stories.js.map +1 -0
- package/dist/components/ChatPanel.d.ts +138 -21
- package/dist/components/ChatPanel.d.ts.map +1 -1
- package/dist/components/ChatPanel.js +181 -26
- package/dist/components/ChatPanel.js.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/state/ChatSessionProvider.d.ts +59 -0
- package/dist/state/ChatSessionProvider.d.ts.map +1 -0
- package/dist/state/ChatSessionProvider.js +71 -0
- package/dist/state/ChatSessionProvider.js.map +1 -0
- package/dist/state/MockChatStreamTransport.d.ts +54 -0
- package/dist/state/MockChatStreamTransport.d.ts.map +1 -0
- package/dist/state/MockChatStreamTransport.js +86 -0
- package/dist/state/MockChatStreamTransport.js.map +1 -0
- package/dist/state/MockChatStreamTransport.stories.d.ts +13 -0
- package/dist/state/MockChatStreamTransport.stories.d.ts.map +1 -0
- package/dist/state/MockChatStreamTransport.stories.js +60 -0
- package/dist/state/MockChatStreamTransport.stories.js.map +1 -0
- package/dist/state/bridgeLegacySSE.d.ts +42 -0
- package/dist/state/bridgeLegacySSE.d.ts.map +1 -0
- package/dist/state/bridgeLegacySSE.js +102 -0
- package/dist/state/bridgeLegacySSE.js.map +1 -0
- package/dist/state/index.d.ts +32 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +24 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/parseSSE.d.ts +50 -0
- package/dist/state/parseSSE.d.ts.map +1 -0
- package/dist/state/parseSSE.js +112 -0
- package/dist/state/parseSSE.js.map +1 -0
- package/dist/state/useChatComposer.d.ts +51 -0
- package/dist/state/useChatComposer.d.ts.map +1 -0
- package/dist/state/useChatComposer.js +82 -0
- package/dist/state/useChatComposer.js.map +1 -0
- package/dist/state/useChatList.d.ts +71 -0
- package/dist/state/useChatList.d.ts.map +1 -0
- package/dist/state/useChatList.js +158 -0
- package/dist/state/useChatList.js.map +1 -0
- package/dist/state/useChatSession.d.ts +29 -0
- package/dist/state/useChatSession.d.ts.map +1 -0
- package/dist/state/useChatSession.js +37 -0
- package/dist/state/useChatSession.js.map +1 -0
- package/dist/state/useScrollLock.d.ts +35 -0
- package/dist/state/useScrollLock.d.ts.map +1 -0
- package/dist/state/useScrollLock.js +74 -0
- package/dist/state/useScrollLock.js.map +1 -0
- package/dist/state/useToolCallStream.d.ts +38 -0
- package/dist/state/useToolCallStream.d.ts.map +1 -0
- package/dist/state/useToolCallStream.js +74 -0
- package/dist/state/useToolCallStream.js.map +1 -0
- package/package.json +10 -2
- package/src/cards/ActionSegmentCard.tsx +60 -0
- package/src/cards/AnalysisDocumentCard.tsx +81 -0
- package/src/cards/AskUserQuestionsCard.tsx +354 -0
- package/src/cards/CoverageReportCard.tsx +215 -0
- package/src/cards/OrchestrationPlanCard.tsx +328 -0
- package/src/cards/PlanSelectorCard.tsx +312 -0
- package/src/cards/PlanStepsCard.tsx +52 -0
- package/src/cards/QuizCard.tsx +309 -0
- package/src/cards/ResearchPlanCard.tsx +169 -0
- package/src/cards/SearchSourcesCard.tsx +244 -0
- package/src/cards/SuggestedRepliesCard.tsx +48 -0
- package/src/cards/SummaryMessageCard.tsx +145 -0
- package/src/cards/ThinkingProcessCard.tsx +155 -0
- package/src/cards/ToolExecutionCard.tsx +152 -0
- package/src/cards/cards.test.tsx +492 -0
- package/src/cards/icons.tsx +249 -0
- package/src/cards/index.ts +53 -0
- package/src/cards/types.ts +27 -0
- package/src/components/AgentSelector.stories.tsx +113 -0
- package/src/components/AgentSelector.test.tsx +70 -0
- package/src/components/AgentSelector.tsx +147 -0
- package/src/components/ChatPanel.test.tsx +130 -0
- package/src/components/ChatPanel.tsx +612 -138
- package/src/components/index.ts +13 -1
- package/src/index.ts +43 -0
- package/src/state/ChatSessionProvider.test.tsx +85 -0
- package/src/state/ChatSessionProvider.tsx +98 -0
- package/src/state/MockChatStreamTransport.stories.tsx +95 -0
- package/src/state/MockChatStreamTransport.test.ts +163 -0
- package/src/state/MockChatStreamTransport.ts +123 -0
- package/src/state/bridgeLegacySSE.test.ts +134 -0
- package/src/state/bridgeLegacySSE.ts +134 -0
- package/src/state/index.ts +82 -0
- package/src/state/parseSSE.test.ts +123 -0
- package/src/state/parseSSE.ts +123 -0
- package/src/state/useChatComposer.test.ts +145 -0
- package/src/state/useChatComposer.ts +117 -0
- package/src/state/useChatList.test.ts +135 -0
- package/src/state/useChatList.ts +217 -0
- package/src/state/useChatSession.test.ts +61 -0
- package/src/state/useChatSession.ts +64 -0
- package/src/state/useScrollLock.test.ts +99 -0
- package/src/state/useScrollLock.ts +105 -0
- package/src/state/useToolCallStream.test.ts +86 -0
- package/src/state/useToolCallStream.ts +110 -0
|
@@ -1,188 +1,662 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ChatPanel` — the
|
|
2
|
+
* `ChatPanel` — the orchestration shell, now exposed as a Radix-style
|
|
3
|
+
* compound: `ChatPanel.Root / .Header / .Messages / .Input / .Empty /
|
|
4
|
+
* .StreamingStatus`.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Compound API
|
|
7
|
+
* ------------
|
|
8
|
+
* <ChatPanel.Root className="…">
|
|
9
|
+
* <ChatPanel.Header>{customHeader}</ChatPanel.Header>
|
|
10
|
+
* <ChatPanel.Messages
|
|
11
|
+
* messages={messages}
|
|
12
|
+
* isStreaming={isStreaming}
|
|
13
|
+
* renderMessage={renderRichBubble}
|
|
14
|
+
* emptyState={<ChatPanel.Empty onSelectPrompt={…} />}
|
|
15
|
+
* />
|
|
16
|
+
* <ChatPanel.Input
|
|
17
|
+
* value={draft}
|
|
18
|
+
* onChange={setDraft}
|
|
19
|
+
* onSubmit={send}
|
|
20
|
+
* onCancel={cancel}
|
|
21
|
+
* isStreaming={isStreaming}
|
|
22
|
+
* />
|
|
23
|
+
* </ChatPanel.Root>
|
|
9
24
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
25
|
+
* Each sub-component owns one slot of the chat. The .Messages and .Input
|
|
26
|
+
* subs accept `as` / `slots` escape hatches so consumers can replace any one
|
|
27
|
+
* leaf without re-implementing the rest.
|
|
28
|
+
*
|
|
29
|
+
* Monolithic alias
|
|
30
|
+
* ----------------
|
|
31
|
+
* <ChatPanel messages={messages} onSubmit={onSubmit} … />
|
|
32
|
+
*
|
|
33
|
+
* The 0.2.x props remain valid — they internally render the compound. New
|
|
34
|
+
* consumers should reach for the compound; the alias exists to make the
|
|
35
|
+
* 0.2 -> 0.3 cutover non-breaking for in-flight integrations.
|
|
13
36
|
*/
|
|
14
37
|
|
|
15
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
forwardRef,
|
|
40
|
+
useCallback,
|
|
41
|
+
useEffect,
|
|
42
|
+
useImperativeHandle,
|
|
43
|
+
useRef,
|
|
44
|
+
useState,
|
|
45
|
+
type KeyboardEvent,
|
|
46
|
+
} from 'react';
|
|
16
47
|
import type { ChatMessage, ToolCall, ToolResult } from '@steerable/agent-protocol';
|
|
17
48
|
import { MessageList, type MessageRendererProps } from './MessageList.js';
|
|
18
49
|
import { ToolCallRenderer } from './ToolCallRenderer.js';
|
|
19
50
|
import { cn } from './cn.js';
|
|
51
|
+
import { useChatSessionContext } from '../state/ChatSessionProvider.js';
|
|
20
52
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*/
|
|
28
|
-
onSubmit: (input: { content: string }) => void | Promise<void>;
|
|
29
|
-
onCancel?: () => void;
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Root
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export interface ChatPanelRootProps {
|
|
58
|
+
children: React.ReactNode;
|
|
30
59
|
className?: string;
|
|
31
|
-
inputPlaceholder?: string;
|
|
32
|
-
/** Slot rendered above the message list (header, agent picker, …). */
|
|
33
|
-
header?: React.ReactNode;
|
|
34
60
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
61
|
+
* Opt out of the default `bg-agent-canvas text-agent-foreground` styling
|
|
62
|
+
* — handy when the consumer's host container already paints the
|
|
63
|
+
* background (e.g. deeppath's `bg-card` rounded shell) and you don't want
|
|
64
|
+
* two layers fighting over which wins in the cascade.
|
|
38
65
|
*/
|
|
66
|
+
unstyled?: boolean;
|
|
67
|
+
/** Render as a different host element (e.g. 'section', 'main'). */
|
|
68
|
+
as?: keyof React.JSX.IntrinsicElements;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ChatPanelRoot({
|
|
72
|
+
children,
|
|
73
|
+
className,
|
|
74
|
+
unstyled = false,
|
|
75
|
+
as: As = 'div',
|
|
76
|
+
}: ChatPanelRootProps) {
|
|
77
|
+
// The `as any` keeps the JSX surface flexible without forcing every host
|
|
78
|
+
// element's prop variance through the inference machinery.
|
|
79
|
+
const Component = As as React.ElementType;
|
|
80
|
+
return (
|
|
81
|
+
<Component
|
|
82
|
+
className={cn(
|
|
83
|
+
'flex h-full w-full flex-col',
|
|
84
|
+
unstyled ? null : 'bg-agent-canvas text-agent-foreground',
|
|
85
|
+
className,
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</Component>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Header
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
export interface ChatPanelHeaderProps {
|
|
98
|
+
children?: React.ReactNode;
|
|
99
|
+
className?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ChatPanelHeader({ children, className }: ChatPanelHeaderProps) {
|
|
103
|
+
if (!children) return null;
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
className={cn(
|
|
107
|
+
'flex items-center gap-2 border-b border-agent-border bg-agent-canvas px-3 py-2',
|
|
108
|
+
className,
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Messages
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export interface ChatPanelMessagesProps {
|
|
121
|
+
messages: ChatMessage[];
|
|
122
|
+
isStreaming?: boolean;
|
|
123
|
+
renderMessage?: (props: MessageRendererProps) => React.ReactNode;
|
|
39
124
|
renderToolCall?: (call: ToolCall, result?: ToolResult) => React.ReactNode;
|
|
40
|
-
/** Empty-state slot for the message list. */
|
|
41
125
|
emptyState?: React.ReactNode;
|
|
42
|
-
|
|
43
|
-
disabled?: boolean;
|
|
126
|
+
className?: string;
|
|
44
127
|
}
|
|
45
128
|
|
|
46
129
|
function defaultRenderToolCall(call: ToolCall, result?: ToolResult) {
|
|
47
130
|
return <ToolCallRenderer key={call.id} call={call} result={result} />;
|
|
48
131
|
}
|
|
49
132
|
|
|
50
|
-
|
|
133
|
+
function ChatPanelMessages(props: ChatPanelMessagesProps) {
|
|
51
134
|
const {
|
|
52
135
|
messages,
|
|
53
136
|
isStreaming = false,
|
|
54
|
-
|
|
55
|
-
onCancel,
|
|
56
|
-
className,
|
|
57
|
-
inputPlaceholder = 'Send a message…',
|
|
58
|
-
header,
|
|
137
|
+
renderMessage,
|
|
59
138
|
renderToolCall = defaultRenderToolCall,
|
|
60
139
|
emptyState,
|
|
61
|
-
|
|
140
|
+
className,
|
|
62
141
|
} = props;
|
|
63
|
-
const [draft, setDraft] = useState('');
|
|
64
142
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
143
|
+
const renderBubble = useCallback(
|
|
144
|
+
(rendererProps: MessageRendererProps) => {
|
|
145
|
+
if (renderMessage) return renderMessage(rendererProps);
|
|
146
|
+
return (
|
|
147
|
+
<DefaultRichBubble
|
|
148
|
+
{...rendererProps}
|
|
149
|
+
renderToolCall={renderToolCall}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
72
152
|
},
|
|
73
|
-
[
|
|
153
|
+
[renderMessage, renderToolCall],
|
|
74
154
|
);
|
|
75
155
|
|
|
76
|
-
|
|
77
|
-
(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
156
|
+
return (
|
|
157
|
+
<div className={cn('flex-1 overflow-hidden', className)}>
|
|
158
|
+
<MessageList
|
|
159
|
+
messages={messages}
|
|
160
|
+
isStreaming={isStreaming}
|
|
161
|
+
renderMessage={renderBubble}
|
|
162
|
+
emptyState={emptyState}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function DefaultRichBubble({
|
|
169
|
+
message,
|
|
170
|
+
isLastAssistant,
|
|
171
|
+
isStreaming,
|
|
172
|
+
index,
|
|
173
|
+
renderToolCall,
|
|
174
|
+
}: MessageRendererProps & {
|
|
175
|
+
renderToolCall: (call: ToolCall, result?: ToolResult) => React.ReactNode;
|
|
176
|
+
}) {
|
|
177
|
+
const isAssistant = message.role === 'assistant';
|
|
178
|
+
return (
|
|
179
|
+
<div
|
|
180
|
+
key={message.id ?? index}
|
|
181
|
+
data-role={message.role}
|
|
182
|
+
className={cn('flex w-full', isAssistant ? 'justify-start' : 'justify-end')}
|
|
183
|
+
>
|
|
184
|
+
<div
|
|
185
|
+
className={cn(
|
|
186
|
+
'flex max-w-[80%] flex-col gap-2',
|
|
187
|
+
isAssistant ? 'items-start' : 'items-end',
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
81
190
|
<div
|
|
82
|
-
key={message.id ?? rendererProps.index}
|
|
83
|
-
data-role={message.role}
|
|
84
191
|
className={cn(
|
|
85
|
-
'
|
|
86
|
-
isAssistant
|
|
192
|
+
'rounded-agent-md border px-3 py-2 text-sm leading-relaxed',
|
|
193
|
+
isAssistant
|
|
194
|
+
? 'border-agent-border bg-agent-muted text-agent-foreground'
|
|
195
|
+
: 'border-transparent bg-agent-accent text-agent-accent-foreground',
|
|
87
196
|
)}
|
|
88
197
|
>
|
|
89
|
-
<div
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
198
|
+
<div className="whitespace-pre-wrap">
|
|
199
|
+
{message.content || (isLastAssistant && isStreaming ? '…' : '')}
|
|
200
|
+
</div>
|
|
201
|
+
{isLastAssistant && isStreaming ? (
|
|
202
|
+
<span
|
|
203
|
+
aria-hidden
|
|
204
|
+
className="ml-0.5 inline-block h-3 w-1.5 align-baseline bg-agent-foreground animate-agent-cursor-blink"
|
|
205
|
+
/>
|
|
206
|
+
) : null}
|
|
207
|
+
</div>
|
|
208
|
+
{Array.isArray(message.toolCalls) && message.toolCalls.length > 0 ? (
|
|
209
|
+
<div className="flex w-full flex-col gap-1.5">
|
|
210
|
+
{message.toolCalls.map((call) => renderToolCall(call, message.toolResult))}
|
|
211
|
+
</div>
|
|
212
|
+
) : null}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Input
|
|
220
|
+
//
|
|
221
|
+
// Multi-line textarea with auto-grow, IME-safe keyboard handling, and a
|
|
222
|
+
// send/stop affordance. This is the rich default lifted from
|
|
223
|
+
// deeppath-agent/apps/web/src/components/chat/ChatInput.tsx — minus the
|
|
224
|
+
// agent-pill and settings gear, which live in agent-specific slots the apps
|
|
225
|
+
// can drop into the toolbar via `toolbarLeft`/`toolbarRight` props.
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
const MIN_TEXTAREA_PX = 44;
|
|
229
|
+
const MAX_TEXTAREA_PX = 220;
|
|
230
|
+
|
|
231
|
+
const IS_MAC =
|
|
232
|
+
typeof navigator !== 'undefined' &&
|
|
233
|
+
/Mac|iPod|iPhone|iPad/.test(navigator.platform || '');
|
|
234
|
+
const MOD_LABEL = IS_MAC ? '⌘' : 'Ctrl';
|
|
235
|
+
|
|
236
|
+
export interface ChatPanelInputHandle {
|
|
237
|
+
focus: () => void;
|
|
238
|
+
focusAtEnd: () => void;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface ChatPanelInputProps {
|
|
242
|
+
value: string;
|
|
243
|
+
onChange: (next: string) => void;
|
|
244
|
+
onSubmit: () => void | Promise<void>;
|
|
245
|
+
onCancel?: () => void;
|
|
246
|
+
isStreaming?: boolean;
|
|
247
|
+
disabled?: boolean;
|
|
248
|
+
placeholder?: string;
|
|
249
|
+
/**
|
|
250
|
+
* Keyboard mode:
|
|
251
|
+
* - 'cmd-enter' (default for the rich Input): Enter = newline,
|
|
252
|
+
* Cmd/Ctrl+Enter = send. IME-safe.
|
|
253
|
+
* - 'enter': Enter = send, Shift+Enter = newline. Matches the cloud
|
|
254
|
+
* chat product.
|
|
255
|
+
*/
|
|
256
|
+
keyMode?: 'cmd-enter' | 'enter';
|
|
257
|
+
/** Optional content slot rendered to the left of the send button. */
|
|
258
|
+
toolbarLeft?: React.ReactNode;
|
|
259
|
+
/** Optional content slot rendered to the right of the textarea. */
|
|
260
|
+
toolbarRight?: React.ReactNode;
|
|
261
|
+
className?: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const ChatPanelInput = forwardRef<ChatPanelInputHandle, ChatPanelInputProps>(
|
|
265
|
+
function ChatPanelInput(
|
|
266
|
+
{
|
|
267
|
+
value,
|
|
268
|
+
onChange,
|
|
269
|
+
onSubmit,
|
|
270
|
+
onCancel,
|
|
271
|
+
isStreaming = false,
|
|
272
|
+
disabled = false,
|
|
273
|
+
placeholder = 'Send a message…',
|
|
274
|
+
keyMode = 'cmd-enter',
|
|
275
|
+
toolbarLeft,
|
|
276
|
+
toolbarRight,
|
|
277
|
+
className,
|
|
278
|
+
},
|
|
279
|
+
ref,
|
|
280
|
+
) {
|
|
281
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
282
|
+
|
|
283
|
+
useImperativeHandle(
|
|
284
|
+
ref,
|
|
285
|
+
() => ({
|
|
286
|
+
focus: () => textareaRef.current?.focus(),
|
|
287
|
+
focusAtEnd: () => {
|
|
288
|
+
const ta = textareaRef.current;
|
|
289
|
+
if (!ta) return;
|
|
290
|
+
ta.focus();
|
|
291
|
+
const len = ta.value.length;
|
|
292
|
+
ta.setSelectionRange(len, len);
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
[],
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Auto-grow: reset height first so shrink works when user deletes text.
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
const ta = textareaRef.current;
|
|
301
|
+
if (!ta) return;
|
|
302
|
+
ta.style.height = 'auto';
|
|
303
|
+
const next = Math.min(Math.max(ta.scrollHeight, MIN_TEXTAREA_PX), MAX_TEXTAREA_PX);
|
|
304
|
+
ta.style.height = `${next}px`;
|
|
305
|
+
}, [value]);
|
|
306
|
+
|
|
307
|
+
const trimmed = value.trim();
|
|
308
|
+
const canSend = trimmed.length > 0 && !disabled && !isStreaming;
|
|
309
|
+
|
|
310
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
311
|
+
if (event.nativeEvent.isComposing) return;
|
|
312
|
+
if (keyMode === 'cmd-enter') {
|
|
313
|
+
const isSendCombo =
|
|
314
|
+
event.key === 'Enter' && (event.metaKey || event.ctrlKey);
|
|
315
|
+
if (isSendCombo) {
|
|
316
|
+
event.preventDefault();
|
|
317
|
+
if (canSend) void onSubmit();
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// keyMode === 'enter'
|
|
322
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
323
|
+
event.preventDefault();
|
|
324
|
+
if (isStreaming) onCancel?.();
|
|
325
|
+
else if (canSend) void onSubmit();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const handleSendClick = () => {
|
|
330
|
+
if (isStreaming) {
|
|
331
|
+
onCancel?.();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (canSend) void onSubmit();
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<div className={cn('chat-input-container px-3 pb-3 pt-1', className)}>
|
|
339
|
+
<div className="chat-input-box flex flex-col rounded-agent-lg border border-agent-border bg-agent-canvas shadow-sm">
|
|
340
|
+
<textarea
|
|
341
|
+
ref={textareaRef}
|
|
342
|
+
value={value}
|
|
343
|
+
onChange={(event) => onChange(event.target.value)}
|
|
344
|
+
onKeyDown={handleKeyDown}
|
|
345
|
+
placeholder={placeholder}
|
|
346
|
+
disabled={disabled}
|
|
347
|
+
rows={1}
|
|
348
|
+
className="block w-full resize-none border-0 bg-transparent px-3 pt-3 text-sm text-agent-foreground outline-none placeholder:text-agent-muted-foreground disabled:opacity-50"
|
|
349
|
+
style={{ minHeight: MIN_TEXTAREA_PX, maxHeight: MAX_TEXTAREA_PX }}
|
|
350
|
+
/>
|
|
351
|
+
<div className="flex items-center justify-between gap-2 px-2 pb-2 pt-1">
|
|
352
|
+
<div className="flex items-center gap-1">{toolbarLeft}</div>
|
|
353
|
+
<div className="flex items-center gap-3">
|
|
354
|
+
{toolbarRight}
|
|
355
|
+
<div className="hidden text-[11px] text-agent-muted-foreground/80 sm:block">
|
|
356
|
+
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
357
|
+
{MOD_LABEL}
|
|
358
|
+
</kbd>
|
|
359
|
+
<span className="mx-0.5">+</span>
|
|
360
|
+
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
361
|
+
{isStreaming ? '.' : 'Enter'}
|
|
362
|
+
</kbd>
|
|
105
363
|
</div>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
onClick={handleSendClick}
|
|
367
|
+
disabled={!isStreaming && !canSend}
|
|
368
|
+
className={
|
|
369
|
+
isStreaming
|
|
370
|
+
? 'flex h-8 w-8 items-center justify-center rounded-full bg-agent-destructive text-white transition hover:opacity-90'
|
|
371
|
+
: canSend
|
|
372
|
+
? 'flex h-8 w-8 items-center justify-center rounded-full bg-agent-foreground text-agent-canvas transition hover:opacity-90'
|
|
373
|
+
: 'flex h-8 w-8 cursor-not-allowed items-center justify-center rounded-full bg-agent-muted text-agent-muted-foreground'
|
|
374
|
+
}
|
|
375
|
+
aria-label={isStreaming ? 'Stop generating' : 'Send message'}
|
|
376
|
+
title={
|
|
377
|
+
isStreaming
|
|
378
|
+
? `Stop (${MOD_LABEL}+.)`
|
|
379
|
+
: `Send (${MOD_LABEL}+Enter)`
|
|
380
|
+
}
|
|
381
|
+
>
|
|
382
|
+
{isStreaming ? (
|
|
383
|
+
<span className="block h-3.5 w-3.5 bg-white" />
|
|
384
|
+
) : (
|
|
385
|
+
<span aria-hidden className="leading-none">↑</span>
|
|
123
386
|
)}
|
|
124
|
-
</
|
|
125
|
-
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
126
389
|
</div>
|
|
127
390
|
</div>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Empty
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
export interface ChatPanelEmptyProps {
|
|
401
|
+
/** Suggested prompts to chip-render. When omitted, the slot is a centred message. */
|
|
402
|
+
prompts?: string[];
|
|
403
|
+
/** Called when the user picks a chip. */
|
|
404
|
+
onSelectPrompt?: (prompt: string) => void;
|
|
405
|
+
title?: string;
|
|
406
|
+
description?: string;
|
|
407
|
+
className?: string;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function ChatPanelEmpty(props: ChatPanelEmptyProps) {
|
|
411
|
+
const { prompts, onSelectPrompt, title, description, className } = props;
|
|
412
|
+
return (
|
|
413
|
+
<div className={cn('flex h-full flex-col justify-end gap-3 p-3', className)}>
|
|
414
|
+
{(title || description) && (
|
|
415
|
+
<div className="text-center text-sm text-agent-muted-foreground">
|
|
416
|
+
{title ? (
|
|
417
|
+
<p className="font-medium text-agent-foreground">{title}</p>
|
|
418
|
+
) : null}
|
|
419
|
+
{description ? <p className="mt-1">{description}</p> : null}
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
{Array.isArray(prompts) && prompts.length > 0 ? (
|
|
423
|
+
<div className="flex flex-col items-start gap-2">
|
|
424
|
+
{prompts.map((p) => (
|
|
425
|
+
<button
|
|
426
|
+
key={p}
|
|
427
|
+
type="button"
|
|
428
|
+
onClick={() => onSelectPrompt?.(p)}
|
|
429
|
+
title={p}
|
|
430
|
+
className="max-w-full cursor-pointer truncate rounded-full border border-agent-border bg-agent-muted px-3 py-1.5 text-xs text-agent-muted-foreground transition-all duration-200 hover:bg-agent-accent hover:text-agent-foreground"
|
|
431
|
+
>
|
|
432
|
+
{p}
|
|
433
|
+
</button>
|
|
434
|
+
))}
|
|
435
|
+
</div>
|
|
436
|
+
) : null}
|
|
437
|
+
</div>
|
|
131
438
|
);
|
|
439
|
+
}
|
|
132
440
|
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// StreamingStatus
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
export interface ChatPanelStreamingStatusProps {
|
|
446
|
+
/** Current round number (1-based). */
|
|
447
|
+
round?: number;
|
|
448
|
+
/** Number of tool calls executed so far in this turn. */
|
|
449
|
+
actionCount?: number;
|
|
450
|
+
/**
|
|
451
|
+
* Suppress the status when the bubble has already received content tokens —
|
|
452
|
+
* the cursor blink in the bubble is enough.
|
|
453
|
+
*/
|
|
454
|
+
hasContent?: boolean;
|
|
455
|
+
/** Override the auto-derived label. */
|
|
456
|
+
label?: string;
|
|
457
|
+
className?: string;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function selectStatusLabel(round: number, actionCount: number): string {
|
|
461
|
+
if (round >= 2 && actionCount === 0) return `Round ${round} · continuing…`;
|
|
462
|
+
if (actionCount > 0) return `Called ${actionCount} tools, analysing…`;
|
|
463
|
+
return 'Thinking…';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function ChatPanelStreamingStatus(props: ChatPanelStreamingStatusProps) {
|
|
467
|
+
const {
|
|
468
|
+
round = 1,
|
|
469
|
+
actionCount = 0,
|
|
470
|
+
hasContent = false,
|
|
471
|
+
label,
|
|
472
|
+
className,
|
|
473
|
+
} = props;
|
|
474
|
+
if (hasContent) return null;
|
|
475
|
+
const text = label ?? selectStatusLabel(round, actionCount);
|
|
133
476
|
return (
|
|
134
477
|
<div
|
|
478
|
+
role="status"
|
|
479
|
+
aria-live="polite"
|
|
480
|
+
aria-atomic="true"
|
|
135
481
|
className={cn(
|
|
136
|
-
'
|
|
482
|
+
'my-2 inline-flex items-center gap-2 text-xs text-agent-muted-foreground',
|
|
137
483
|
className,
|
|
138
484
|
)}
|
|
139
485
|
>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
onSubmit={handleSubmit}
|
|
152
|
-
>
|
|
153
|
-
<input
|
|
154
|
-
type="text"
|
|
155
|
-
value={draft}
|
|
156
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
157
|
-
placeholder={inputPlaceholder}
|
|
158
|
-
disabled={disabled}
|
|
159
|
-
className={cn(
|
|
160
|
-
'flex-1 rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-sm text-agent-foreground placeholder:text-agent-muted-foreground',
|
|
161
|
-
'focus:border-agent-accent focus:outline-none focus:ring-1 focus:ring-agent-accent',
|
|
162
|
-
disabled && 'opacity-50',
|
|
163
|
-
)}
|
|
164
|
-
/>
|
|
165
|
-
{isStreaming && onCancel ? (
|
|
166
|
-
<button
|
|
167
|
-
type="button"
|
|
168
|
-
onClick={onCancel}
|
|
169
|
-
className="rounded-agent-md border border-agent-border px-3 py-2 text-sm text-agent-foreground hover:bg-agent-muted"
|
|
170
|
-
>
|
|
171
|
-
Stop
|
|
172
|
-
</button>
|
|
173
|
-
) : (
|
|
174
|
-
<button
|
|
175
|
-
type="submit"
|
|
176
|
-
disabled={disabled || !draft.trim()}
|
|
177
|
-
className={cn(
|
|
178
|
-
'rounded-agent-md bg-agent-accent px-3 py-2 text-sm font-medium text-agent-accent-foreground',
|
|
179
|
-
(disabled || !draft.trim()) && 'opacity-50',
|
|
180
|
-
)}
|
|
181
|
-
>
|
|
182
|
-
Send
|
|
183
|
-
</button>
|
|
184
|
-
)}
|
|
185
|
-
</form>
|
|
486
|
+
<span className="inline-flex items-center gap-1">
|
|
487
|
+
<span className="h-1 w-1 animate-pulse rounded-full bg-agent-foreground/40 [animation-delay:0ms]" />
|
|
488
|
+
<span className="h-1 w-1 animate-pulse rounded-full bg-agent-foreground/40 [animation-delay:150ms]" />
|
|
489
|
+
<span className="h-1 w-1 animate-pulse rounded-full bg-agent-foreground/40 [animation-delay:300ms]" />
|
|
490
|
+
</span>
|
|
491
|
+
<span>{text}</span>
|
|
492
|
+
{round > 1 ? (
|
|
493
|
+
<span className="rounded-full border border-agent-border bg-agent-muted/40 px-1.5 py-[1px] font-mono text-[10px] text-agent-muted-foreground">
|
|
494
|
+
round {round}
|
|
495
|
+
</span>
|
|
496
|
+
) : null}
|
|
186
497
|
</div>
|
|
187
498
|
);
|
|
188
499
|
}
|
|
500
|
+
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// Backwards-compat monolithic alias
|
|
503
|
+
//
|
|
504
|
+
// The 0.2.x signature `<ChatPanel messages onSubmit isStreaming … />` keeps
|
|
505
|
+
// working — we just wrap it with the compound primitives internally. Existing
|
|
506
|
+
// /dev/framework-preview, mock stories, and the deeppath-agent LocalChatPanel
|
|
507
|
+
// continue to render without churn.
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
export interface ChatPanelProps {
|
|
511
|
+
messages: ChatMessage[];
|
|
512
|
+
isStreaming?: boolean;
|
|
513
|
+
onSubmit: (input: { content: string }) => void | Promise<void>;
|
|
514
|
+
onCancel?: () => void;
|
|
515
|
+
className?: string;
|
|
516
|
+
inputPlaceholder?: string;
|
|
517
|
+
header?: React.ReactNode;
|
|
518
|
+
renderToolCall?: (call: ToolCall, result?: ToolResult) => React.ReactNode;
|
|
519
|
+
renderMessage?: (props: MessageRendererProps) => React.ReactNode;
|
|
520
|
+
emptyState?: React.ReactNode;
|
|
521
|
+
disabled?: boolean;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function ChatPanelMonolith(props: ChatPanelProps) {
|
|
525
|
+
const {
|
|
526
|
+
messages,
|
|
527
|
+
isStreaming = false,
|
|
528
|
+
onSubmit,
|
|
529
|
+
onCancel,
|
|
530
|
+
className,
|
|
531
|
+
inputPlaceholder,
|
|
532
|
+
header,
|
|
533
|
+
renderToolCall,
|
|
534
|
+
renderMessage,
|
|
535
|
+
emptyState,
|
|
536
|
+
disabled = false,
|
|
537
|
+
} = props;
|
|
538
|
+
const [draft, setDraft] = useState('');
|
|
539
|
+
const handleSubmit = useCallback(async () => {
|
|
540
|
+
const trimmed = draft.trim();
|
|
541
|
+
if (!trimmed || disabled) return;
|
|
542
|
+
setDraft('');
|
|
543
|
+
await onSubmit({ content: trimmed });
|
|
544
|
+
}, [draft, disabled, onSubmit]);
|
|
545
|
+
|
|
546
|
+
return (
|
|
547
|
+
<ChatPanelRoot className={className}>
|
|
548
|
+
{header ? <ChatPanelHeader>{header}</ChatPanelHeader> : null}
|
|
549
|
+
<ChatPanelMessages
|
|
550
|
+
messages={messages}
|
|
551
|
+
isStreaming={isStreaming}
|
|
552
|
+
renderMessage={renderMessage}
|
|
553
|
+
renderToolCall={renderToolCall}
|
|
554
|
+
emptyState={emptyState}
|
|
555
|
+
/>
|
|
556
|
+
<ChatPanelInput
|
|
557
|
+
value={draft}
|
|
558
|
+
onChange={setDraft}
|
|
559
|
+
onSubmit={handleSubmit}
|
|
560
|
+
onCancel={onCancel}
|
|
561
|
+
isStreaming={isStreaming}
|
|
562
|
+
disabled={disabled}
|
|
563
|
+
placeholder={inputPlaceholder}
|
|
564
|
+
keyMode="enter"
|
|
565
|
+
/>
|
|
566
|
+
</ChatPanelRoot>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// Connected
|
|
572
|
+
//
|
|
573
|
+
// The wave-4 deliverable: a `ChatPanel.Connected` shortcut that reads from a
|
|
574
|
+
// `ChatSessionProvider` mounted above, so an embedding looks like:
|
|
575
|
+
//
|
|
576
|
+
// <ChatSessionProvider value={useChatSession({ transport })}>
|
|
577
|
+
// <ChatPanel.Connected emptyPrompts={['Hello', 'Resume']} />
|
|
578
|
+
// </ChatSessionProvider>
|
|
579
|
+
//
|
|
580
|
+
// Apps that haven't migrated to a provider continue to pass props explicitly
|
|
581
|
+
// via the compound parts.
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
|
|
584
|
+
export interface ChatPanelConnectedProps {
|
|
585
|
+
className?: string;
|
|
586
|
+
header?: React.ReactNode;
|
|
587
|
+
emptyPrompts?: string[];
|
|
588
|
+
emptyTitle?: string;
|
|
589
|
+
emptyDescription?: string;
|
|
590
|
+
inputPlaceholder?: string;
|
|
591
|
+
renderMessage?: (props: MessageRendererProps) => React.ReactNode;
|
|
592
|
+
renderToolCall?: (call: ToolCall, result?: ToolResult) => React.ReactNode;
|
|
593
|
+
/** Apply the panel's own background/foreground tokens (default). */
|
|
594
|
+
styled?: boolean;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function ChatPanelConnected(props: ChatPanelConnectedProps) {
|
|
598
|
+
const {
|
|
599
|
+
className,
|
|
600
|
+
header,
|
|
601
|
+
emptyPrompts,
|
|
602
|
+
emptyTitle,
|
|
603
|
+
emptyDescription,
|
|
604
|
+
inputPlaceholder,
|
|
605
|
+
renderMessage,
|
|
606
|
+
renderToolCall,
|
|
607
|
+
styled = true,
|
|
608
|
+
} = props;
|
|
609
|
+
const session = useChatSessionContext();
|
|
610
|
+
const { composer } = session;
|
|
611
|
+
|
|
612
|
+
const emptyState =
|
|
613
|
+
emptyPrompts || emptyTitle || emptyDescription ? (
|
|
614
|
+
<ChatPanelEmpty
|
|
615
|
+
prompts={emptyPrompts}
|
|
616
|
+
title={emptyTitle}
|
|
617
|
+
description={emptyDescription}
|
|
618
|
+
onSelectPrompt={(text) => {
|
|
619
|
+
composer.setValue(text);
|
|
620
|
+
void composer.submit();
|
|
621
|
+
}}
|
|
622
|
+
/>
|
|
623
|
+
) : undefined;
|
|
624
|
+
|
|
625
|
+
return (
|
|
626
|
+
<ChatPanelRoot className={className} unstyled={!styled}>
|
|
627
|
+
{header ? <ChatPanelHeader>{header}</ChatPanelHeader> : null}
|
|
628
|
+
<ChatPanelMessages
|
|
629
|
+
messages={session.messages}
|
|
630
|
+
isStreaming={session.isStreaming}
|
|
631
|
+
renderMessage={renderMessage}
|
|
632
|
+
renderToolCall={renderToolCall}
|
|
633
|
+
emptyState={emptyState}
|
|
634
|
+
/>
|
|
635
|
+
<ChatPanelInput
|
|
636
|
+
value={composer.value}
|
|
637
|
+
onChange={composer.setValue}
|
|
638
|
+
onSubmit={composer.submit}
|
|
639
|
+
onCancel={composer.cancel}
|
|
640
|
+
isStreaming={session.isStreaming}
|
|
641
|
+
placeholder={inputPlaceholder}
|
|
642
|
+
/>
|
|
643
|
+
</ChatPanelRoot>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// `ChatPanel` is the monolith (backwards-compat); the compound parts hang
|
|
648
|
+
// off it as named attributes. This mirrors Radix's pattern so consumers can
|
|
649
|
+
// keep `<ChatPanel ...>` working OR migrate to `<ChatPanel.Root>…</ChatPanel.Root>`
|
|
650
|
+
// in their own time.
|
|
651
|
+
const ChatPanelCompound = Object.assign(ChatPanelMonolith, {
|
|
652
|
+
Root: ChatPanelRoot,
|
|
653
|
+
Header: ChatPanelHeader,
|
|
654
|
+
Messages: ChatPanelMessages,
|
|
655
|
+
Input: ChatPanelInput,
|
|
656
|
+
Empty: ChatPanelEmpty,
|
|
657
|
+
StreamingStatus: ChatPanelStreamingStatus,
|
|
658
|
+
Connected: ChatPanelConnected,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
export { ChatPanelCompound as ChatPanel };
|
|
662
|
+
export type { MessageRendererProps };
|