@admin-layout/ai-chat-ext-browser 12.2.4-alpha.94 → 12.2.5-alpha.15
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/lib/components/MessageWidgets/MessageFlights.d.ts +47 -0
- package/lib/components/MessageWidgets/MessageFlights.d.ts.map +1 -0
- package/lib/components/MessageWidgets/MessageFlights.js +328 -0
- package/lib/components/MessageWidgets/MessageFlights.js.map +1 -0
- package/lib/components/MessageWidgets/MessageMedia.d.ts +7 -5
- package/lib/components/MessageWidgets/MessageMedia.d.ts.map +1 -1
- package/lib/components/MessageWidgets/MessageMedia.js +86 -54
- package/lib/components/MessageWidgets/MessageMedia.js.map +1 -1
- package/lib/components/MessageWidgets/MessageResults.d.ts +54 -0
- package/lib/components/MessageWidgets/MessageResults.d.ts.map +1 -0
- package/lib/components/MessageWidgets/MessageResults.js +163 -0
- package/lib/components/MessageWidgets/MessageResults.js.map +1 -0
- package/lib/components/MessageWidgets/MessageTravel.d.ts +2 -0
- package/lib/components/MessageWidgets/MessageTravel.d.ts.map +1 -1
- package/lib/components/MessageWidgets/MessageTravel.js +22 -2
- package/lib/components/MessageWidgets/MessageTravel.js.map +1 -1
- package/lib/components/MessageWidgets/ResultCards.d.ts +19 -0
- package/lib/components/MessageWidgets/ResultCards.d.ts.map +1 -0
- package/lib/components/MessageWidgets/ResultCards.js +543 -0
- package/lib/components/MessageWidgets/ResultCards.js.map +1 -0
- package/lib/components/MessageWidgets/ResultRow.d.ts +57 -0
- package/lib/components/MessageWidgets/ResultRow.d.ts.map +1 -0
- package/lib/components/MessageWidgets/ResultRow.js +216 -0
- package/lib/components/MessageWidgets/ResultRow.js.map +1 -0
- package/lib/components/MessageWidgets/index.d.ts +7 -0
- package/lib/components/MessageWidgets/index.d.ts.map +1 -1
- package/lib/components/MessageWidgets/mediaUtils.d.ts +21 -0
- package/lib/components/MessageWidgets/mediaUtils.d.ts.map +1 -0
- package/lib/components/MessageWidgets/mediaUtils.js +76 -0
- package/lib/components/MessageWidgets/mediaUtils.js.map +1 -0
- package/lib/components/index.d.ts +2 -1
- package/lib/components/index.d.ts.map +1 -1
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/interfaces/ai-chat.interface.d.ts +11 -1
- package/lib/interfaces/ai-chat.interface.d.ts.map +1 -1
- package/lib/interfaces/index.d.ts +1 -1
- package/lib/interfaces/index.d.ts.map +1 -1
- package/lib/module.d.ts.map +1 -1
- package/lib/module.js +13 -1
- package/lib/module.js.map +1 -1
- package/lib/slot-fill/chat-settings-fill.js +1 -1
- package/lib/slot-fill/chat-settings-fill.js.map +1 -1
- package/lib/slot-fill/index.d.ts +4 -0
- package/lib/slot-fill/index.d.ts.map +1 -1
- package/lib/slot-fill/message-ai-assistant-fill.js +1 -1
- package/lib/slot-fill/message-ai-assistant-fill.js.map +1 -1
- package/lib/slot-fill/message-codeblock-fill.js +1 -1
- package/lib/slot-fill/message-codeblock-fill.js.map +1 -1
- package/lib/slot-fill/message-connector-fill.js +1 -1
- package/lib/slot-fill/message-connector-fill.js.map +1 -1
- package/lib/slot-fill/message-generic-fill.js +1 -1
- package/lib/slot-fill/message-generic-fill.js.map +1 -1
- package/lib/slot-fill/message-jobs-fill.d.ts +4 -0
- package/lib/slot-fill/message-jobs-fill.d.ts.map +1 -0
- package/lib/slot-fill/message-jobs-fill.js +6 -0
- package/lib/slot-fill/message-jobs-fill.js.map +1 -0
- package/lib/slot-fill/message-media-fill.js +1 -1
- package/lib/slot-fill/message-media-fill.js.map +1 -1
- package/lib/slot-fill/message-movies-fill.d.ts +4 -0
- package/lib/slot-fill/message-movies-fill.d.ts.map +1 -0
- package/lib/slot-fill/message-movies-fill.js +6 -0
- package/lib/slot-fill/message-movies-fill.js.map +1 -0
- package/lib/slot-fill/message-resume-fill.d.ts +4 -0
- package/lib/slot-fill/message-resume-fill.d.ts.map +1 -0
- package/lib/slot-fill/message-resume-fill.js +6 -0
- package/lib/slot-fill/message-resume-fill.js.map +1 -0
- package/lib/slot-fill/message-service-fill.js +1 -1
- package/lib/slot-fill/message-service-fill.js.map +1 -1
- package/lib/slot-fill/message-shopping-fill.d.ts +4 -0
- package/lib/slot-fill/message-shopping-fill.d.ts.map +1 -0
- package/lib/slot-fill/message-shopping-fill.js +6 -0
- package/lib/slot-fill/message-shopping-fill.js.map +1 -0
- package/lib/slot-fill/message-thinking-fill.js +1 -1
- package/lib/slot-fill/message-thinking-fill.js.map +1 -1
- package/lib/slot-fill/message-tooluse-fill.js +1 -1
- package/lib/slot-fill/message-tooluse-fill.js.map +1 -1
- package/lib/slot-fill/message-travel-fill.js +1 -1
- package/lib/slot-fill/message-travel-fill.js.map +1 -1
- package/lib/slot-fill/message-workflow-fill.js +1 -1
- package/lib/slot-fill/message-workflow-fill.js.map +1 -1
- package/lib/utils/parseWidgetPost.d.ts.map +1 -1
- package/lib/utils/parseWidgetPost.js +1 -1
- package/lib/utils/parseWidgetPost.js.map +1 -1
- package/package.json +7 -5
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IPost } from 'common';
|
|
3
|
+
import { TravelAttachmentsInterface } from '../../interfaces';
|
|
4
|
+
import { FlightCardView, FlightSliceView, FlightSort } from '@admin-layout/client';
|
|
5
|
+
export interface IMessageFlightsProps {
|
|
6
|
+
message: IPost;
|
|
7
|
+
attachment: TravelAttachmentsInterface;
|
|
8
|
+
/** Opens the full result set in the travel side panel. */
|
|
9
|
+
onMessageClick?: (ref: IPost) => void;
|
|
10
|
+
}
|
|
11
|
+
/** Soft monogram tile colours, indexed by `airlineTone`; shared by every result card. */
|
|
12
|
+
export declare const TONES: string[];
|
|
13
|
+
/**
|
|
14
|
+
* The route between the two times: duration above, a line with one amber dot
|
|
15
|
+
* per stop and the plane at the arrival end, stops below.
|
|
16
|
+
*/
|
|
17
|
+
export declare const RouteLine: ({ slice }: {
|
|
18
|
+
slice: FlightSliceView;
|
|
19
|
+
}) => React.JSX.Element;
|
|
20
|
+
export interface IFlightCardProps {
|
|
21
|
+
card: FlightCardView;
|
|
22
|
+
onOpen?: () => void;
|
|
23
|
+
fullWidth?: boolean;
|
|
24
|
+
/** Show the departure date on the card (offers on different dates, or a round trip). */
|
|
25
|
+
showDate?: boolean;
|
|
26
|
+
/** Briefly outlined, e.g. after a quick-pick chip jumped to it. */
|
|
27
|
+
highlighted?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare const FlightCard: ({ card, onOpen, fullWidth, showDate, highlighted, }: IFlightCardProps) => React.JSX.Element;
|
|
30
|
+
export interface IFlightCardListProps {
|
|
31
|
+
attachment: TravelAttachmentsInterface;
|
|
32
|
+
/** Fired by a card with no seller link; omit it to render such cards as plain content. */
|
|
33
|
+
onOpen?: () => void;
|
|
34
|
+
/** Ordering; `recommended` (default) keeps the agent's order. */
|
|
35
|
+
sort?: FlightSort;
|
|
36
|
+
}
|
|
37
|
+
/** Every flight as a full-width vertical list, for a side panel or a results page. */
|
|
38
|
+
export declare const FlightCardList: ({ attachment, onOpen, sort }: IFlightCardListProps) => React.JSX.Element;
|
|
39
|
+
/**
|
|
40
|
+
* Flight results as a swipeable row of cards: one card per offer, the next
|
|
41
|
+
* one peeking in so the row reads as scrollable. Above it, the route and date
|
|
42
|
+
* once, and quick picks (cheapest, fastest, best value) that jump to their
|
|
43
|
+
* card. Touch swipes and snaps; a pointer gets arrow buttons, since sideways
|
|
44
|
+
* scrolling is awkward with a mouse.
|
|
45
|
+
*/
|
|
46
|
+
export declare const MessageFlights: ({ message, attachment, onMessageClick }: IMessageFlightsProps) => React.JSX.Element;
|
|
47
|
+
//# sourceMappingURL=MessageFlights.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MessageFlights.d.ts","sourceRoot":"","sources":["../../../src/components/MessageWidgets/MessageFlights.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAKjF,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC/B,OAAO,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAEH,cAAc,EACd,eAAe,EACf,UAAU,EAIb,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,KAAK,CAAC;IACf,UAAU,EAAE,0BAA0B,CAAC;IACvC,0DAA0D;IAC1D,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CACzC;AAED,yFAAyF;AACzF,eAAO,MAAM,KAAK,UASjB,CAAC;AA4BF;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,WAAW;IAAE,KAAK,EAAE,eAAe,CAAA;CAAE,sBAiC9D,CAAC;AAwCF,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,UAAU,GAAI,qDAMxB,gBAAgB,sBAmHlB,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACjC,UAAU,EAAE,0BAA0B,CAAC;IACvC,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,iEAAiE;IACjE,IAAI,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,sFAAsF;AACtF,eAAO,MAAM,cAAc,GAAI,8BAA8C,oBAAoB,sBAchG,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,yCAAyC,oBAAoB,sBA8J3F,CAAC"}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import React,{useMemo,useRef,useState,useCallback,useEffect}from'react';import {FaPlane}from'@react-icons/all-files/fa/FaPlane.js';import {FiChevronLeft}from'@react-icons/all-files/fi/FiChevronLeft.js';import {FiChevronRight}from'@react-icons/all-files/fi/FiChevronRight.js';import {FiExternalLink}from'@react-icons/all-files/fi/FiExternalLink.js';import {toFlightCards,flightResultSummary,sortFlightCards}from'@admin-layout/client';/** Soft monogram tile colours, indexed by `airlineTone`; shared by every result card. */
|
|
2
|
+
const TONES = ['bg-sky-100 text-sky-700 dark:bg-sky-500/20 dark:text-sky-300', 'bg-violet-100 text-violet-700 dark:bg-violet-500/20 dark:text-violet-300', 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300', 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300', 'bg-rose-100 text-rose-700 dark:bg-rose-500/20 dark:text-rose-300', 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300', 'bg-teal-100 text-teal-700 dark:bg-teal-500/20 dark:text-teal-300', 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300'];
|
|
3
|
+
const BADGE = {
|
|
4
|
+
cheapest: {
|
|
5
|
+
label: 'Cheapest',
|
|
6
|
+
pill: 'bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-300 dark:ring-emerald-500/30',
|
|
7
|
+
text: 'text-emerald-600 dark:text-emerald-400'
|
|
8
|
+
},
|
|
9
|
+
fastest: {
|
|
10
|
+
label: 'Fastest',
|
|
11
|
+
pill: 'bg-violet-50 text-violet-700 ring-violet-200 dark:bg-violet-500/15 dark:text-violet-300 dark:ring-violet-500/30',
|
|
12
|
+
text: 'text-violet-600 dark:text-violet-400'
|
|
13
|
+
},
|
|
14
|
+
best: {
|
|
15
|
+
label: 'Best value',
|
|
16
|
+
pill: 'bg-blue-50 text-blue-700 ring-blue-200 dark:bg-blue-500/15 dark:text-blue-300 dark:ring-blue-500/30',
|
|
17
|
+
text: 'text-blue-600 dark:text-blue-400'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const BadgePill = ({
|
|
21
|
+
badge
|
|
22
|
+
}) => React.createElement("span", {
|
|
23
|
+
className: `inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-[11px] font-semibold leading-4 ring-1 ring-inset ${BADGE[badge].pill}`
|
|
24
|
+
}, BADGE[badge].label);
|
|
25
|
+
/**
|
|
26
|
+
* The route between the two times: duration above, a line with one amber dot
|
|
27
|
+
* per stop and the plane at the arrival end, stops below.
|
|
28
|
+
*/
|
|
29
|
+
const RouteLine = ({
|
|
30
|
+
slice
|
|
31
|
+
}) => {
|
|
32
|
+
const stops = slice.stopCount ?? 0;
|
|
33
|
+
return React.createElement("div", {
|
|
34
|
+
className: "flex min-w-0 flex-col items-center self-center px-1",
|
|
35
|
+
"aria-hidden": "true"
|
|
36
|
+
}, React.createElement("p", {
|
|
37
|
+
className: "text-[11px] font-medium leading-4 text-gray-500 dark:text-gray-400"
|
|
38
|
+
}, slice.duration ?? ' '), React.createElement("div", {
|
|
39
|
+
className: "my-1 flex w-full items-center"
|
|
40
|
+
}, React.createElement("span", {
|
|
41
|
+
className: "h-1.5 w-1.5 shrink-0 rounded-full border border-gray-400 bg-white dark:border-gray-500 dark:bg-gray-800"
|
|
42
|
+
}), React.createElement("span", {
|
|
43
|
+
className: "relative h-px flex-1 bg-gray-300 dark:bg-gray-600"
|
|
44
|
+
}, Array.from({
|
|
45
|
+
length: stops
|
|
46
|
+
}, (_, i) => React.createElement("span", {
|
|
47
|
+
key: i,
|
|
48
|
+
className: "absolute top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-400 ring-2 ring-white dark:ring-gray-800",
|
|
49
|
+
style: {
|
|
50
|
+
left: `${(i + 1) / (stops + 1) * 100}%`
|
|
51
|
+
}
|
|
52
|
+
}))), React.createElement(FaPlane, {
|
|
53
|
+
className: "ml-0.5 h-3 w-3 shrink-0 text-blue-500 dark:text-blue-400"
|
|
54
|
+
})), React.createElement("p", {
|
|
55
|
+
className: `text-[11px] font-medium leading-4 ${stops === 0 && slice.stops ? 'text-emerald-600 dark:text-emerald-400' : stops > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-400'}`
|
|
56
|
+
}, slice.stops ?? ' '));
|
|
57
|
+
};
|
|
58
|
+
const SliceRow = ({
|
|
59
|
+
slice,
|
|
60
|
+
caption
|
|
61
|
+
}) => React.createElement("div", null, caption ? React.createElement("p", {
|
|
62
|
+
className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500"
|
|
63
|
+
}, caption) : null, React.createElement("div", {
|
|
64
|
+
className: "grid grid-cols-[auto_1fr_auto] items-center gap-2"
|
|
65
|
+
}, React.createElement("div", null, React.createElement("p", {
|
|
66
|
+
className: "text-[22px] font-bold leading-none tracking-tight tabular-nums text-gray-900 dark:text-gray-50"
|
|
67
|
+
}, slice.departTime ?? '--:--'), React.createElement("p", {
|
|
68
|
+
className: "mt-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
69
|
+
}, slice.origin)), React.createElement(RouteLine, {
|
|
70
|
+
slice: slice
|
|
71
|
+
}), React.createElement("div", {
|
|
72
|
+
className: "text-right"
|
|
73
|
+
}, React.createElement("p", {
|
|
74
|
+
className: "text-[22px] font-bold leading-none tracking-tight tabular-nums text-gray-900 dark:text-gray-50"
|
|
75
|
+
}, slice.arriveTime ?? '--:--', slice.dayOffset ? React.createElement("sup", {
|
|
76
|
+
className: "ml-0.5 text-[11px] font-semibold text-amber-600 dark:text-amber-400"
|
|
77
|
+
}, slice.dayOffset) : null), React.createElement("p", {
|
|
78
|
+
className: "mt-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
79
|
+
}, slice.destination))));
|
|
80
|
+
const CARD_CLASS = 'group/card relative flex flex-col rounded-2xl border bg-white p-4 text-left shadow-sm transition-all duration-200 ' + 'hover:-translate-y-0.5 hover:border-blue-300 hover:shadow-lg hover:shadow-blue-500/10 ' + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ' + 'dark:bg-gray-800 dark:hover:border-blue-500/60 dark:hover:shadow-black/30 dark:focus-visible:ring-offset-gray-900';
|
|
81
|
+
const IN_ROW = 'w-[min(20rem,85%)] shrink-0 snap-start';
|
|
82
|
+
const FlightCard = ({
|
|
83
|
+
card,
|
|
84
|
+
onOpen,
|
|
85
|
+
fullWidth = false,
|
|
86
|
+
showDate = false,
|
|
87
|
+
highlighted = false
|
|
88
|
+
}) => {
|
|
89
|
+
const className = `${CARD_CLASS} ${fullWidth ? 'w-full' : IN_ROW} ${highlighted ? 'border-blue-400 ring-2 ring-blue-500/40 dark:border-blue-400' : 'border-gray-200 dark:border-gray-700'}`;
|
|
90
|
+
const subtitle = [card.flightNumber, card.cabin].filter(Boolean).join(' · ') || (card.roundTrip ? 'Round trip' : 'One way');
|
|
91
|
+
const captionFor = (s, i) => {
|
|
92
|
+
if (card.roundTrip) return [i === 0 ? 'Outbound' : 'Return', s.date].filter(Boolean).join(' · ');
|
|
93
|
+
return showDate ? s.date : undefined;
|
|
94
|
+
};
|
|
95
|
+
const body = React.createElement(React.Fragment, null, React.createElement("div", {
|
|
96
|
+
className: "flex items-center gap-2.5"
|
|
97
|
+
}, React.createElement("span", {
|
|
98
|
+
"aria-hidden": "true",
|
|
99
|
+
className: `flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-[11px] font-bold tracking-wide ${TONES[card.airlineTone % TONES.length]}`
|
|
100
|
+
}, card.airlineCode), React.createElement("div", {
|
|
101
|
+
className: "min-w-0 flex-1"
|
|
102
|
+
}, React.createElement("p", {
|
|
103
|
+
className: "truncate text-sm font-semibold leading-5 text-gray-900 dark:text-gray-50"
|
|
104
|
+
}, card.airline ?? card.footer), React.createElement("p", {
|
|
105
|
+
className: "truncate text-xs leading-4 text-gray-500 dark:text-gray-400"
|
|
106
|
+
}, subtitle)), card.badges[0] ? React.createElement(BadgePill, {
|
|
107
|
+
badge: card.badges[0]
|
|
108
|
+
}) : null), React.createElement("div", {
|
|
109
|
+
className: "my-4 space-y-4"
|
|
110
|
+
}, card.slices.map((s, i) => React.createElement(SliceRow, {
|
|
111
|
+
key: i,
|
|
112
|
+
slice: s,
|
|
113
|
+
caption: captionFor(s, i)
|
|
114
|
+
}))), card.fareLine ? React.createElement("p", {
|
|
115
|
+
className: "-mt-1 mb-3 line-clamp-2 text-[11px] leading-4 text-gray-500 dark:text-gray-400",
|
|
116
|
+
"data-fare-line": ""
|
|
117
|
+
}, card.fareLine.split(' · ').map((part, i, parts) => React.createElement("span", {
|
|
118
|
+
key: i
|
|
119
|
+
}, React.createElement("span", {
|
|
120
|
+
className: "whitespace-nowrap"
|
|
121
|
+
}, part, i < parts.length - 1 ? ' ·' : ''), i < parts.length - 1 ? ' ' : ''))) : null, React.createElement("div", {
|
|
122
|
+
className: "mt-auto flex items-center justify-between gap-3 border-t border-gray-100 pt-3 dark:border-gray-700"
|
|
123
|
+
}, React.createElement("p", {
|
|
124
|
+
className: "min-w-0 truncate text-xs text-gray-500 dark:text-gray-400"
|
|
125
|
+
}, card.seller ? `via ${card.seller}` : card.roundTrip ? 'Round trip' : card.slices[0].stops ?? ''), React.createElement("span", {
|
|
126
|
+
className: "flex shrink-0 items-center gap-1.5"
|
|
127
|
+
}, card.price ? React.createElement("span", {
|
|
128
|
+
className: "text-lg font-bold leading-none tabular-nums text-gray-900 dark:text-white"
|
|
129
|
+
}, card.price) : null, card.url ? React.createElement(FiExternalLink, {
|
|
130
|
+
className: "h-3.5 w-3.5 text-gray-400 transition-colors group-hover/card:text-blue-500"
|
|
131
|
+
}) : null)));
|
|
132
|
+
// A bookable offer goes straight to the seller; without a link the card
|
|
133
|
+
// opens the full results in the side panel instead.
|
|
134
|
+
if (card.url) {
|
|
135
|
+
return React.createElement("a", {
|
|
136
|
+
href: card.url,
|
|
137
|
+
target: "_blank",
|
|
138
|
+
rel: "noopener noreferrer",
|
|
139
|
+
"aria-label": card.accessibleLabel,
|
|
140
|
+
className: className,
|
|
141
|
+
"data-flight-card": "",
|
|
142
|
+
"data-flight-key": card.key
|
|
143
|
+
}, body);
|
|
144
|
+
}
|
|
145
|
+
if (!onOpen) {
|
|
146
|
+
// Nothing to open: plain content, not a button that does nothing.
|
|
147
|
+
return React.createElement("div", {
|
|
148
|
+
role: "group",
|
|
149
|
+
"aria-label": card.accessibleLabel,
|
|
150
|
+
className: className,
|
|
151
|
+
"data-flight-card": "",
|
|
152
|
+
"data-flight-key": card.key
|
|
153
|
+
}, body);
|
|
154
|
+
}
|
|
155
|
+
return React.createElement("button", {
|
|
156
|
+
type: "button",
|
|
157
|
+
onClick: onOpen,
|
|
158
|
+
"aria-label": card.accessibleLabel,
|
|
159
|
+
className: className,
|
|
160
|
+
"data-flight-card": "",
|
|
161
|
+
"data-flight-key": card.key
|
|
162
|
+
}, body);
|
|
163
|
+
};
|
|
164
|
+
/** Every flight as a full-width vertical list, for a side panel or a results page. */
|
|
165
|
+
const FlightCardList = ({
|
|
166
|
+
attachment,
|
|
167
|
+
onOpen,
|
|
168
|
+
sort = 'recommended'
|
|
169
|
+
}) => {
|
|
170
|
+
const cards = useMemo(() => toFlightCards(attachment), [attachment]);
|
|
171
|
+
const summary = useMemo(() => flightResultSummary(attachment, cards), [attachment, cards]);
|
|
172
|
+
const ordered = useMemo(() => sortFlightCards(cards, sort), [cards, sort]);
|
|
173
|
+
if (cards.length === 0) return null;
|
|
174
|
+
return React.createElement("ul", {
|
|
175
|
+
className: "space-y-3",
|
|
176
|
+
"aria-label": attachment.title || 'Flight options'
|
|
177
|
+
}, ordered.map(c => React.createElement("li", {
|
|
178
|
+
key: c.key
|
|
179
|
+
}, React.createElement(FlightCard, {
|
|
180
|
+
card: c,
|
|
181
|
+
onOpen: onOpen,
|
|
182
|
+
fullWidth: true,
|
|
183
|
+
showDate: !summary.sameDate
|
|
184
|
+
}))));
|
|
185
|
+
};
|
|
186
|
+
const HIGHLIGHT_MS = 1600;
|
|
187
|
+
/**
|
|
188
|
+
* Flight results as a swipeable row of cards: one card per offer, the next
|
|
189
|
+
* one peeking in so the row reads as scrollable. Above it, the route and date
|
|
190
|
+
* once, and quick picks (cheapest, fastest, best value) that jump to their
|
|
191
|
+
* card. Touch swipes and snaps; a pointer gets arrow buttons, since sideways
|
|
192
|
+
* scrolling is awkward with a mouse.
|
|
193
|
+
*/
|
|
194
|
+
const MessageFlights = ({
|
|
195
|
+
message,
|
|
196
|
+
attachment,
|
|
197
|
+
onMessageClick
|
|
198
|
+
}) => {
|
|
199
|
+
const cards = useMemo(() => toFlightCards(attachment), [attachment]);
|
|
200
|
+
const summary = useMemo(() => flightResultSummary(attachment, cards), [attachment, cards]);
|
|
201
|
+
const rowRef = useRef(null);
|
|
202
|
+
const [edges, setEdges] = useState({
|
|
203
|
+
atStart: true,
|
|
204
|
+
atEnd: true
|
|
205
|
+
});
|
|
206
|
+
const [highlighted, setHighlighted] = useState(null);
|
|
207
|
+
const measure = useCallback(() => {
|
|
208
|
+
const el = rowRef.current;
|
|
209
|
+
if (!el) return;
|
|
210
|
+
setEdges({
|
|
211
|
+
atStart: el.scrollLeft <= 1,
|
|
212
|
+
atEnd: el.scrollLeft + el.clientWidth >= el.scrollWidth - 1
|
|
213
|
+
});
|
|
214
|
+
}, []);
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
measure();
|
|
217
|
+
const el = rowRef.current;
|
|
218
|
+
if (!el || typeof ResizeObserver === 'undefined') return undefined;
|
|
219
|
+
const ro = new ResizeObserver(measure);
|
|
220
|
+
ro.observe(el);
|
|
221
|
+
return () => ro.disconnect();
|
|
222
|
+
}, [measure, cards.length]);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!highlighted) return undefined;
|
|
225
|
+
const t = setTimeout(() => setHighlighted(null), HIGHLIGHT_MS);
|
|
226
|
+
return () => clearTimeout(t);
|
|
227
|
+
}, [highlighted]);
|
|
228
|
+
if (cards.length === 0) return null;
|
|
229
|
+
const step = dir => {
|
|
230
|
+
const el = rowRef.current;
|
|
231
|
+
const card = el?.querySelector('[data-flight-card]');
|
|
232
|
+
if (!el || !card) return;
|
|
233
|
+
el.scrollBy({
|
|
234
|
+
left: dir * (card.offsetWidth + 12),
|
|
235
|
+
behavior: 'smooth'
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
const jumpTo = key => {
|
|
239
|
+
const el = rowRef.current;
|
|
240
|
+
const card = el?.querySelector(`[data-flight-key="${CSS.escape(key)}"]`);
|
|
241
|
+
if (!el || !card) return;
|
|
242
|
+
// 8px: the row's px-2, so the card lands where the first one sits.
|
|
243
|
+
el.scrollTo({
|
|
244
|
+
left: card.offsetLeft - 8,
|
|
245
|
+
behavior: 'smooth'
|
|
246
|
+
});
|
|
247
|
+
setHighlighted(key);
|
|
248
|
+
};
|
|
249
|
+
const open = () => onMessageClick?.(message);
|
|
250
|
+
const count = summary.count;
|
|
251
|
+
const title = summary.route ?? attachment.title ?? 'Flights';
|
|
252
|
+
const subtitle = [summary.date, `${count} ${count === 1 ? 'flight' : 'flights'}`, summary.fromPrice ? `from ${summary.fromPrice}` : undefined, summary.seller].filter(Boolean).join(' · ');
|
|
253
|
+
const picks = cards.length > 2 ? summary.picks : [];
|
|
254
|
+
// Arrows sit over the first and last visible card, so they show on hover or
|
|
255
|
+
// keyboard focus only; the next card peeking in already says the row scrolls.
|
|
256
|
+
const arrow = 'absolute top-1/2 z-10 hidden h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full border border-gray-200 bg-white/95 text-gray-700 shadow-md transition hover:bg-white ' + 'opacity-0 group-hover/row:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none disabled:!opacity-0 [@media(pointer:fine)]:flex ' + 'dark:border-gray-600 dark:bg-gray-800/95 dark:text-gray-200';
|
|
257
|
+
return React.createElement("div", {
|
|
258
|
+
className: "my-2 w-full",
|
|
259
|
+
role: "group",
|
|
260
|
+
"aria-label": attachment.title || 'Flight options'
|
|
261
|
+
}, React.createElement("div", {
|
|
262
|
+
className: "mx-2 mb-2 flex items-center justify-between gap-3"
|
|
263
|
+
}, React.createElement("div", {
|
|
264
|
+
className: "flex min-w-0 items-center gap-2.5"
|
|
265
|
+
}, React.createElement("span", {
|
|
266
|
+
"aria-hidden": "true",
|
|
267
|
+
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-blue-50 text-blue-600 dark:bg-blue-500/15 dark:text-blue-300"
|
|
268
|
+
}, React.createElement(FaPlane, {
|
|
269
|
+
className: "h-4 w-4"
|
|
270
|
+
})), React.createElement("div", {
|
|
271
|
+
className: "min-w-0"
|
|
272
|
+
}, React.createElement("p", {
|
|
273
|
+
className: "truncate text-sm font-semibold leading-5 text-gray-900 dark:text-gray-50"
|
|
274
|
+
}, title), React.createElement("p", {
|
|
275
|
+
className: "truncate text-xs leading-4 text-gray-500 dark:text-gray-400"
|
|
276
|
+
}, subtitle))), onMessageClick && count > 1 ? React.createElement("button", {
|
|
277
|
+
type: "button",
|
|
278
|
+
onClick: open,
|
|
279
|
+
className: "inline-flex shrink-0 items-center gap-0.5 rounded-full px-2.5 py-1 text-xs font-semibold text-blue-600 transition hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-500/10"
|
|
280
|
+
}, "View all ", count, React.createElement(FiChevronRight, {
|
|
281
|
+
className: "h-3.5 w-3.5"
|
|
282
|
+
})) : null), picks.length > 0 ? React.createElement("div", {
|
|
283
|
+
className: "mx-2 mb-2 flex flex-wrap gap-1.5",
|
|
284
|
+
"aria-label": "Quick picks"
|
|
285
|
+
}, picks.map(p => React.createElement("button", {
|
|
286
|
+
key: p.badge,
|
|
287
|
+
type: "button",
|
|
288
|
+
onClick: () => jumpTo(p.key),
|
|
289
|
+
className: "inline-flex items-center gap-1.5 rounded-full border border-gray-200 bg-white px-2.5 py-1 text-xs transition hover:border-blue-300 hover:bg-blue-50/60 dark:border-gray-700 dark:bg-gray-800 dark:hover:border-blue-500/60 dark:hover:bg-blue-500/10"
|
|
290
|
+
}, React.createElement("span", {
|
|
291
|
+
className: `font-semibold ${BADGE[p.badge].text}`
|
|
292
|
+
}, p.label), React.createElement("span", {
|
|
293
|
+
className: "font-semibold tabular-nums text-gray-900 dark:text-gray-50"
|
|
294
|
+
}, p.badge === 'fastest' ? p.duration ?? p.price : p.price), p.airline ? React.createElement("span", {
|
|
295
|
+
className: "truncate text-gray-500 dark:text-gray-400"
|
|
296
|
+
}, "\u00B7 ", p.airline) : null))) : null, React.createElement("div", {
|
|
297
|
+
className: "group/row relative"
|
|
298
|
+
}, React.createElement("div", {
|
|
299
|
+
ref: rowRef,
|
|
300
|
+
onScroll: measure,
|
|
301
|
+
// scroll-px-2 matches px-2: without it a snapped card sits flush
|
|
302
|
+
// against the edge while the first sits 8px in, and snapping
|
|
303
|
+
// nudges the row off 0 on load so "previous" looks enabled.
|
|
304
|
+
className: "flex snap-x snap-mandatory scroll-px-2 gap-3 overflow-x-auto scroll-smooth px-2 pb-2 pt-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
305
|
+
}, cards.map(c => React.createElement(FlightCard, {
|
|
306
|
+
key: c.key,
|
|
307
|
+
card: c,
|
|
308
|
+
onOpen: open,
|
|
309
|
+
showDate: !summary.sameDate,
|
|
310
|
+
highlighted: highlighted === c.key
|
|
311
|
+
}))), React.createElement("button", {
|
|
312
|
+
type: "button",
|
|
313
|
+
"aria-label": "Previous flights",
|
|
314
|
+
onClick: () => step(-1),
|
|
315
|
+
disabled: edges.atStart,
|
|
316
|
+
className: `${arrow} left-0`
|
|
317
|
+
}, React.createElement(FiChevronLeft, {
|
|
318
|
+
className: "h-5 w-5"
|
|
319
|
+
})), React.createElement("button", {
|
|
320
|
+
type: "button",
|
|
321
|
+
"aria-label": "More flights",
|
|
322
|
+
onClick: () => step(1),
|
|
323
|
+
disabled: edges.atEnd,
|
|
324
|
+
className: `${arrow} right-0`
|
|
325
|
+
}, React.createElement(FiChevronRight, {
|
|
326
|
+
className: "h-5 w-5"
|
|
327
|
+
}))));
|
|
328
|
+
};export{FlightCard,FlightCardList,MessageFlights,RouteLine,TONES};//# sourceMappingURL=MessageFlights.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MessageFlights.js","sources":["../../../src/components/MessageWidgets/MessageFlights.tsx"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -7,11 +7,13 @@ export interface IMessageMediaProps {
|
|
|
7
7
|
onMessageClick?: (ref: IPost) => void;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Media card — renders the image / video / audio a cdecli generation
|
|
11
|
-
* (yantra-image-generation, yantra-video-generation, audio / TTS, …)
|
|
12
|
-
* inline in the chat. Shows the asset(s)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Media result card — renders the image / video / audio a cdecli generation
|
|
11
|
+
* connector (yantra-image-generation, yantra-video-generation, audio / TTS, …)
|
|
12
|
+
* returned, inline in the chat. Shows the asset(s) in a real player, a technical
|
|
13
|
+
* metadata row (kind · dimensions · duration · format · model) derived from the
|
|
14
|
+
* loaded asset, the prompt as a caption, and open / download / copy-link actions.
|
|
15
|
+
* The asset URLs live on the message's propsConfiguration, so they persist with
|
|
16
|
+
* the chat.
|
|
15
17
|
*/
|
|
16
18
|
export declare const MessageMedia: (props: IMessageMediaProps) => React.JSX.Element;
|
|
17
19
|
//# sourceMappingURL=MessageMedia.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageMedia.d.ts","sourceRoot":"","sources":["../../../src/components/MessageWidgets/MessageMedia.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"MessageMedia.d.ts","sourceRoot":"","sources":["../../../src/components/MessageWidgets/MessageMedia.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAQxC,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAa/B,MAAM,WAAW,kBAAkB;IAC/B,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CACzC;AAaD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,kBAAkB,sBAyIrD,CAAC"}
|
|
@@ -1,29 +1,28 @@
|
|
|
1
|
-
import React
|
|
2
|
-
image: {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
},
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}),
|
|
12
|
-
label: 'Video'
|
|
13
|
-
},
|
|
14
|
-
audio: {
|
|
15
|
-
icon: React.createElement(FiMusic, {
|
|
16
|
-
className: "w-4 h-4"
|
|
17
|
-
}),
|
|
18
|
-
label: 'Audio'
|
|
19
|
-
}
|
|
1
|
+
import React,{useState}from'react';import {FiImage}from'@react-icons/all-files/fi/FiImage.js';import {FiVideo}from'@react-icons/all-files/fi/FiVideo.js';import {FiMusic}from'@react-icons/all-files/fi/FiMusic.js';import {FiDownload}from'@react-icons/all-files/fi/FiDownload.js';import {FiExternalLink}from'@react-icons/all-files/fi/FiExternalLink.js';import {FiCopy}from'@react-icons/all-files/fi/FiCopy.js';import {FiCheck}from'@react-icons/all-files/fi/FiCheck.js';import {MediaAsset}from'@admin-layout/connector-widgets-browser';import {deriveMediaItems,MEDIA_KIND_LABEL,formatAttribution,formatDimensions,formatDuration,formatMediaFormat}from'./mediaUtils.js';const KIND_ICON = {
|
|
2
|
+
image: React.createElement(FiImage, {
|
|
3
|
+
className: "w-3.5 h-3.5"
|
|
4
|
+
}),
|
|
5
|
+
video: React.createElement(FiVideo, {
|
|
6
|
+
className: "w-3.5 h-3.5"
|
|
7
|
+
}),
|
|
8
|
+
audio: React.createElement(FiMusic, {
|
|
9
|
+
className: "w-3.5 h-3.5"
|
|
10
|
+
})
|
|
20
11
|
};
|
|
12
|
+
/** One compact `·`-separated metadata cell. */
|
|
13
|
+
const MetaCell = ({
|
|
14
|
+
children
|
|
15
|
+
}) => React.createElement("span", {
|
|
16
|
+
className: "inline-flex items-center whitespace-nowrap"
|
|
17
|
+
}, children);
|
|
21
18
|
/**
|
|
22
|
-
* Media card — renders the image / video / audio a cdecli generation
|
|
23
|
-
* (yantra-image-generation, yantra-video-generation, audio / TTS, …)
|
|
24
|
-
* inline in the chat. Shows the asset(s)
|
|
25
|
-
*
|
|
26
|
-
*
|
|
19
|
+
* Media result card — renders the image / video / audio a cdecli generation
|
|
20
|
+
* connector (yantra-image-generation, yantra-video-generation, audio / TTS, …)
|
|
21
|
+
* returned, inline in the chat. Shows the asset(s) in a real player, a technical
|
|
22
|
+
* metadata row (kind · dimensions · duration · format · model) derived from the
|
|
23
|
+
* loaded asset, the prompt as a caption, and open / download / copy-link actions.
|
|
24
|
+
* The asset URLs live on the message's propsConfiguration, so they persist with
|
|
25
|
+
* the chat.
|
|
27
26
|
*/
|
|
28
27
|
const MessageMedia = props => {
|
|
29
28
|
const {
|
|
@@ -34,58 +33,91 @@ const MessageMedia = props => {
|
|
|
34
33
|
const {
|
|
35
34
|
attachment
|
|
36
35
|
} = message.propsConfiguration?.contents ?? {};
|
|
36
|
+
const [assetMeta, setAssetMeta] = useState({});
|
|
37
|
+
const [copied, setCopied] = useState(false);
|
|
37
38
|
if (!attachment || !attachment.url) return null;
|
|
38
|
-
const items = attachment
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
39
|
+
const items = deriveMediaItems(attachment);
|
|
40
|
+
if (items.length === 0) return null;
|
|
41
|
+
const kind = attachment.kind;
|
|
42
|
+
const kindLabel = MEDIA_KIND_LABEL[kind] ?? MEDIA_KIND_LABEL.image;
|
|
43
|
+
const kindIcon = KIND_ICON[kind] ?? KIND_ICON.image;
|
|
44
|
+
const title = attachment.title || `${kindLabel} generated`;
|
|
45
|
+
const attribution = formatAttribution(attachment);
|
|
46
|
+
const isImageGrid = items.length > 1 && kind === 'image';
|
|
47
|
+
// Technical metadata — dimensions/duration come from the loaded asset (single
|
|
48
|
+
// asset only; a grid has no one set of dimensions), format from the mime type.
|
|
49
|
+
const dimensions = !isImageGrid ? formatDimensions(assetMeta.width, assetMeta.height) : undefined;
|
|
50
|
+
const duration = !isImageGrid ? formatDuration(assetMeta.duration) : undefined;
|
|
51
|
+
const format = formatMediaFormat(attachment.mimeType, attachment.fileName);
|
|
52
|
+
const metaCells = [isImageGrid ? `${items.length} images` : undefined, dimensions, duration, format, attribution].filter(Boolean);
|
|
49
53
|
const open = () => onMessageClick?.(message);
|
|
54
|
+
const copyLink = async () => {
|
|
55
|
+
try {
|
|
56
|
+
await navigator.clipboard?.writeText(attachment.url);
|
|
57
|
+
setCopied(true);
|
|
58
|
+
window.setTimeout(() => setCopied(false), 1600);
|
|
59
|
+
} catch {
|
|
60
|
+
/* clipboard unavailable — no-op */
|
|
61
|
+
}
|
|
62
|
+
};
|
|
50
63
|
return React.createElement("div", {
|
|
51
64
|
key: `${index}`,
|
|
52
|
-
className: "my-2 mx-2 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/60 overflow-hidden"
|
|
65
|
+
className: "my-2 mx-2 w-[calc(100%-1rem)] rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/60 overflow-hidden"
|
|
53
66
|
}, React.createElement("div", {
|
|
54
|
-
className: "flex items-center gap-2 px-3 py-2 border-b border-gray-100 dark:border-gray-700"
|
|
67
|
+
className: "flex items-center gap-2 px-3 py-2.5 border-b border-gray-100 dark:border-gray-700/80"
|
|
55
68
|
}, React.createElement("span", {
|
|
56
|
-
className: "flex-shrink-0 text-blue-
|
|
57
|
-
},
|
|
58
|
-
className: "flex-1 min-w-0 text-sm font-semibold text-gray-900 dark:text-gray-100 truncate"
|
|
69
|
+
className: "flex-shrink-0 inline-flex items-center gap-1 rounded-md bg-blue-50 dark:bg-gray-700 px-1.5 py-0.5 text-[11px] font-medium text-blue-600 dark:text-blue-400"
|
|
70
|
+
}, kindIcon, kindLabel), React.createElement("p", {
|
|
71
|
+
className: "flex-1 min-w-0 text-sm font-semibold text-gray-900 dark:text-gray-100 truncate",
|
|
72
|
+
title: title
|
|
59
73
|
}, title), attribution && React.createElement("span", {
|
|
60
|
-
className: "flex-shrink-0 text-[
|
|
74
|
+
className: "flex-shrink-0 text-[11px] text-gray-400 dark:text-gray-500 truncate max-w-[55%]"
|
|
61
75
|
}, attribution)), React.createElement("div", {
|
|
62
|
-
className: `p-2 ${
|
|
76
|
+
className: `p-2 ${isImageGrid ? 'grid grid-cols-2 gap-2' : 'flex flex-col gap-2'}`,
|
|
63
77
|
onClick: open,
|
|
64
78
|
role: onMessageClick ? 'button' : undefined
|
|
65
79
|
}, items.map((item, i) => React.createElement(MediaAsset, {
|
|
66
80
|
key: (item.url ?? '') + i,
|
|
67
|
-
item: item
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
item: item,
|
|
82
|
+
onMeta: items.length === 1 ? setAssetMeta : undefined
|
|
83
|
+
}))), metaCells.length > 0 && React.createElement("div", {
|
|
84
|
+
className: "px-3 pb-1 flex flex-wrap items-center gap-2 text-[11px] text-gray-500 dark:text-gray-400"
|
|
85
|
+
}, metaCells.map((cell, i) => React.createElement(React.Fragment, {
|
|
86
|
+
key: cell + i
|
|
87
|
+
}, i > 0 && React.createElement("span", {
|
|
88
|
+
className: "text-gray-300 dark:text-gray-600"
|
|
89
|
+
}, "\u00B7"), React.createElement(MetaCell, null, cell)))), React.createElement("div", {
|
|
90
|
+
className: "px-3 pb-3 pt-2"
|
|
70
91
|
}, attachment.prompt && React.createElement("p", {
|
|
71
92
|
className: "text-xs text-gray-500 dark:text-gray-400 line-clamp-2",
|
|
72
93
|
title: attachment.prompt
|
|
73
94
|
}, attachment.prompt), React.createElement("div", {
|
|
74
|
-
className: "mt-2 flex items-center gap-
|
|
95
|
+
className: "mt-2 flex flex-wrap items-center gap-2"
|
|
75
96
|
}, React.createElement("a", {
|
|
76
97
|
href: attachment.url,
|
|
77
98
|
target: "_blank",
|
|
78
99
|
rel: "noopener noreferrer",
|
|
79
100
|
onClick: e => e.stopPropagation(),
|
|
80
|
-
className: "inline-flex items-center gap-1 text-xs font-medium text-
|
|
81
|
-
},
|
|
82
|
-
className: "w-3 h-3"
|
|
83
|
-
})), React.createElement("a", {
|
|
101
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-blue-600 hover:bg-blue-700 px-2.5 py-1.5 text-xs font-medium text-white transition-colors"
|
|
102
|
+
}, React.createElement(FiExternalLink, {
|
|
103
|
+
className: "w-3.5 h-3.5"
|
|
104
|
+
}), " Open"), React.createElement("a", {
|
|
84
105
|
href: attachment.url,
|
|
85
106
|
download: attachment.fileName || true,
|
|
86
107
|
onClick: e => e.stopPropagation(),
|
|
87
|
-
className: "inline-flex items-center gap-1 text-xs font-medium text-gray-
|
|
88
|
-
},
|
|
89
|
-
className: "w-3 h-3"
|
|
90
|
-
}))
|
|
108
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-200 transition-colors"
|
|
109
|
+
}, React.createElement(FiDownload, {
|
|
110
|
+
className: "w-3.5 h-3.5"
|
|
111
|
+
}), " Download"), React.createElement("button", {
|
|
112
|
+
type: "button",
|
|
113
|
+
onClick: e => {
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
void copyLink();
|
|
116
|
+
},
|
|
117
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-200 transition-colors"
|
|
118
|
+
}, copied ? React.createElement(FiCheck, {
|
|
119
|
+
className: "w-3.5 h-3.5"
|
|
120
|
+
}) : React.createElement(FiCopy, {
|
|
121
|
+
className: "w-3.5 h-3.5"
|
|
122
|
+
}), copied ? 'Copied' : 'Copy link'))));
|
|
91
123
|
};export{MessageMedia};//# sourceMappingURL=MessageMedia.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageMedia.js","sources":["../../../src/components/MessageWidgets/MessageMedia.tsx"],"sourcesContent":[null],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MessageMedia.js","sources":["../../../src/components/MessageWidgets/MessageMedia.tsx"],"sourcesContent":[null],"names":[],"mappings":"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"}
|