@flamingo-stack/openframe-frontend-core 0.0.685-snapshot.20260929164409 → 0.0.685
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/{chunk-DL67S7H4.cjs → chunk-5LJZPJHX.cjs} +59 -59
- package/dist/{chunk-DL67S7H4.cjs.map → chunk-5LJZPJHX.cjs.map} +1 -1
- package/dist/{chunk-IZ3WKG62.js → chunk-65KTXRBY.js} +6 -6
- package/dist/{chunk-IBKMIJ7Y.js → chunk-7XF63THA.js} +6 -6
- package/dist/{chunk-SI77HF4U.cjs → chunk-A3CSQB4A.cjs} +805 -1247
- package/dist/chunk-A3CSQB4A.cjs.map +1 -0
- package/dist/{chunk-5MUR43QD.cjs → chunk-B25CF3PO.cjs} +30 -30
- package/dist/{chunk-5MUR43QD.cjs.map → chunk-B25CF3PO.cjs.map} +1 -1
- package/dist/{chunk-32KBPEF7.js → chunk-B32YLOYQ.js} +4 -4
- package/dist/{chunk-OBLED7JY.cjs → chunk-B5N3AHPG.cjs} +81 -81
- package/dist/{chunk-OBLED7JY.cjs.map → chunk-B5N3AHPG.cjs.map} +1 -1
- package/dist/{chunk-QSRUYHTW.js → chunk-BZ2XOEHW.js} +3 -3
- package/dist/{chunk-XATA4OVO.cjs → chunk-C3B36OYZ.cjs} +3 -3
- package/dist/{chunk-XATA4OVO.cjs.map → chunk-C3B36OYZ.cjs.map} +1 -1
- package/dist/{chunk-V26TKJ4Z.cjs → chunk-CMQXPD5C.cjs} +28 -28
- package/dist/{chunk-V26TKJ4Z.cjs.map → chunk-CMQXPD5C.cjs.map} +1 -1
- package/dist/{chunk-QNWQQ4RT.cjs → chunk-DQOQ3KTP.cjs} +73 -78
- package/dist/chunk-DQOQ3KTP.cjs.map +1 -0
- package/dist/{chunk-5HVGOIAE.cjs → chunk-DWXWEGGO.cjs} +22 -22
- package/dist/{chunk-5HVGOIAE.cjs.map → chunk-DWXWEGGO.cjs.map} +1 -1
- package/dist/{chunk-6CMH53MP.cjs → chunk-EFBMNVSH.cjs} +15 -15
- package/dist/{chunk-6CMH53MP.cjs.map → chunk-EFBMNVSH.cjs.map} +1 -1
- package/dist/{chunk-DDFYKF3B.js → chunk-GHLGXFNU.js} +3 -3
- package/dist/{chunk-VDV7K5TD.js → chunk-GXV6UEVY.js} +3 -3
- package/dist/{chunk-AEAFQTVR.cjs → chunk-HMXUOLZR.cjs} +20 -20
- package/dist/{chunk-AEAFQTVR.cjs.map → chunk-HMXUOLZR.cjs.map} +1 -1
- package/dist/{chunk-2OD2PPNO.cjs → chunk-K2HZG4RM.cjs} +32 -32
- package/dist/{chunk-2OD2PPNO.cjs.map → chunk-K2HZG4RM.cjs.map} +1 -1
- package/dist/{chunk-JNDHXL2D.cjs → chunk-M4HYIXP6.cjs} +383 -384
- package/dist/chunk-M4HYIXP6.cjs.map +1 -0
- package/dist/{chunk-KTWJKCUB.js → chunk-N4Z6X5BV.js} +5 -5
- package/dist/{chunk-T2RHI676.js → chunk-O7LOZW4R.js} +5 -5
- package/dist/{chunk-ZHYYGIAI.cjs → chunk-P2ASEPA2.cjs} +6 -6
- package/dist/{chunk-ZHYYGIAI.cjs.map → chunk-P2ASEPA2.cjs.map} +1 -1
- package/dist/{chunk-CHRDLXXX.js → chunk-PSQKD6ZN.js} +3 -3
- package/dist/{chunk-GSJR33UP.cjs → chunk-QHLJSB2J.cjs} +12 -12
- package/dist/{chunk-GSJR33UP.cjs.map → chunk-QHLJSB2J.cjs.map} +1 -1
- package/dist/{chunk-CFL3EW4V.cjs → chunk-T4DUTP5C.cjs} +5 -5
- package/dist/{chunk-CFL3EW4V.cjs.map → chunk-T4DUTP5C.cjs.map} +1 -1
- package/dist/{chunk-22RSJ57Z.js → chunk-T6KWNJDW.js} +465 -470
- package/dist/chunk-T6KWNJDW.js.map +1 -0
- package/dist/{chunk-2FGFB2HY.cjs → chunk-TJI5V5BR.cjs} +7 -7
- package/dist/{chunk-2FGFB2HY.cjs.map → chunk-TJI5V5BR.cjs.map} +1 -1
- package/dist/{chunk-HLEXYTOE.js → chunk-TUG7IT6H.js} +2 -2
- package/dist/{chunk-DWCVA6XT.cjs → chunk-U3IXG7TR.cjs} +33 -33
- package/dist/{chunk-DWCVA6XT.cjs.map → chunk-U3IXG7TR.cjs.map} +1 -1
- package/dist/{chunk-WJ5XN4AL.js → chunk-UT333EOT.js} +4 -4
- package/dist/{chunk-OTV4W3YD.js → chunk-VDPCK437.js} +5 -6
- package/dist/{chunk-OTV4W3YD.js.map → chunk-VDPCK437.js.map} +1 -1
- package/dist/{chunk-5GK7TU2Q.js → chunk-WJK2EETW.js} +2 -2
- package/dist/{chunk-YPXXXX6R.js → chunk-XGX6JSEU.js} +1644 -2086
- package/dist/chunk-XGX6JSEU.js.map +1 -0
- package/dist/{chunk-XODE4YTG.js → chunk-XW7MUIW4.js} +3 -3
- package/dist/{chunk-N74MQ4HQ.js → chunk-Y7ZYOQNB.js} +3 -3
- package/dist/{chunk-2NUB5EMG.cjs → chunk-YCTVLN47.cjs} +10 -10
- package/dist/{chunk-2NUB5EMG.cjs.map → chunk-YCTVLN47.cjs.map} +1 -1
- package/dist/{chunk-TILC52ZF.js → chunk-ZF4Z36G4.js} +7 -7
- package/dist/components/case-studies/index.cjs +16 -16
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/ai-assistant-info.d.ts.map +1 -1
- package/dist/components/chat/approval-request-message.d.ts.map +1 -1
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/error-message-display.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +3 -21
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +0 -6
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +2 -20
- package/dist/components/chat/msp-organization-card.d.ts +1 -7
- package/dist/components/chat/msp-organization-card.d.ts.map +1 -1
- package/dist/components/chat/types/chat.types.d.ts +0 -13
- package/dist/components/chat/types/chat.types.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +1 -20
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +8 -8
- package/dist/components/docs/index.js +7 -7
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +91 -91
- package/dist/components/help-center-pages/index.js +15 -15
- package/dist/components/index.cjs +55 -73
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +15 -33
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +41 -41
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/avatar-stack.d.ts +1 -5
- package/dist/components/ui/avatar-stack.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +4 -22
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +3 -21
- package/dist/components/ui/reopen-ticket-request-modal.d.ts +1 -4
- package/dist/components/ui/reopen-ticket-request-modal.d.ts.map +1 -1
- package/dist/index.cjs +9 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/ai-assistant-info.tsx +1 -7
- package/src/components/chat/approval-request-message.tsx +1 -6
- package/src/components/chat/chat-input.tsx +1 -26
- package/src/components/chat/chat-message-enhanced.tsx +142 -180
- package/src/components/chat/chat-message-list.tsx +164 -169
- package/src/components/chat/error-message-display.tsx +3 -21
- package/src/components/chat/index.ts +0 -6
- package/src/components/chat/msp-organization-card.tsx +0 -35
- package/src/components/chat/types/chat.types.ts +0 -15
- package/src/components/chat/types/component.types.ts +0 -20
- package/src/components/ui/avatar-stack.tsx +5 -15
- package/src/components/ui/reopen-ticket-request-modal.tsx +2 -6
- package/src/styles/ods-colors.css +0 -10
- package/src/styles/ods-effects.css +0 -13
- package/dist/chunk-22RSJ57Z.js.map +0 -1
- package/dist/chunk-JNDHXL2D.cjs.map +0 -1
- package/dist/chunk-QNWQQ4RT.cjs.map +0 -1
- package/dist/chunk-SI77HF4U.cjs.map +0 -1
- package/dist/chunk-YPXXXX6R.js.map +0 -1
- package/dist/components/chat/chat-appearance-context.d.ts +0 -9
- package/dist/components/chat/chat-appearance-context.d.ts.map +0 -1
- package/dist/components/chat/chat-nav-sidebar.d.ts +0 -50
- package/dist/components/chat/chat-nav-sidebar.d.ts.map +0 -1
- package/dist/components/chat/chat-top-navigation.d.ts +0 -29
- package/dist/components/chat/chat-top-navigation.d.ts.map +0 -1
- package/dist/components/chat/onboarding-carousel.d.ts +0 -29
- package/dist/components/chat/onboarding-carousel.d.ts.map +0 -1
- package/dist/components/chat/system-event-message.d.ts +0 -14
- package/dist/components/chat/system-event-message.d.ts.map +0 -1
- package/src/components/chat/__tests__/chat-appearance.test.tsx +0 -79
- package/src/components/chat/__tests__/onboarding-carousel.test.tsx +0 -56
- package/src/components/chat/chat-appearance-context.tsx +0 -15
- package/src/components/chat/chat-nav-sidebar.tsx +0 -163
- package/src/components/chat/chat-top-navigation.tsx +0 -109
- package/src/components/chat/onboarding-carousel.tsx +0 -162
- package/src/components/chat/system-event-message.tsx +0 -54
- package/src/components/ui/__tests__/avatar-stack.test.tsx +0 -25
- package/src/stories/FaeLayoutV2.stories.tsx +0 -150
- package/src/stories/FaeOnboardingV2.stories.tsx +0 -68
- package/src/stories/FaeThreadV2.stories.tsx +0 -192
- package/src/stories/fae-brand-decorator.tsx +0 -28
- /package/dist/{chunk-IZ3WKG62.js.map → chunk-65KTXRBY.js.map} +0 -0
- /package/dist/{chunk-IBKMIJ7Y.js.map → chunk-7XF63THA.js.map} +0 -0
- /package/dist/{chunk-32KBPEF7.js.map → chunk-B32YLOYQ.js.map} +0 -0
- /package/dist/{chunk-QSRUYHTW.js.map → chunk-BZ2XOEHW.js.map} +0 -0
- /package/dist/{chunk-DDFYKF3B.js.map → chunk-GHLGXFNU.js.map} +0 -0
- /package/dist/{chunk-VDV7K5TD.js.map → chunk-GXV6UEVY.js.map} +0 -0
- /package/dist/{chunk-KTWJKCUB.js.map → chunk-N4Z6X5BV.js.map} +0 -0
- /package/dist/{chunk-T2RHI676.js.map → chunk-O7LOZW4R.js.map} +0 -0
- /package/dist/{chunk-CHRDLXXX.js.map → chunk-PSQKD6ZN.js.map} +0 -0
- /package/dist/{chunk-HLEXYTOE.js.map → chunk-TUG7IT6H.js.map} +0 -0
- /package/dist/{chunk-WJ5XN4AL.js.map → chunk-UT333EOT.js.map} +0 -0
- /package/dist/{chunk-5GK7TU2Q.js.map → chunk-WJK2EETW.js.map} +0 -0
- /package/dist/{chunk-XODE4YTG.js.map → chunk-XW7MUIW4.js.map} +0 -0
- /package/dist/{chunk-N74MQ4HQ.js.map → chunk-Y7ZYOQNB.js.map} +0 -0
- /package/dist/{chunk-TILC52ZF.js.map → chunk-ZF4Z36G4.js.map} +0 -0
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { type CSSProperties, type KeyboardEvent, type ReactNode, useState } from 'react';
|
|
4
|
-
import { cn } from '../../utils/cn';
|
|
5
|
-
import { Button } from '../ui/button';
|
|
6
|
-
|
|
7
|
-
export interface OnboardingCarouselStep {
|
|
8
|
-
id: string;
|
|
9
|
-
/** Glyph in the 64px tile above the title. Pass it sized to 28px. */
|
|
10
|
-
icon: ReactNode;
|
|
11
|
-
title: ReactNode;
|
|
12
|
-
description?: ReactNode;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface OnboardingCarouselProps {
|
|
16
|
-
steps: OnboardingCarouselStep[];
|
|
17
|
-
/** The last step's primary action. */
|
|
18
|
-
onComplete: () => void;
|
|
19
|
-
/** Label of the last step's primary action. */
|
|
20
|
-
completeLabel?: string;
|
|
21
|
-
previousLabel?: string;
|
|
22
|
-
nextLabel?: string;
|
|
23
|
-
/** Accessible name of the carousel region. */
|
|
24
|
-
label?: string;
|
|
25
|
-
className?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Step-by-step introduction: one card at a time in the middle, its neighbours
|
|
30
|
-
* peeking in from the sides under an edge fade, dots below, Previous / Next
|
|
31
|
-
* underneath. The last step swaps Next for `completeLabel`. There is no skip:
|
|
32
|
-
* the design walks every step. Fills the height its parent gives it: the cards
|
|
33
|
-
* stretch, the dots and buttons keep their size.
|
|
34
|
-
*/
|
|
35
|
-
export function OnboardingCarousel({
|
|
36
|
-
steps,
|
|
37
|
-
onComplete,
|
|
38
|
-
completeLabel = 'Start Chat',
|
|
39
|
-
previousLabel = 'Previous Step',
|
|
40
|
-
nextLabel = 'Next Step',
|
|
41
|
-
label = 'Introduction',
|
|
42
|
-
className,
|
|
43
|
-
}: OnboardingCarouselProps) {
|
|
44
|
-
const [index, setIndex] = useState(0);
|
|
45
|
-
const lastIndex = steps.length - 1;
|
|
46
|
-
const isFirst = index === 0;
|
|
47
|
-
const isLast = index === lastIndex;
|
|
48
|
-
|
|
49
|
-
const goTo = (next: number) => setIndex(Math.min(Math.max(next, 0), lastIndex));
|
|
50
|
-
|
|
51
|
-
const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {
|
|
52
|
-
if (event.key === 'ArrowRight') goTo(index + 1);
|
|
53
|
-
else if (event.key === 'ArrowLeft') goTo(index - 1);
|
|
54
|
-
else return;
|
|
55
|
-
event.preventDefault();
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Card width and the gap between cards drive both the card size and the
|
|
59
|
-
// track offset, so they live in one place. Percentages resolve against the
|
|
60
|
-
// viewport width in both uses (the track is as wide as the viewport).
|
|
61
|
-
const trackStyle = {
|
|
62
|
-
'--onboarding-card-w': 'min(600px, calc(100% - 2 * var(--spacing-system-xl)))',
|
|
63
|
-
'--onboarding-gap': 'var(--spacing-system-lf)',
|
|
64
|
-
transform: `translateX(calc(${-index} * (var(--onboarding-card-w) + var(--onboarding-gap))))`,
|
|
65
|
-
} as CSSProperties;
|
|
66
|
-
|
|
67
|
-
// The design's large steps (card padding, dots to buttons) are 80px at the
|
|
68
|
-
// app's 1024px default width, where `--spacing-system-xxl` is still 48px:
|
|
69
|
-
// spelled as twice the fixed 40px step.
|
|
70
|
-
return (
|
|
71
|
-
<section
|
|
72
|
-
aria-roledescription="carousel"
|
|
73
|
-
aria-label={label}
|
|
74
|
-
onKeyDown={handleKeyDown}
|
|
75
|
-
className={cn(
|
|
76
|
-
'flex min-h-0 w-full flex-1 flex-col items-center gap-[calc(2*var(--spacing-system-xlf))]',
|
|
77
|
-
className,
|
|
78
|
-
)}
|
|
79
|
-
>
|
|
80
|
-
<div className="flex min-h-0 w-full flex-1 flex-col items-center gap-[var(--spacing-system-xs)]">
|
|
81
|
-
<div className="relative flex min-h-0 w-full flex-1 overflow-hidden">
|
|
82
|
-
<div
|
|
83
|
-
className="flex w-full gap-[var(--onboarding-gap)] transition-transform duration-300 ease-out motion-reduce:transition-none"
|
|
84
|
-
style={trackStyle}
|
|
85
|
-
>
|
|
86
|
-
{steps.map((step, i) => {
|
|
87
|
-
const isCurrent = i === index;
|
|
88
|
-
return (
|
|
89
|
-
<div
|
|
90
|
-
key={step.id}
|
|
91
|
-
role="group"
|
|
92
|
-
aria-roledescription="slide"
|
|
93
|
-
aria-label={`${i + 1} of ${steps.length}`}
|
|
94
|
-
aria-hidden={!isCurrent}
|
|
95
|
-
inert={!isCurrent || undefined}
|
|
96
|
-
className={cn(
|
|
97
|
-
'flex w-[var(--onboarding-card-w)] shrink-0 flex-col items-center justify-center gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card px-[var(--spacing-system-xl)] py-[calc(2*var(--spacing-system-xlf))] text-center',
|
|
98
|
-
i === 0 && 'ml-[calc((100%-var(--onboarding-card-w))/2)]',
|
|
99
|
-
)}
|
|
100
|
-
>
|
|
101
|
-
<span className="flex size-16 shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-bg text-ods-accent">
|
|
102
|
-
{step.icon}
|
|
103
|
-
</span>
|
|
104
|
-
<div className="flex flex-col gap-[var(--spacing-system-l)] text-ods-text-primary [word-break:break-word]">
|
|
105
|
-
<h2 className="text-h2">{step.title}</h2>
|
|
106
|
-
{step.description && <p className="whitespace-pre-line text-h6">{step.description}</p>}
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
})}
|
|
111
|
-
</div>
|
|
112
|
-
<div className="pointer-events-none absolute inset-y-0 left-0 w-[120px] bg-gradient-to-r from-ods-bg to-transparent" />
|
|
113
|
-
<div className="pointer-events-none absolute inset-y-0 right-0 w-[120px] bg-gradient-to-l from-ods-bg to-transparent" />
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
{/* The slides only toggle visibility, which is not announced. */}
|
|
117
|
-
<p className="sr-only" aria-live="polite">
|
|
118
|
-
Step {index + 1} of {steps.length}: {steps[index]?.title}
|
|
119
|
-
</p>
|
|
120
|
-
|
|
121
|
-
<div className="flex items-center" role="group" aria-label="Choose step">
|
|
122
|
-
{steps.map((step, i) => {
|
|
123
|
-
const isCurrent = i === index;
|
|
124
|
-
return (
|
|
125
|
-
<Button
|
|
126
|
-
key={step.id}
|
|
127
|
-
variant="transparent"
|
|
128
|
-
aria-current={isCurrent ? 'step' : undefined}
|
|
129
|
-
aria-label={`Go to step ${i + 1}`}
|
|
130
|
-
onClick={() => goTo(i)}
|
|
131
|
-
className="flex size-6 shrink-0 items-center justify-center rounded-full p-0 md:h-6"
|
|
132
|
-
>
|
|
133
|
-
<span
|
|
134
|
-
className={cn(
|
|
135
|
-
'size-1.5 rounded-full transition-colors duration-150',
|
|
136
|
-
isCurrent ? 'bg-ods-accent' : 'bg-ods-text-secondary',
|
|
137
|
-
)}
|
|
138
|
-
/>
|
|
139
|
-
</Button>
|
|
140
|
-
);
|
|
141
|
-
})}
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<div className="flex flex-wrap items-center justify-center gap-[var(--spacing-system-m)]">
|
|
146
|
-
{/* aria-disabled, not disabled: stepping back onto the first card must
|
|
147
|
-
not drop keyboard focus from the button the user just pressed. */}
|
|
148
|
-
<Button
|
|
149
|
-
variant="outline"
|
|
150
|
-
className="w-[200px]"
|
|
151
|
-
aria-disabled={isFirst || undefined}
|
|
152
|
-
onClick={() => goTo(index - 1)}
|
|
153
|
-
>
|
|
154
|
-
{previousLabel}
|
|
155
|
-
</Button>
|
|
156
|
-
<Button variant="accent" className="w-[200px]" onClick={isLast ? onComplete : () => goTo(index + 1)}>
|
|
157
|
-
{isLast ? completeLabel : nextLabel}
|
|
158
|
-
</Button>
|
|
159
|
-
</div>
|
|
160
|
-
</section>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { forwardRef, type HTMLAttributes } from 'react';
|
|
4
|
-
import { InfoCircleIcon } from '../icons-v2-generated';
|
|
5
|
-
import { SquareAvatar } from '../ui/square-avatar';
|
|
6
|
-
import { AiAssistantInfo } from './ai-assistant-info';
|
|
7
|
-
|
|
8
|
-
// The chat backend announces a technician taking the chat over (or handing it
|
|
9
|
-
// back) as a plain system line: "{name} joined the chat" / "{name} left the
|
|
10
|
-
// chat". The copy is fixed server-side, so these two shapes are the contract.
|
|
11
|
-
const JOINED = /^(.+) joined the chat$/;
|
|
12
|
-
const LEFT = /^(.+) left the chat$/;
|
|
13
|
-
|
|
14
|
-
export interface SystemEventMessageProps extends HTMLAttributes<HTMLDivElement> {
|
|
15
|
-
/** The system line as the backend sent it. */
|
|
16
|
-
text: string;
|
|
17
|
-
timestamp?: Date;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* A system line in the fae chat v2 thread (`ai-assistant-info`,
|
|
22
|
-
* type=direct-chat): a technician joining becomes "Technician Joined" with
|
|
23
|
-
* their initials; any other line keeps its own wording under an info icon.
|
|
24
|
-
*/
|
|
25
|
-
const SystemEventMessage = forwardRef<HTMLDivElement, SystemEventMessageProps>(({ text, timestamp, ...props }, ref) => {
|
|
26
|
-
const joined = JOINED.exec(text)?.[1];
|
|
27
|
-
if (joined) {
|
|
28
|
-
return (
|
|
29
|
-
<AiAssistantInfo
|
|
30
|
-
ref={ref}
|
|
31
|
-
leading={<SquareAvatar alt={joined} fallback={joined} size="lg" variant="round" className="shrink-0" />}
|
|
32
|
-
title="Technician Joined"
|
|
33
|
-
body={`You're now chatting with ${joined}.`}
|
|
34
|
-
timestamp={timestamp}
|
|
35
|
-
{...props}
|
|
36
|
-
/>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
const left = LEFT.exec(text)?.[1];
|
|
40
|
-
return (
|
|
41
|
-
<AiAssistantInfo
|
|
42
|
-
ref={ref}
|
|
43
|
-
icon={<InfoCircleIcon className="size-6 text-ods-accent" />}
|
|
44
|
-
title={left ? 'Technician Left' : text}
|
|
45
|
-
body={left ? `${left} left the conversation.` : undefined}
|
|
46
|
-
timestamp={timestamp}
|
|
47
|
-
{...props}
|
|
48
|
-
/>
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
SystemEventMessage.displayName = 'SystemEventMessage';
|
|
53
|
-
|
|
54
|
-
export { SystemEventMessage };
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { AvatarStack } from '../avatar-stack';
|
|
4
|
-
|
|
5
|
-
const people = [{ name: 'Ada Lin' }, { name: 'Roman Smith' }, { name: 'Michael Johnson' }];
|
|
6
|
-
|
|
7
|
-
describe('AvatarStack', () => {
|
|
8
|
-
it('counts the overflow from the people it gets', () => {
|
|
9
|
-
render(<AvatarStack people={[...people, { name: 'Kim Park' }]} max={3} label="Technicians" />);
|
|
10
|
-
expect(screen.getByText('+1')).toHaveAttribute('title', 'Kim Park');
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('counts everyone not shown when the people are a sample of a larger group', () => {
|
|
14
|
-
render(<AvatarStack people={people} total={7} max={3} label="Technicians" />);
|
|
15
|
-
expect(screen.getByText('+4')).toHaveAttribute('title', '4 more');
|
|
16
|
-
expect(screen.getByRole('group')).toHaveAccessibleName(
|
|
17
|
-
'Technicians: Ada Lin, Roman Smith, Michael Johnson and 4 more',
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('shows no overflow when the sample is the whole group', () => {
|
|
22
|
-
render(<AvatarStack people={people} total={3} max={3} label="Technicians" />);
|
|
23
|
-
expect(screen.queryByText(/^\+/)).not.toBeInTheDocument();
|
|
24
|
-
});
|
|
25
|
-
});
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
|
|
4
|
-
import { ChatInput } from '../components/chat/chat-input';
|
|
5
|
-
import { ChatListEmptyState } from '../components/chat/chat-list-empty-state';
|
|
6
|
-
import { ChatMessageList } from '../components/chat/chat-message-list';
|
|
7
|
-
import { ChatListItem, ChatNavSidebar } from '../components/chat/chat-nav-sidebar';
|
|
8
|
-
import { ChatTopNavigation } from '../components/chat/chat-top-navigation';
|
|
9
|
-
import { MspOrganizationCard } from '../components/chat/msp-organization-card';
|
|
10
|
-
import type { Message } from '../components/chat/types/message.types';
|
|
11
|
-
import { OpenFrameLogo } from '../components/icons';
|
|
12
|
-
import {
|
|
13
|
-
ChatPlusIcon,
|
|
14
|
-
ChatQuestionIcon,
|
|
15
|
-
ChatsIcon,
|
|
16
|
-
CheckCircleIcon,
|
|
17
|
-
ClockHistoryIcon,
|
|
18
|
-
DotsLoaderIcon,
|
|
19
|
-
HourglassClockIcon,
|
|
20
|
-
MessageIndicatorIcon,
|
|
21
|
-
UserCheckIcon,
|
|
22
|
-
} from '../components/icons-v2-generated';
|
|
23
|
-
import { Button } from '../components/ui/button';
|
|
24
|
-
import { withFaeBrand } from './fae-brand-decorator';
|
|
25
|
-
|
|
26
|
-
const at = (minute: number) => new Date(2026, 8, 29, 14, minute);
|
|
27
|
-
|
|
28
|
-
const THREAD: Message[] = [
|
|
29
|
-
{
|
|
30
|
-
id: 'u1',
|
|
31
|
-
role: 'user',
|
|
32
|
-
name: 'John Smith',
|
|
33
|
-
content: 'My laptop has been running really slowly lately',
|
|
34
|
-
timestamp: at(47),
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: 'a1',
|
|
38
|
-
role: 'assistant',
|
|
39
|
-
name: 'Fae',
|
|
40
|
-
assistantType: 'fae',
|
|
41
|
-
content: "I'll run a quick diagnostic to identify what's causing the performance issues.",
|
|
42
|
-
timestamp: at(47),
|
|
43
|
-
},
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const CHATS = [
|
|
47
|
-
{ id: '1', title: 'Slow Laptop', mark: <DotsLoaderIcon className="text-ods-accent" /> },
|
|
48
|
-
{ id: '2', title: 'Forgot my password' },
|
|
49
|
-
{ id: '3', title: 'Teams mic not working', mark: <HourglassClockIcon className="text-ods-accent" /> },
|
|
50
|
-
{ id: '4', title: 'Email not syncing on phone', mark: <MessageIndicatorIcon className="text-ods-accent" /> },
|
|
51
|
-
{ id: '5', title: 'Blue screen on startup', resolved: true },
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
function Layout({ withChats, withThread }: { withChats: boolean; withThread: boolean }) {
|
|
55
|
-
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
56
|
-
return (
|
|
57
|
-
<div data-surface="brand-tinted" className="flex h-[800px] w-[1024px] border-t border-ods-border bg-ods-bg">
|
|
58
|
-
{sidebarOpen && (
|
|
59
|
-
<ChatNavSidebar
|
|
60
|
-
logo={
|
|
61
|
-
<span className="flex items-center gap-[var(--spacing-system-xxs)] text-ods-text-primary">
|
|
62
|
-
<OpenFrameLogo
|
|
63
|
-
className="size-6"
|
|
64
|
-
lowerPathColor="var(--ods-open-yellow-base)"
|
|
65
|
-
upperPathColor="var(--color-text-primary)"
|
|
66
|
-
/>
|
|
67
|
-
<span className="text-wordmark">OpenFrame</span>
|
|
68
|
-
</span>
|
|
69
|
-
}
|
|
70
|
-
items={[
|
|
71
|
-
{ id: 'new', label: 'Start New Chat', icon: <ChatPlusIcon />, active: !withThread },
|
|
72
|
-
{ id: 'archive', label: 'Chat Archive', icon: <ClockHistoryIcon /> },
|
|
73
|
-
{ id: 'guide', label: 'What to ask Fae?', icon: <ChatQuestionIcon /> },
|
|
74
|
-
]}
|
|
75
|
-
sectionLabel="Your Current Chats"
|
|
76
|
-
footer={<MspOrganizationCard variant="footer" name="TechFlow Solutions" onOpenWebsite={() => undefined} />}
|
|
77
|
-
>
|
|
78
|
-
{withChats ? (
|
|
79
|
-
CHATS.map((chat, i) => (
|
|
80
|
-
<ChatListItem
|
|
81
|
-
key={chat.id}
|
|
82
|
-
title={chat.title}
|
|
83
|
-
subtitle="Ticket Number: 1932"
|
|
84
|
-
active={withThread && i === 0}
|
|
85
|
-
leading={chat.resolved ? <CheckCircleIcon className="text-ods-success" /> : undefined}
|
|
86
|
-
trailing={chat.mark}
|
|
87
|
-
/>
|
|
88
|
-
))
|
|
89
|
-
) : (
|
|
90
|
-
<ChatListEmptyState
|
|
91
|
-
icon={<ChatsIcon size={24} />}
|
|
92
|
-
title="No Current Chats"
|
|
93
|
-
description="Your conversations will show here"
|
|
94
|
-
/>
|
|
95
|
-
)}
|
|
96
|
-
</ChatNavSidebar>
|
|
97
|
-
)}
|
|
98
|
-
<div className="flex min-w-0 flex-1 flex-col">
|
|
99
|
-
<ChatTopNavigation
|
|
100
|
-
name="Fae"
|
|
101
|
-
subtitle="Your AI Assistant"
|
|
102
|
-
sidebarOpen={sidebarOpen}
|
|
103
|
-
onToggleSidebar={() => setSidebarOpen(open => !open)}
|
|
104
|
-
onOpenSettings={() => undefined}
|
|
105
|
-
actions={
|
|
106
|
-
<Button variant="outline" size="small" leftIcon={<UserCheckIcon />}>
|
|
107
|
-
Request a Technician
|
|
108
|
-
</Button>
|
|
109
|
-
}
|
|
110
|
-
/>
|
|
111
|
-
<main className="ods-glow-accent-corner flex min-h-0 flex-1 flex-col gap-[var(--spacing-system-m)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)] pt-[var(--spacing-system-mf)]">
|
|
112
|
-
{withThread ? (
|
|
113
|
-
<ChatMessageList
|
|
114
|
-
messages={THREAD}
|
|
115
|
-
assistantType="fae"
|
|
116
|
-
approvalVariant="client"
|
|
117
|
-
appearance="v2"
|
|
118
|
-
fullWidth
|
|
119
|
-
contentClassName="px-0"
|
|
120
|
-
autoScroll={false}
|
|
121
|
-
/>
|
|
122
|
-
) : (
|
|
123
|
-
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-[var(--spacing-system-l)]">
|
|
124
|
-
<div className="flex flex-col gap-[var(--spacing-system-xsf)] text-center">
|
|
125
|
-
<h1 className="text-ods-text-primary text-h2">Hi, I'm your IT assistant</h1>
|
|
126
|
-
<p className="text-ods-text-secondary text-h4">
|
|
127
|
-
Having trouble with your computer, email, Wi-Fi, or software? Just describe it.
|
|
128
|
-
</p>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
)}
|
|
132
|
-
<ChatInput fullWidth appearance="v2" placeholder="Enter your request here..." />
|
|
133
|
-
</main>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const meta: Meta<typeof Layout> = {
|
|
140
|
-
decorators: [withFaeBrand],
|
|
141
|
-
title: 'Chat/Fae Layout v2',
|
|
142
|
-
component: Layout,
|
|
143
|
-
parameters: { layout: 'fullscreen' },
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
export default meta;
|
|
147
|
-
type Story = StoryObj<typeof Layout>;
|
|
148
|
-
|
|
149
|
-
export const StartEmpty: Story = { args: { withChats: false, withThread: false } };
|
|
150
|
-
export const ChatOpen: Story = { args: { withChats: true, withThread: true } };
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
-
|
|
3
|
-
import { OnboardingCarousel, type OnboardingCarouselStep } from '../components/chat/onboarding-carousel';
|
|
4
|
-
import { PoweredByFlamingo } from '../components/features/auth/auth-branding';
|
|
5
|
-
import { OpenFrameLogo } from '../components/icons';
|
|
6
|
-
import { ClockCheckIcon, MessagesIcon, UserCheckIcon } from '../components/icons-v2-generated';
|
|
7
|
-
import { SquareAvatar } from '../components/ui/square-avatar';
|
|
8
|
-
import { withFaeBrand } from './fae-brand-decorator';
|
|
9
|
-
|
|
10
|
-
const STEPS: OnboardingCarouselStep[] = [
|
|
11
|
-
{
|
|
12
|
-
id: 'meet',
|
|
13
|
-
icon: <SquareAvatar variant="round" sizePx={28} fallback="Fae" />,
|
|
14
|
-
title: 'Meet Your AI IT Assistant',
|
|
15
|
-
description: 'Fixes what it can right away,\nand hands off the rest to your technicians.',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
id: 'type',
|
|
19
|
-
icon: <MessagesIcon size={28} />,
|
|
20
|
-
title: "Type what's going on.",
|
|
21
|
-
description:
|
|
22
|
-
"Your assistant troubleshoots first.\nAnything it can't solve goes to your technician with full context.",
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: 'human',
|
|
26
|
-
icon: <UserCheckIcon size={28} />,
|
|
27
|
-
title: 'Need a human?',
|
|
28
|
-
description: 'Skip the chat and create a ticket.\nIt goes straight to your technicians.',
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
id: 'anytime',
|
|
32
|
-
icon: <ClockCheckIcon size={28} />,
|
|
33
|
-
title: 'Ask anything, anytime.',
|
|
34
|
-
description: 'No hold music, no queue.\nJust immediate help.',
|
|
35
|
-
},
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
function Screen() {
|
|
39
|
-
return (
|
|
40
|
-
<div className="flex h-[800px] w-[1024px] flex-col items-center gap-[calc(2*var(--spacing-system-xlf))] bg-ods-bg px-[calc(2*var(--spacing-system-xlf))] py-[var(--spacing-system-xl)]">
|
|
41
|
-
<span className="flex items-center gap-[var(--spacing-system-xxs)] text-ods-text-primary">
|
|
42
|
-
<OpenFrameLogo
|
|
43
|
-
className="size-6"
|
|
44
|
-
lowerPathColor="var(--ods-open-yellow-base)"
|
|
45
|
-
upperPathColor="var(--color-text-primary)"
|
|
46
|
-
/>
|
|
47
|
-
<span className="text-wordmark">OpenFrame</span>
|
|
48
|
-
</span>
|
|
49
|
-
{/* The side cards run to the window edge, past the screen padding. */}
|
|
50
|
-
<div className="-mx-[calc(2*var(--spacing-system-xlf))] flex min-h-0 flex-1 self-stretch">
|
|
51
|
-
<OnboardingCarousel steps={STEPS} onComplete={() => undefined} />
|
|
52
|
-
</div>
|
|
53
|
-
<PoweredByFlamingo />
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const meta: Meta<typeof Screen> = {
|
|
59
|
-
decorators: [withFaeBrand],
|
|
60
|
-
title: 'Chat/Fae Onboarding v2',
|
|
61
|
-
component: Screen,
|
|
62
|
-
parameters: { layout: 'fullscreen' },
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export default meta;
|
|
66
|
-
type Story = StoryObj<typeof Screen>;
|
|
67
|
-
|
|
68
|
-
export const Introduction: Story = {};
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
-
|
|
3
|
-
import { ChatInput } from '../components/chat/chat-input';
|
|
4
|
-
import { ChatMessageList } from '../components/chat/chat-message-list';
|
|
5
|
-
import type { ChatAppearance } from '../components/chat/types/chat.types';
|
|
6
|
-
import type { Message } from '../components/chat/types/message.types';
|
|
7
|
-
import { withFaeBrand } from './fae-brand-decorator';
|
|
8
|
-
|
|
9
|
-
const at = (minute: number) => new Date(2026, 8, 29, 14, minute);
|
|
10
|
-
|
|
11
|
-
// The "Admin joins" frame of the fae chat v2 escalation flow: a user request,
|
|
12
|
-
// an escalation offer the user approved, the hand-off and join receipts, then
|
|
13
|
-
// the technician's reply.
|
|
14
|
-
const ESCALATION_THREAD: Message[] = [
|
|
15
|
-
{
|
|
16
|
-
id: 'u1',
|
|
17
|
-
role: 'user',
|
|
18
|
-
name: 'John Smith',
|
|
19
|
-
content: 'My laptop has been running really slowly lately',
|
|
20
|
-
timestamp: at(47),
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
id: 'a1',
|
|
24
|
-
role: 'assistant',
|
|
25
|
-
name: 'Fae',
|
|
26
|
-
assistantType: 'fae',
|
|
27
|
-
content: 'Sorry, I can’t help with this problem',
|
|
28
|
-
timestamp: at(47),
|
|
29
|
-
},
|
|
30
|
-
{ id: 'u2', role: 'user', name: 'John Smith', content: 'call the admin', timestamp: at(47) },
|
|
31
|
-
{
|
|
32
|
-
id: 'a2',
|
|
33
|
-
role: 'assistant',
|
|
34
|
-
name: 'Fae',
|
|
35
|
-
assistantType: 'fae',
|
|
36
|
-
timestamp: at(47),
|
|
37
|
-
content: [
|
|
38
|
-
{
|
|
39
|
-
type: 'escalation_offer',
|
|
40
|
-
status: 'approved',
|
|
41
|
-
resolvedByName: 'User',
|
|
42
|
-
data: {
|
|
43
|
-
offerId: 'offer-1',
|
|
44
|
-
text: 'Having issues with the AI assistant? This ticket can be handed off to a technician. Fae will no longer respond in this conversation. A technician will review the ticket and reply when available.',
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
{ type: 'ticket_escalated', data: { ticketId: 't-1', reason: 'MANUAL' } },
|
|
48
|
-
{
|
|
49
|
-
type: 'error',
|
|
50
|
-
title: 'AI response error',
|
|
51
|
-
details: 'Something went wrong. Please try again.',
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: 's1',
|
|
57
|
-
role: 'user',
|
|
58
|
-
authorType: 'system',
|
|
59
|
-
name: 'Michael Johnson joined the chat',
|
|
60
|
-
content: '',
|
|
61
|
-
timestamp: at(48),
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: 't1',
|
|
65
|
-
role: 'assistant',
|
|
66
|
-
name: 'Michael Johnson',
|
|
67
|
-
authorType: 'admin',
|
|
68
|
-
content: 'I’ll do it faster',
|
|
69
|
-
timestamp: at(48),
|
|
70
|
-
},
|
|
71
|
-
{ id: 'u3', role: 'user', name: 'John Smith', content: 'Cool!', timestamp: at(48) },
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
const APPROVAL_THREAD: Message[] = [
|
|
75
|
-
{
|
|
76
|
-
id: 'u1',
|
|
77
|
-
role: 'user',
|
|
78
|
-
name: 'John Smith',
|
|
79
|
-
content: 'My laptop has been running really slowly lately',
|
|
80
|
-
timestamp: at(47),
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
id: 'a1',
|
|
84
|
-
role: 'assistant',
|
|
85
|
-
name: 'Fae',
|
|
86
|
-
assistantType: 'fae',
|
|
87
|
-
content:
|
|
88
|
-
'I found that your C: drive is 89% full. I can clean up temporary files to free some space - may I proceed with clearing C:/temp?',
|
|
89
|
-
timestamp: at(47),
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: 'a2',
|
|
93
|
-
role: 'assistant',
|
|
94
|
-
name: 'Fae',
|
|
95
|
-
assistantType: 'fae',
|
|
96
|
-
timestamp: at(47),
|
|
97
|
-
content: [
|
|
98
|
-
{
|
|
99
|
-
type: 'approval_request',
|
|
100
|
-
status: 'pending',
|
|
101
|
-
data: {
|
|
102
|
-
requestId: 'req-1',
|
|
103
|
-
command: 'Remove-Item C:\\Temp\\* -Recurse',
|
|
104
|
-
explanation: 'Clears temporary files in C:\\Temp to free disk space.',
|
|
105
|
-
},
|
|
106
|
-
onApprove: () => undefined,
|
|
107
|
-
onReject: () => undefined,
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
},
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
function Thread({
|
|
114
|
-
messages,
|
|
115
|
-
appearance,
|
|
116
|
-
handedOff,
|
|
117
|
-
}: {
|
|
118
|
-
messages: Message[];
|
|
119
|
-
appearance: ChatAppearance;
|
|
120
|
-
handedOff?: boolean;
|
|
121
|
-
}) {
|
|
122
|
-
return (
|
|
123
|
-
<div className="flex h-[720px] w-[728px] flex-col bg-ods-bg">
|
|
124
|
-
<ChatMessageList
|
|
125
|
-
messages={messages}
|
|
126
|
-
assistantType="fae"
|
|
127
|
-
approvalVariant="client"
|
|
128
|
-
appearance={appearance}
|
|
129
|
-
fullWidth
|
|
130
|
-
autoScroll={false}
|
|
131
|
-
/>
|
|
132
|
-
<div className="px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]">
|
|
133
|
-
<ChatInput
|
|
134
|
-
fullWidth
|
|
135
|
-
placeholder="Enter your request here..."
|
|
136
|
-
awaitingResponse={handedOff}
|
|
137
|
-
appearance={appearance}
|
|
138
|
-
awaitingTeam={[
|
|
139
|
-
{ key: '1', name: 'Roman Smith' },
|
|
140
|
-
{ key: '2', name: 'Ada Lin' },
|
|
141
|
-
{ key: '3', name: 'Michael Johnson' },
|
|
142
|
-
]}
|
|
143
|
-
/>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const meta: Meta<typeof Thread> = {
|
|
150
|
-
decorators: [withFaeBrand],
|
|
151
|
-
title: 'Chat/Fae Thread v2',
|
|
152
|
-
component: Thread,
|
|
153
|
-
parameters: { layout: 'centered' },
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
export default meta;
|
|
157
|
-
type Story = StoryObj<typeof Thread>;
|
|
158
|
-
|
|
159
|
-
export const EscalationV2: Story = { args: { messages: ESCALATION_THREAD, appearance: 'v2', handedOff: true } };
|
|
160
|
-
export const EscalationClassic: Story = {
|
|
161
|
-
args: { messages: ESCALATION_THREAD, appearance: 'classic', handedOff: true },
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const approvalCard = (id: string, status: 'approved' | 'rejected' | 'cancelled', resolvedByName: string) => ({
|
|
165
|
-
type: 'approval_request' as const,
|
|
166
|
-
status,
|
|
167
|
-
resolvedByName,
|
|
168
|
-
data: {
|
|
169
|
-
requestId: id,
|
|
170
|
-
command: 'Remove-Item C:\\Temp\\* -Recurse',
|
|
171
|
-
explanation: 'Clears temporary files in C:\\Temp.',
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const APPROVAL_STATES_THREAD: Message[] = [
|
|
176
|
-
{
|
|
177
|
-
id: 'a1',
|
|
178
|
-
role: 'assistant',
|
|
179
|
-
name: 'Fae',
|
|
180
|
-
assistantType: 'fae',
|
|
181
|
-
timestamp: at(47),
|
|
182
|
-
content: [
|
|
183
|
-
approvalCard('r1', 'approved', 'Michael Johnson'),
|
|
184
|
-
approvalCard('r2', 'rejected', 'Michael Johnson'),
|
|
185
|
-
approvalCard('r3', 'cancelled', 'Michael Johnson'),
|
|
186
|
-
],
|
|
187
|
-
},
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
export const ApprovalV2: Story = { args: { messages: APPROVAL_THREAD, appearance: 'v2' } };
|
|
191
|
-
|
|
192
|
-
export const ApprovalStatesV2: Story = { args: { messages: APPROVAL_STATES_THREAD, appearance: 'v2' } };
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { Decorator } from '@storybook/nextjs-vite';
|
|
2
|
-
import { useLayoutEffect, type ReactNode } from 'react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Renders a story in the Fae client's default brand. The chat app sets
|
|
6
|
-
* `data-app-type="flamingo"` on <html> before first paint, which points the
|
|
7
|
-
* accent tokens at flamingo pink; without it Storybook falls back to the
|
|
8
|
-
* OpenFrame yellow and a Fae screen shows two brand colours at once. Restores
|
|
9
|
-
* the previous value so the next story is unaffected.
|
|
10
|
-
*/
|
|
11
|
-
function FaeBrand({ children }: { children: ReactNode }) {
|
|
12
|
-
useLayoutEffect(() => {
|
|
13
|
-
const root = document.documentElement;
|
|
14
|
-
const previous = root.getAttribute('data-app-type');
|
|
15
|
-
root.setAttribute('data-app-type', 'flamingo');
|
|
16
|
-
return () => {
|
|
17
|
-
if (previous === null) root.removeAttribute('data-app-type');
|
|
18
|
-
else root.setAttribute('data-app-type', previous);
|
|
19
|
-
};
|
|
20
|
-
}, []);
|
|
21
|
-
return <>{children}</>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const withFaeBrand: Decorator = Story => (
|
|
25
|
-
<FaeBrand>
|
|
26
|
-
<Story />
|
|
27
|
-
</FaeBrand>
|
|
28
|
-
);
|
|
File without changes
|
|
File without changes
|