@wealthx/shadcn 1.5.38 → 1.5.40
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/.turbo/turbo-build.log +98 -95
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-LSSIWLYU.mjs → chunk-6XNEHTII.mjs} +1 -1
- package/dist/{chunk-ULQ53FRJ.mjs → chunk-7NQKFPXE.mjs} +1 -1
- package/dist/{chunk-DSVKEVX6.mjs → chunk-CZOGJC76.mjs} +1 -1
- package/dist/{chunk-JPGL36WQ.mjs → chunk-FL7DEYUA.mjs} +6 -7
- package/dist/{chunk-2CHH5QOA.mjs → chunk-FQUT5XD6.mjs} +1 -1
- package/dist/chunk-MGIDYXOP.mjs +814 -0
- package/dist/{chunk-OG2VM34K.mjs → chunk-MHBQJVHE.mjs} +1 -1
- package/dist/{chunk-R7M657QL.mjs → chunk-STN5QIWN.mjs} +36 -1
- package/dist/components/ui/file-preview-dialog.js +6 -7
- package/dist/components/ui/file-preview-dialog.mjs +2 -2
- package/dist/components/ui/kanban-column.js +6 -7
- package/dist/components/ui/kanban-column.mjs +3 -3
- package/dist/components/ui/opportunity-card.js +6 -7
- package/dist/components/ui/opportunity-card.mjs +2 -2
- package/dist/components/ui/pipeline-board.js +6 -7
- package/dist/components/ui/pipeline-board.mjs +4 -4
- package/dist/components/ui/policy-ai/index.js +1636 -0
- package/dist/components/ui/policy-ai/index.mjs +36 -0
- package/dist/components/ui/progress.js +6 -7
- package/dist/components/ui/progress.mjs +1 -1
- package/dist/components/ui/stage-timeline.js +6 -7
- package/dist/components/ui/stage-timeline.mjs +2 -2
- package/dist/components/ui/support-agent/index.js +31 -1
- package/dist/components/ui/support-agent/index.mjs +1 -1
- package/dist/index.js +4105 -3299
- package/dist/index.mjs +25 -7
- package/dist/styles.css +1 -1
- package/package.json +10 -5
- package/src/components/index.tsx +30 -0
- package/src/components/ui/policy-ai/index.tsx +41 -0
- package/src/components/ui/policy-ai/policy-ai-panel.tsx +526 -0
- package/src/components/ui/policy-ai/policy-ai-primitives.tsx +332 -0
- package/src/components/ui/policy-ai/policy-ai-responses.tsx +543 -0
- package/src/components/ui/progress.tsx +15 -12
- package/src/components/ui/support-agent/support-agent-panel.tsx +46 -2
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wealthx/shadcn",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.40",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -564,16 +564,21 @@
|
|
|
564
564
|
"import": "./dist/components/ui/support-agent/index.mjs",
|
|
565
565
|
"require": "./dist/components/ui/support-agent/index.js"
|
|
566
566
|
},
|
|
567
|
-
"./ai
|
|
568
|
-
"types": "./src/components/ui/ai
|
|
569
|
-
"import": "./dist/components/ui/ai
|
|
570
|
-
"require": "./dist/components/ui/ai
|
|
567
|
+
"./policy-ai": {
|
|
568
|
+
"types": "./src/components/ui/policy-ai/index.tsx",
|
|
569
|
+
"import": "./dist/components/ui/policy-ai/index.mjs",
|
|
570
|
+
"require": "./dist/components/ui/policy-ai/index.js"
|
|
571
571
|
},
|
|
572
572
|
"./chat-input-area": {
|
|
573
573
|
"types": "./src/components/ui/chat-input-area.tsx",
|
|
574
574
|
"import": "./dist/components/ui/chat-input-area.mjs",
|
|
575
575
|
"require": "./dist/components/ui/chat-input-area.js"
|
|
576
576
|
},
|
|
577
|
+
"./ai-conversations": {
|
|
578
|
+
"types": "./src/components/ui/ai-conversations/index.tsx",
|
|
579
|
+
"import": "./dist/components/ui/ai-conversations/index.mjs",
|
|
580
|
+
"require": "./dist/components/ui/ai-conversations/index.js"
|
|
581
|
+
},
|
|
577
582
|
"./chat-widget-primitives": {
|
|
578
583
|
"types": "./src/components/ui/chat-widget-primitives.tsx",
|
|
579
584
|
"import": "./dist/components/ui/chat-widget-primitives.mjs",
|
package/src/components/index.tsx
CHANGED
|
@@ -139,6 +139,36 @@ export type {
|
|
|
139
139
|
export { ChatInputArea } from "./ui/chat-input-area";
|
|
140
140
|
export type { ChatInputAreaProps } from "./ui/chat-input-area";
|
|
141
141
|
|
|
142
|
+
export {
|
|
143
|
+
PolicyQueryChip,
|
|
144
|
+
PolicyVerdictBadge,
|
|
145
|
+
PolicyCitationPanel,
|
|
146
|
+
PolicySingleBankAnswer,
|
|
147
|
+
PolicyComparisonTable,
|
|
148
|
+
PolicyRankedList,
|
|
149
|
+
PolicyAIFAB,
|
|
150
|
+
PolicyAIPanel,
|
|
151
|
+
} from "./ui/policy-ai";
|
|
152
|
+
export type {
|
|
153
|
+
PolicyType,
|
|
154
|
+
PolicyQueryType,
|
|
155
|
+
PolicyVerdict,
|
|
156
|
+
PolicyQueryContext,
|
|
157
|
+
PolicyCitationItem,
|
|
158
|
+
PolicyBankVerdict,
|
|
159
|
+
PolicyRankedBankItem,
|
|
160
|
+
PolicyResponseContent,
|
|
161
|
+
PolicyAIMessage,
|
|
162
|
+
PolicyQueryChipProps,
|
|
163
|
+
PolicyVerdictBadgeProps,
|
|
164
|
+
PolicyCitationPanelProps,
|
|
165
|
+
PolicySummaryCounts,
|
|
166
|
+
PolicySingleBankAnswerProps,
|
|
167
|
+
PolicyComparisonTableProps,
|
|
168
|
+
PolicyRankedListProps,
|
|
169
|
+
PolicyAIFABProps,
|
|
170
|
+
PolicyAIPanelProps,
|
|
171
|
+
} from "./ui/policy-ai";
|
|
142
172
|
export {
|
|
143
173
|
ChatWidgetLauncher,
|
|
144
174
|
ChatWidgetHeader,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Policy AI — domain barrel
|
|
2
|
+
// Exports all components, hooks, and types for the Policy AI response UI.
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
PolicyQueryChip,
|
|
6
|
+
PolicyVerdictBadge,
|
|
7
|
+
PolicyCitationPanel,
|
|
8
|
+
} from "./policy-ai-primitives";
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
PolicyType,
|
|
12
|
+
PolicyQueryType,
|
|
13
|
+
PolicyVerdict,
|
|
14
|
+
PolicyQueryContext,
|
|
15
|
+
PolicyCitationItem,
|
|
16
|
+
PolicyBankVerdict,
|
|
17
|
+
PolicyRankedBankItem,
|
|
18
|
+
PolicyResponseContent,
|
|
19
|
+
PolicyConversationItem,
|
|
20
|
+
PolicyAIMessage,
|
|
21
|
+
PolicyQueryChipProps,
|
|
22
|
+
PolicyVerdictBadgeProps,
|
|
23
|
+
PolicyCitationPanelProps,
|
|
24
|
+
} from "./policy-ai-primitives";
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
PolicySingleBankAnswer,
|
|
28
|
+
PolicyComparisonTable,
|
|
29
|
+
PolicyRankedList,
|
|
30
|
+
} from "./policy-ai-responses";
|
|
31
|
+
|
|
32
|
+
export type {
|
|
33
|
+
PolicySummaryCounts,
|
|
34
|
+
PolicySingleBankAnswerProps,
|
|
35
|
+
PolicyComparisonTableProps,
|
|
36
|
+
PolicyRankedListProps,
|
|
37
|
+
} from "./policy-ai-responses";
|
|
38
|
+
|
|
39
|
+
export { PolicyAIFAB, PolicyAIPanel } from "./policy-ai-panel";
|
|
40
|
+
|
|
41
|
+
export type { PolicyAIFABProps, PolicyAIPanelProps } from "./policy-ai-panel";
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
BrainCircuit,
|
|
4
|
+
Check,
|
|
5
|
+
ChevronDown,
|
|
6
|
+
Minus,
|
|
7
|
+
RotateCcw,
|
|
8
|
+
X,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { ChatInputArea } from "@/components/ui/chat-input-area";
|
|
13
|
+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
14
|
+
import { ChatWidgetMessage } from "@/components/ui/chat-widget-primitives";
|
|
15
|
+
import {
|
|
16
|
+
PolicySingleBankAnswer,
|
|
17
|
+
PolicyComparisonTable,
|
|
18
|
+
PolicyRankedList,
|
|
19
|
+
} from "./policy-ai-responses";
|
|
20
|
+
import type {
|
|
21
|
+
PolicyAIMessage,
|
|
22
|
+
PolicyQueryContext,
|
|
23
|
+
PolicyResponseContent,
|
|
24
|
+
} from "./policy-ai-primitives";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// PolicyAIFAB props
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface PolicyAIFABProps {
|
|
31
|
+
onClick: () => void;
|
|
32
|
+
hasNudge?: boolean;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// PolicyAIPanelProps
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export interface PolicyAIPanelProps {
|
|
41
|
+
open: boolean;
|
|
42
|
+
onClose: () => void;
|
|
43
|
+
messages?: PolicyAIMessage[];
|
|
44
|
+
suggestedQuestions?: Record<string, string[]>;
|
|
45
|
+
isStreaming?: boolean;
|
|
46
|
+
isLoading?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Custom thinking steps shown during loading/streaming.
|
|
49
|
+
* Defaults to the built-in 4-step sequence if not provided.
|
|
50
|
+
*/
|
|
51
|
+
thinkingSteps?: string[];
|
|
52
|
+
onSendMessage?: (text: string) => void;
|
|
53
|
+
/** Called when the user selects files via the paperclip attachment button. */
|
|
54
|
+
onAttachFile?: (files: FileList) => void;
|
|
55
|
+
/** Called when the user selects images via the image upload button. */
|
|
56
|
+
onAttachImage?: (files: FileList) => void;
|
|
57
|
+
onReset?: () => void;
|
|
58
|
+
/** Position relative to viewport — defaults to bottom-right. Ignored when `inline` is true. */
|
|
59
|
+
position?: "bottom-right" | "bottom-left";
|
|
60
|
+
/**
|
|
61
|
+
* When true, renders as an inline block filling its parent container instead
|
|
62
|
+
* of a fixed-position float widget. Use inside drawers, sheets, or tab panels.
|
|
63
|
+
* Hides the minimize and close buttons.
|
|
64
|
+
*/
|
|
65
|
+
inline?: boolean;
|
|
66
|
+
className?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Internal helpers
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
type PolicyTypeTab =
|
|
74
|
+
| "Income"
|
|
75
|
+
| "Security"
|
|
76
|
+
| "Serviceability"
|
|
77
|
+
| "Loan Type"
|
|
78
|
+
| "Borrower";
|
|
79
|
+
|
|
80
|
+
const DEFAULT_SUGGESTED: Record<PolicyTypeTab, string[]> = {
|
|
81
|
+
Income: [
|
|
82
|
+
"Which banks accept casual income?",
|
|
83
|
+
"Which lenders accept bonus income, ranked best to worst?",
|
|
84
|
+
"How is self-employed income assessed across lenders?",
|
|
85
|
+
],
|
|
86
|
+
Security: [
|
|
87
|
+
"Which banks accept apartments under 40sqm?",
|
|
88
|
+
"What is the maximum LVR for a serviced apartment?",
|
|
89
|
+
"Do any lenders accept hobby farm properties?",
|
|
90
|
+
],
|
|
91
|
+
Serviceability: [
|
|
92
|
+
"Which banks have the most flexible DTI ratio?",
|
|
93
|
+
"How is HECS debt treated by different lenders?",
|
|
94
|
+
"Which lenders apply the lowest assessment buffer rate?",
|
|
95
|
+
],
|
|
96
|
+
"Loan Type": [
|
|
97
|
+
"Which lenders offer cashback on refinances?",
|
|
98
|
+
"Which banks accept low-doc construction loans?",
|
|
99
|
+
"What are the pre-approval policies across lenders?",
|
|
100
|
+
],
|
|
101
|
+
Borrower: [
|
|
102
|
+
"Which banks accept non-resident borrowers?",
|
|
103
|
+
"Which lenders offer LMI waiver for medical professionals?",
|
|
104
|
+
"How do lenders treat prior credit impairment?",
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const DEFAULT_THINKING_STEPS = [
|
|
109
|
+
"Classifying query type…",
|
|
110
|
+
"Searching across 40+ banks…",
|
|
111
|
+
"Retrieving policy documents…",
|
|
112
|
+
"Generating response…",
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// PolicyAIThinkingSteps
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Animated thinking steps shown while the AI is working.
|
|
121
|
+
* Steps progress automatically on a timer; completed steps show a check mark.
|
|
122
|
+
*/
|
|
123
|
+
function PolicyAIThinkingSteps({ steps }: { steps: string[] }) {
|
|
124
|
+
const [currentStep, setCurrentStep] = React.useState(0);
|
|
125
|
+
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
setCurrentStep(0);
|
|
128
|
+
}, [steps]);
|
|
129
|
+
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
if (currentStep >= steps.length - 1) return;
|
|
132
|
+
const timer = setTimeout(() => setCurrentStep((s) => s + 1), 1400);
|
|
133
|
+
return () => clearTimeout(timer);
|
|
134
|
+
}, [currentStep, steps.length]);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex flex-col gap-2.5 px-4 py-3">
|
|
138
|
+
{steps.map((step, i) => {
|
|
139
|
+
const isDone = i < currentStep;
|
|
140
|
+
const isCurrent = i === currentStep;
|
|
141
|
+
const isPending = i > currentStep;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div
|
|
145
|
+
key={step}
|
|
146
|
+
className={cn(
|
|
147
|
+
"flex items-center gap-2.5 transition-opacity duration-300",
|
|
148
|
+
isPending && "opacity-30",
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
<span className="shrink-0 size-4 flex items-center justify-center">
|
|
152
|
+
{isDone ? (
|
|
153
|
+
<Check className="size-3.5 text-primary" aria-hidden="true" />
|
|
154
|
+
) : isCurrent ? (
|
|
155
|
+
<span
|
|
156
|
+
className="flex items-center gap-0.5"
|
|
157
|
+
aria-label="In progress"
|
|
158
|
+
>
|
|
159
|
+
{[0, 1, 2].map((j) => (
|
|
160
|
+
<span
|
|
161
|
+
key={j}
|
|
162
|
+
className="size-1 bg-muted-foreground/70 animate-bounce"
|
|
163
|
+
style={{ animationDelay: `${j * 150}ms` }}
|
|
164
|
+
/>
|
|
165
|
+
))}
|
|
166
|
+
</span>
|
|
167
|
+
) : null}
|
|
168
|
+
</span>
|
|
169
|
+
|
|
170
|
+
<span
|
|
171
|
+
className={cn(
|
|
172
|
+
"text-sm leading-snug transition-colors duration-200",
|
|
173
|
+
isDone && "text-muted-foreground",
|
|
174
|
+
isCurrent && "text-foreground font-medium",
|
|
175
|
+
isPending && "text-muted-foreground",
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
{step}
|
|
179
|
+
</span>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// ResponseCard — dispatches to the correct response molecule
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
function ResponseCard({
|
|
192
|
+
content,
|
|
193
|
+
queryContext,
|
|
194
|
+
}: {
|
|
195
|
+
content: PolicyResponseContent;
|
|
196
|
+
queryContext?: PolicyQueryContext;
|
|
197
|
+
}) {
|
|
198
|
+
if (content.type === "single_bank") {
|
|
199
|
+
return (
|
|
200
|
+
<PolicySingleBankAnswer
|
|
201
|
+
bankName={content.bankName}
|
|
202
|
+
verdict={content.verdict}
|
|
203
|
+
answer={content.answer}
|
|
204
|
+
citations={content.citations}
|
|
205
|
+
queryContext={queryContext}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (content.type === "cross_bank_comparison") {
|
|
210
|
+
return (
|
|
211
|
+
<PolicyComparisonTable
|
|
212
|
+
categories={content.categories}
|
|
213
|
+
banks={content.banks}
|
|
214
|
+
summaryCounts={content.summaryCounts}
|
|
215
|
+
citations={content.citations}
|
|
216
|
+
queryContext={queryContext}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (content.type === "ranked_list") {
|
|
221
|
+
return (
|
|
222
|
+
<PolicyRankedList
|
|
223
|
+
categories={content.categories}
|
|
224
|
+
banks={content.banks}
|
|
225
|
+
citations={content.citations}
|
|
226
|
+
queryContext={queryContext}
|
|
227
|
+
/>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// PolicyAIFAB (Molecule)
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Floating action button that opens the PolicyAIPanel.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* <PolicyAIFAB onClick={() => setOpen(true)} />
|
|
242
|
+
*/
|
|
243
|
+
export function PolicyAIFAB({
|
|
244
|
+
onClick,
|
|
245
|
+
hasNudge,
|
|
246
|
+
className,
|
|
247
|
+
}: PolicyAIFABProps) {
|
|
248
|
+
return (
|
|
249
|
+
<div className={cn("relative inline-flex shrink-0", className)}>
|
|
250
|
+
<Button
|
|
251
|
+
data-slot="policy-ai-fab"
|
|
252
|
+
size="icon"
|
|
253
|
+
onClick={onClick}
|
|
254
|
+
className="size-12 shadow-lg rounded-full"
|
|
255
|
+
aria-label="Open Policy AI"
|
|
256
|
+
>
|
|
257
|
+
<BrainCircuit className="size-5" aria-hidden="true" />
|
|
258
|
+
</Button>
|
|
259
|
+
{hasNudge && (
|
|
260
|
+
<span className="absolute -top-1 -right-1 size-3 bg-destructive rounded-full border-2 border-background" />
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// PolicyAIPanel (Organism) — Jira Rovo-style floating / inline widget
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Policy AI chat panel — shows suggested questions in home state and
|
|
272
|
+
* structured response cards (SingleBankAnswer / ComparisonTable / RankedList)
|
|
273
|
+
* in chat state.
|
|
274
|
+
*
|
|
275
|
+
* Float mode: fixed-position overlay next to the FAB.
|
|
276
|
+
* Inline mode: fills its parent container — use inside drawers or tab panels.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* <PolicyAIFAB onClick={() => setOpen(true)} />
|
|
280
|
+
* <PolicyAIPanel
|
|
281
|
+
* open={open}
|
|
282
|
+
* onClose={() => setOpen(false)}
|
|
283
|
+
* messages={messages}
|
|
284
|
+
* onSendMessage={handleSend}
|
|
285
|
+
* onReset={() => setMessages([])}
|
|
286
|
+
* />
|
|
287
|
+
*/
|
|
288
|
+
export function PolicyAIPanel({
|
|
289
|
+
open,
|
|
290
|
+
onClose,
|
|
291
|
+
messages = [],
|
|
292
|
+
suggestedQuestions,
|
|
293
|
+
isStreaming = false,
|
|
294
|
+
isLoading = false,
|
|
295
|
+
thinkingSteps,
|
|
296
|
+
onSendMessage,
|
|
297
|
+
onAttachFile,
|
|
298
|
+
onAttachImage,
|
|
299
|
+
onReset,
|
|
300
|
+
position = "bottom-right",
|
|
301
|
+
inline = false,
|
|
302
|
+
className,
|
|
303
|
+
}: PolicyAIPanelProps) {
|
|
304
|
+
const resolvedThinkingSteps = thinkingSteps ?? DEFAULT_THINKING_STEPS;
|
|
305
|
+
const [inputValue, setInputValue] = React.useState("");
|
|
306
|
+
const [minimised, setMinimised] = React.useState(false);
|
|
307
|
+
const [activeTab, setActiveTab] = React.useState<PolicyTypeTab>("Income");
|
|
308
|
+
const messagesEndRef = React.useRef<HTMLDivElement>(null);
|
|
309
|
+
|
|
310
|
+
const isChatMode = messages.length > 0;
|
|
311
|
+
|
|
312
|
+
// Auto-scroll to latest message
|
|
313
|
+
React.useEffect(() => {
|
|
314
|
+
if (open && !minimised) {
|
|
315
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
316
|
+
}
|
|
317
|
+
}, [messages, open, minimised]);
|
|
318
|
+
|
|
319
|
+
const handleSend = React.useCallback(
|
|
320
|
+
(text: string) => {
|
|
321
|
+
if (!text || isStreaming) return;
|
|
322
|
+
onSendMessage?.(text);
|
|
323
|
+
setInputValue("");
|
|
324
|
+
},
|
|
325
|
+
[isStreaming, onSendMessage],
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (!open) return null;
|
|
329
|
+
|
|
330
|
+
const positionClass = position === "bottom-left" ? "left-6" : "right-6";
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<div
|
|
334
|
+
data-slot="policy-ai-panel"
|
|
335
|
+
className={cn(
|
|
336
|
+
"flex flex-col bg-card",
|
|
337
|
+
inline
|
|
338
|
+
? "h-full border-0"
|
|
339
|
+
: [
|
|
340
|
+
"fixed z-50 border border-border shadow-2xl",
|
|
341
|
+
"bottom-20 w-[400px]",
|
|
342
|
+
"animate-in slide-in-from-bottom-4 fade-in duration-200",
|
|
343
|
+
positionClass,
|
|
344
|
+
minimised && "shadow-lg",
|
|
345
|
+
],
|
|
346
|
+
className,
|
|
347
|
+
)}
|
|
348
|
+
style={
|
|
349
|
+
inline
|
|
350
|
+
? undefined
|
|
351
|
+
: {
|
|
352
|
+
maxHeight: minimised ? "auto" : "min(600px, calc(100vh - 120px))",
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
>
|
|
356
|
+
{/* ── Header — float mode only ── */}
|
|
357
|
+
{!inline && (
|
|
358
|
+
<div className="shrink-0 flex items-center gap-2.5 px-3 py-2.5 border-b border-border bg-card">
|
|
359
|
+
<BrainCircuit
|
|
360
|
+
className="size-4 text-primary shrink-0"
|
|
361
|
+
aria-hidden="true"
|
|
362
|
+
/>
|
|
363
|
+
<span className="text-sm font-semibold text-foreground flex-1 truncate">
|
|
364
|
+
Policy AI
|
|
365
|
+
</span>
|
|
366
|
+
|
|
367
|
+
<div className="flex items-center gap-0.5">
|
|
368
|
+
{isChatMode && onReset && !minimised && (
|
|
369
|
+
<Button
|
|
370
|
+
variant="ghost"
|
|
371
|
+
size="icon-sm"
|
|
372
|
+
onClick={onReset}
|
|
373
|
+
title="New conversation"
|
|
374
|
+
aria-label="New conversation"
|
|
375
|
+
>
|
|
376
|
+
<RotateCcw className="size-3.5" aria-hidden="true" />
|
|
377
|
+
</Button>
|
|
378
|
+
)}
|
|
379
|
+
<Button
|
|
380
|
+
variant="ghost"
|
|
381
|
+
size="icon-sm"
|
|
382
|
+
onClick={() => setMinimised((v) => !v)}
|
|
383
|
+
title={minimised ? "Restore" : "Minimise"}
|
|
384
|
+
aria-label={minimised ? "Restore panel" : "Minimise panel"}
|
|
385
|
+
>
|
|
386
|
+
{minimised ? (
|
|
387
|
+
<ChevronDown className="size-3.5" aria-hidden="true" />
|
|
388
|
+
) : (
|
|
389
|
+
<Minus className="size-3.5" aria-hidden="true" />
|
|
390
|
+
)}
|
|
391
|
+
</Button>
|
|
392
|
+
<Button
|
|
393
|
+
variant="ghost"
|
|
394
|
+
size="icon-sm"
|
|
395
|
+
onClick={onClose}
|
|
396
|
+
title="Close"
|
|
397
|
+
aria-label="Close Policy AI"
|
|
398
|
+
>
|
|
399
|
+
<X className="size-3.5" aria-hidden="true" />
|
|
400
|
+
</Button>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
{/* ── Body ── */}
|
|
406
|
+
{(!minimised || inline) && (
|
|
407
|
+
<>
|
|
408
|
+
{/* Scroll container:
|
|
409
|
+
Float — scrolls internally (flex-1 + overflow-y-auto).
|
|
410
|
+
Inline — parent tab/drawer is the scroller; content flows naturally. */}
|
|
411
|
+
<div className={cn(!inline && "flex-1 overflow-y-auto min-h-0")}>
|
|
412
|
+
{isLoading ? (
|
|
413
|
+
<div className="flex flex-col justify-center h-full py-8">
|
|
414
|
+
<PolicyAIThinkingSteps steps={resolvedThinkingSteps} />
|
|
415
|
+
</div>
|
|
416
|
+
) : !isChatMode ? (
|
|
417
|
+
/* Home state — suggested questions by policy type */
|
|
418
|
+
<div className="flex flex-col">
|
|
419
|
+
<div className="px-3 pt-3 pb-2">
|
|
420
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
421
|
+
Ask me about lending policies across 40+ Australian banks —
|
|
422
|
+
income, LVR, security types, serviceability, and more.
|
|
423
|
+
</p>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<div className="border-b border-border px-3 pt-1">
|
|
427
|
+
<Tabs
|
|
428
|
+
value={activeTab}
|
|
429
|
+
onValueChange={(v) => setActiveTab(v as PolicyTypeTab)}
|
|
430
|
+
>
|
|
431
|
+
<TabsList
|
|
432
|
+
variant="line"
|
|
433
|
+
className="justify-start h-8 gap-0 -mb-px overflow-x-auto"
|
|
434
|
+
>
|
|
435
|
+
{(Object.keys(DEFAULT_SUGGESTED) as PolicyTypeTab[]).map(
|
|
436
|
+
(type) => (
|
|
437
|
+
<TabsTrigger
|
|
438
|
+
key={type}
|
|
439
|
+
value={type}
|
|
440
|
+
className="text-xs px-2 h-7 shrink-0"
|
|
441
|
+
>
|
|
442
|
+
{type}
|
|
443
|
+
</TabsTrigger>
|
|
444
|
+
),
|
|
445
|
+
)}
|
|
446
|
+
</TabsList>
|
|
447
|
+
</Tabs>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<div className="flex flex-col px-3 py-2 gap-1">
|
|
451
|
+
{(
|
|
452
|
+
(suggestedQuestions ?? DEFAULT_SUGGESTED)[activeTab] ?? []
|
|
453
|
+
).map((q) => (
|
|
454
|
+
<button
|
|
455
|
+
key={q}
|
|
456
|
+
onClick={() => setInputValue(q)}
|
|
457
|
+
className="w-full text-left text-xs text-foreground px-3 py-2 border border-border hover:bg-muted/60 transition-colors leading-relaxed"
|
|
458
|
+
>
|
|
459
|
+
{q}
|
|
460
|
+
</button>
|
|
461
|
+
))}
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
) : (
|
|
465
|
+
/* Chat state — messages + structured response cards */
|
|
466
|
+
<div className="flex flex-col gap-4 px-3 py-3">
|
|
467
|
+
{messages.map((msg) =>
|
|
468
|
+
msg.role === "user" ? (
|
|
469
|
+
<ChatWidgetMessage
|
|
470
|
+
key={msg.id}
|
|
471
|
+
role="user"
|
|
472
|
+
content={msg.content}
|
|
473
|
+
/>
|
|
474
|
+
) : (
|
|
475
|
+
<div key={msg.id} className="flex flex-col gap-1.5">
|
|
476
|
+
{msg.content && (
|
|
477
|
+
<p className="text-sm text-foreground leading-relaxed">
|
|
478
|
+
{msg.content}
|
|
479
|
+
</p>
|
|
480
|
+
)}
|
|
481
|
+
{msg.responseContent && (
|
|
482
|
+
<ResponseCard
|
|
483
|
+
content={msg.responseContent}
|
|
484
|
+
queryContext={msg.queryContext}
|
|
485
|
+
/>
|
|
486
|
+
)}
|
|
487
|
+
</div>
|
|
488
|
+
),
|
|
489
|
+
)}
|
|
490
|
+
|
|
491
|
+
{isStreaming && (
|
|
492
|
+
<PolicyAIThinkingSteps steps={resolvedThinkingSteps} />
|
|
493
|
+
)}
|
|
494
|
+
|
|
495
|
+
<div ref={messagesEndRef} />
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
{/* Footer input — hidden during loading.
|
|
501
|
+
Float: shrink-0 pins it to the flex column bottom.
|
|
502
|
+
Inline: sticky bottom-0 keeps it visible as the parent tab scrolls. */}
|
|
503
|
+
{!isLoading && (
|
|
504
|
+
<div
|
|
505
|
+
className={cn(
|
|
506
|
+
"border-t border-border px-3 py-3 bg-card",
|
|
507
|
+
inline ? "sticky bottom-0 z-10" : "shrink-0",
|
|
508
|
+
)}
|
|
509
|
+
>
|
|
510
|
+
<ChatInputArea
|
|
511
|
+
value={inputValue}
|
|
512
|
+
onChange={setInputValue}
|
|
513
|
+
onSend={handleSend}
|
|
514
|
+
onAttachFile={onAttachFile}
|
|
515
|
+
onAttachImage={onAttachImage}
|
|
516
|
+
disabled={isStreaming}
|
|
517
|
+
placeholder="Ask about lending policies…"
|
|
518
|
+
autoFocus={!minimised}
|
|
519
|
+
/>
|
|
520
|
+
</div>
|
|
521
|
+
)}
|
|
522
|
+
</>
|
|
523
|
+
)}
|
|
524
|
+
</div>
|
|
525
|
+
);
|
|
526
|
+
}
|