@flamingo-stack/openframe-frontend-core 0.0.222 → 0.0.223-snapshot.20260604201541
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-QYRV6MKX.js → chunk-53EA4QPZ.js} +13 -7
- package/dist/chunk-53EA4QPZ.js.map +1 -0
- package/dist/{chunk-GMNE7P7V.cjs → chunk-7JGR67H2.cjs} +26 -26
- package/dist/{chunk-GMNE7P7V.cjs.map → chunk-7JGR67H2.cjs.map} +1 -1
- package/dist/{chunk-KM4KK2A6.cjs → chunk-AENTBOSN.cjs} +2656 -1774
- package/dist/chunk-AENTBOSN.cjs.map +1 -0
- package/dist/{chunk-AIKPR6BE.js → chunk-BNYPQCKL.js} +6179 -5297
- package/dist/chunk-BNYPQCKL.js.map +1 -0
- package/dist/{chunk-QNYH3WUU.cjs → chunk-JUWPBMQM.cjs} +13 -7
- package/dist/chunk-JUWPBMQM.cjs.map +1 -0
- package/dist/{chunk-CCSEXVI4.js → chunk-LEHEHEVW.js} +2 -2
- package/dist/{chunk-ND24XSVT.js → chunk-OAOLVEYW.js} +3 -3
- package/dist/chunk-OAOLVEYW.js.map +1 -0
- package/dist/{chunk-4JOWZLEL.cjs → chunk-SZCTY3YF.cjs} +7 -7
- package/dist/{chunk-4JOWZLEL.cjs.map → chunk-SZCTY3YF.cjs.map} +1 -1
- package/dist/{chunk-RMOHKAHA.js → chunk-V6ME2VMU.js} +3 -3
- package/dist/{chunk-IZA5WF44.cjs → chunk-WIAQRCJ5.cjs} +12 -12
- package/dist/{chunk-IZA5WF44.cjs.map → chunk-WIAQRCJ5.cjs.map} +1 -1
- package/dist/{chunk-HRIAZQ5D.js → chunk-YEOM52ZR.js} +3 -3
- package/dist/{chunk-A2SFHYVF.cjs → chunk-ZCHTRQBO.cjs} +10 -10
- package/dist/{chunk-A2SFHYVF.cjs.map → chunk-ZCHTRQBO.cjs.map} +1 -1
- package/dist/components/chat/chat-archive-page.d.ts.map +1 -1
- package/dist/components/chat/chat-composer.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-skeleton.d.ts +19 -4
- package/dist/components/chat/chat-message-skeleton.d.ts.map +1 -1
- package/dist/components/chat/chat-panel-header-mobile.d.ts +24 -0
- package/dist/components/chat/chat-panel-header-mobile.d.ts.map +1 -0
- package/dist/components/chat/chat-panel-header.d.ts +6 -0
- package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
- package/dist/components/chat/chat-quick-action-row.d.ts +31 -0
- package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -0
- package/dist/components/chat/embeddable-chat.d.ts +27 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.d.ts +14 -0
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/github-activity-card.d.ts +10 -1
- package/dist/components/chat/entity-cards/github-activity-card.d.ts.map +1 -1
- package/dist/components/chat/guide-welcome.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +9 -0
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-slash-commands.d.ts +26 -0
- package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +12 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-unified-chat.d.ts +12 -0
- package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +9 -3
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +8 -2
- package/dist/components/chat/mingo-chat-history.d.ts +10 -0
- package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
- package/dist/components/chat/mingo-info-card.d.ts +63 -0
- package/dist/components/chat/mingo-info-card.d.ts.map +1 -0
- package/dist/components/chat/mingo-welcome.d.ts +14 -1
- package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
- package/dist/components/chat/types/chat.types.d.ts +6 -0
- package/dist/components/chat/types/chat.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +40 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/index.cjs +110 -104
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +36 -30
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-layout-drawer.d.ts +7 -0
- package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +26 -26
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/tickets/index.cjs +67 -67
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/actions-menu.d.ts +9 -0
- package/dist/components/ui/actions-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-button.d.ts +2 -2
- package/dist/components/ui/dropdown-button.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +9 -3
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +8 -2
- package/dist/components/ui/modal-v2.d.ts.map +1 -1
- package/dist/components/ui/more-actions-menu.d.ts +13 -0
- package/dist/components/ui/more-actions-menu.d.ts.map +1 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +9 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
- package/dist/components/ui/square-avatar.d.ts +4 -0
- package/dist/components/ui/square-avatar.d.ts.map +1 -1
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/hooks/index.cjs +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/ui/use-auto-limit-tags.d.ts +7 -1
- package/dist/hooks/ui/use-auto-limit-tags.d.ts.map +1 -1
- package/dist/index.cjs +9 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -2
- package/package.json +1 -1
- package/src/components/chat/chat-archive-page.tsx +32 -7
- package/src/components/chat/chat-composer.tsx +6 -2
- package/src/components/chat/chat-message-enhanced.tsx +91 -21
- package/src/components/chat/chat-message-list.tsx +1 -1
- package/src/components/chat/chat-message-skeleton.tsx +60 -60
- package/src/components/chat/chat-panel-header-mobile.tsx +94 -0
- package/src/components/chat/chat-panel-header.tsx +78 -50
- package/src/components/chat/chat-quick-action-row.tsx +146 -0
- package/src/components/chat/embeddable-chat.tsx +185 -52
- package/src/components/chat/entity-cards/dispatch.tsx +770 -232
- package/src/components/chat/entity-cards/github-activity-card.tsx +9 -3
- package/src/components/chat/guide-welcome.tsx +11 -8
- package/src/components/chat/hooks/use-chat-card-item.ts +17 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +80 -7
- package/src/components/chat/hooks/use-slash-commands.ts +39 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +49 -45
- package/src/components/chat/hooks/use-unified-chat.ts +69 -34
- package/src/components/chat/mingo-chat-history.tsx +118 -36
- package/src/components/chat/mingo-info-card.tsx +214 -0
- package/src/components/chat/mingo-welcome.tsx +89 -36
- package/src/components/chat/types/chat.types.ts +4 -0
- package/src/components/chat/types/unified-chat-state.types.ts +46 -0
- package/src/components/chat/utils/process-historical-messages.ts +8 -0
- package/src/components/content-loading-container.tsx +1 -1
- package/src/components/empty-state.tsx +3 -3
- package/src/components/faq-accordion.tsx +2 -2
- package/src/components/features/changelog-manager.tsx +2 -2
- package/src/components/features/highlight-config-section.tsx +2 -2
- package/src/components/features/og-editor-preview.tsx +3 -3
- package/src/components/features/release-media-manager.tsx +2 -2
- package/src/components/loading/card-skeleton.tsx +3 -3
- package/src/components/loading/device-card-skeleton.tsx +1 -1
- package/src/components/loading/margin-report-skeleton.tsx +2 -2
- package/src/components/loading/organization-card-skeleton.tsx +1 -1
- package/src/components/loading/organization-icon-skeleton.tsx +1 -1
- package/src/components/loading/page-layout-skeleton.tsx +9 -9
- package/src/components/loading/users-grid-skeleton.tsx +2 -2
- package/src/components/logs-list.tsx +1 -1
- package/src/components/media-carousel.tsx +1 -1
- package/src/components/navigation/app-layout-drawer.tsx +70 -6
- package/src/components/open-source-features.tsx +2 -2
- package/src/components/ui/actions-menu.tsx +36 -13
- package/src/components/ui/allowed-domains-input.tsx +3 -3
- package/src/components/ui/cursor-pagination.tsx +1 -1
- package/src/components/ui/dropdown-button.tsx +6 -6
- package/src/components/ui/info-row.tsx +2 -2
- package/src/components/ui/modal-v2.tsx +8 -1
- package/src/components/ui/more-actions-menu.tsx +13 -0
- package/src/components/ui/simple-markdown-renderer.tsx +12 -2
- package/src/components/ui/square-avatar.tsx +6 -1
- package/src/components/ui/tags-input.tsx +1 -1
- package/src/components/ui/textarea.tsx +18 -4
- package/src/components/vendor-icon.tsx +1 -1
- package/src/components/vendor-tag.tsx +1 -1
- package/src/components/why-it-matters.tsx +1 -1
- package/src/hooks/ui/use-auto-limit-tags.ts +22 -8
- package/src/stories/ActionsMenu.stories.tsx +256 -0
- package/src/stories/AppLayoutSidebar.stories.tsx +6 -0
- package/src/stories/Board.stories.tsx +1 -1
- package/src/stories/ChatMingoInfoCards.stories.tsx +748 -0
- package/src/stories/ChatTicketList.stories.tsx +1 -1
- package/src/stories/DataTable.stories.tsx +17 -13
- package/src/stories/EmbeddableChat.stories.tsx +233 -1
- package/src/stories/MingoInfoCard.stories.tsx +193 -0
- package/src/stories/MingoOnboardingCard.stories.tsx +1 -1
- package/src/stories/Modal.stories.tsx +1 -0
- package/src/stories/NotificationDrawer.stories.tsx +14 -3
- package/src/stories/PageActions.stories.tsx +4 -4
- package/src/stories/PageContainer.stories.tsx +5 -5
- package/src/stories/PageLayout.stories.tsx +1 -1
- package/src/stories/TabSelector.stories.tsx +2 -2
- package/src/stories/Tag.stories.tsx +10 -5
- package/src/stories/TicketInfoSection.stories.tsx +23 -11
- package/src/stories/Toaster.stories.tsx +5 -5
- package/src/stories/__fixtures__/chat-card-decorator.tsx +44 -0
- package/dist/chunk-AIKPR6BE.js.map +0 -1
- package/dist/chunk-KM4KK2A6.cjs.map +0 -1
- package/dist/chunk-ND24XSVT.js.map +0 -1
- package/dist/chunk-QNYH3WUU.cjs.map +0 -1
- package/dist/chunk-QYRV6MKX.js.map +0 -1
- package/src/stories/.TicketDescriptionViewer.stories.md +0 -39
- package/src/stories/.TicketKnowledgeBaseList.stories.md +0 -36
- package/src/stories/TicketDescriptionViewer.stories.tsx +0 -70
- package/src/stories/TicketKnowledgeBaseList.stories.tsx +0 -51
- /package/dist/{chunk-CCSEXVI4.js.map → chunk-LEHEHEVW.js.map} +0 -0
- /package/dist/{chunk-RMOHKAHA.js.map → chunk-V6ME2VMU.js.map} +0 -0
- /package/dist/{chunk-HRIAZQ5D.js.map → chunk-YEOM52ZR.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import React, { forwardRef, memo, useMemo } from "react"
|
|
3
|
+
import React, { forwardRef, memo, useMemo, useRef } from "react"
|
|
4
4
|
import { cn } from "../../utils/cn"
|
|
5
5
|
import { SquareAvatar } from "../ui/square-avatar"
|
|
6
6
|
import { ToolExecutionDisplay } from "./tool-execution-display"
|
|
@@ -57,6 +57,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
57
57
|
|
|
58
58
|
const segments = useMemo(() => normalizeContent(content), [content])
|
|
59
59
|
|
|
60
|
+
// Cross-render cache of rendered inline-card nodes, keyed by `type:id`.
|
|
61
|
+
// A fetch-mode card lives inside the assistant message, which re-renders on
|
|
62
|
+
// every stream chunk (and the whole list re-renders when a new message
|
|
63
|
+
// arrives). `renderEntityCard` produces a FRESH element each time, so React
|
|
64
|
+
// re-mounts the card — closing any open menu/popover and re-triggering its
|
|
65
|
+
// fetch. Caching the produced node by key and returning the SAME element
|
|
66
|
+
// reference lets React bail out of re-rendering that subtree, so the card
|
|
67
|
+
// (and its open menu) survives across chunks. Invalidated per key when the
|
|
68
|
+
// backing ref or the render fn identity changes.
|
|
69
|
+
const renderedCardNodeCache = useRef(
|
|
70
|
+
new Map<string, { refMatch: ChatRef | undefined; render: ((ref: ChatRef) => React.ReactNode) | undefined; node: React.ReactNode }>(),
|
|
71
|
+
)
|
|
72
|
+
|
|
60
73
|
/**
|
|
61
74
|
* Per-message rendering plan for `[card://type:id]` markers.
|
|
62
75
|
*
|
|
@@ -94,7 +107,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
94
107
|
| { kind: 'block'; key: string; node: React.ReactNode }
|
|
95
108
|
const partsBySegment = new Map<number, SegmentPart[]>()
|
|
96
109
|
if (!render) return { inlineByKey, partsBySegment }
|
|
97
|
-
const
|
|
110
|
+
const cache = renderedCardNodeCache.current
|
|
111
|
+
const usedKeys = new Set<string>()
|
|
112
|
+
// Card keys already emitted as a hoisted block (`b-<key>`). The same
|
|
113
|
+
// marker can legitimately appear twice in one message (LLM references
|
|
114
|
+
// the same entity twice); we hoist the FIRST occurrence and skip the
|
|
115
|
+
// duplicates so two siblings never collide on the same React key.
|
|
116
|
+
const emittedBlockKeys = new Set<string>()
|
|
98
117
|
segments.forEach((segment, segIdx) => {
|
|
99
118
|
if (segment.type !== 'text') return
|
|
100
119
|
const text = segment.text
|
|
@@ -106,11 +125,15 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
106
125
|
const cardType = match[1]
|
|
107
126
|
const cardId = match[2]
|
|
108
127
|
const key = `${cardType}:${cardId}`
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
usedKeys.add(key)
|
|
129
|
+
// Reuse the cached node when neither the backing ref nor the render
|
|
130
|
+
// fn changed — returning the SAME element reference across renders is
|
|
131
|
+
// what stops React from re-mounting the card (and closing its open
|
|
132
|
+
// menu / re-fetching) on every stream chunk. Also dedups the same key
|
|
133
|
+
// emitted twice within one message.
|
|
134
|
+
const refMatch = refs[key]
|
|
135
|
+
let entry = cache.get(key)
|
|
136
|
+
if (!entry || entry.refMatch !== refMatch || entry.render !== render) {
|
|
114
137
|
// Always invoke render() — even when the metadata map has
|
|
115
138
|
// no entry for this marker. Fetch-mode card types
|
|
116
139
|
// (delivery_item, roadmap_item, internal_task, etc.) don't
|
|
@@ -129,25 +152,30 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
129
152
|
// `ref.title` shows the id rather than `undefined`) and
|
|
130
153
|
// `url` to null (matches the no-link semantics fetch-mode
|
|
131
154
|
// cards rely on — they resolve their own URL after fetch).
|
|
132
|
-
const refMatch = refs[key]
|
|
133
155
|
const refForRender: ChatRef = refMatch ?? {
|
|
134
156
|
type: cardType,
|
|
135
157
|
id: cardId,
|
|
136
158
|
title: cardId,
|
|
137
159
|
url: null,
|
|
138
160
|
}
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
entry = { refMatch, render, node: render(refForRender) }
|
|
162
|
+
cache.set(key, entry)
|
|
141
163
|
}
|
|
164
|
+
const rendered = entry.node
|
|
142
165
|
if (React.isValidElement(rendered) && rendered.type === BlockCard) {
|
|
143
166
|
const props = rendered.props as BlockCardProps
|
|
144
167
|
const markerEnd = match.index + match[0].length
|
|
145
168
|
// Text chunk INCLUDING the marker — the inline pill renders
|
|
146
169
|
// at the marker position via the `<a>` override.
|
|
147
170
|
parts.push({ kind: 'text', text: text.slice(cursor, markerEnd) })
|
|
148
|
-
|
|
171
|
+
// Hoist the block payload only on the FIRST occurrence of this key
|
|
172
|
+
// — a repeated marker still gets its inline pill (text + override
|
|
173
|
+
// above) but must not push a second `b-<key>` sibling.
|
|
174
|
+
if (!emittedBlockKeys.has(key)) {
|
|
175
|
+
emittedBlockKeys.add(key)
|
|
176
|
+
parts.push({ kind: 'block', key, node: props.children })
|
|
177
|
+
}
|
|
149
178
|
cursor = markerEnd
|
|
150
|
-
const refMatch = refs[key]
|
|
151
179
|
inlineByKey.set(
|
|
152
180
|
key,
|
|
153
181
|
props.inline != null
|
|
@@ -155,8 +183,26 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
155
183
|
: <span className="text-ods-text-primary font-medium">{refMatch?.title ?? cardId}</span>,
|
|
156
184
|
)
|
|
157
185
|
} else if (rendered != null) {
|
|
158
|
-
//
|
|
159
|
-
|
|
186
|
+
// Hoist fetch-mode entity cards (roadmap/blog/case-study/release/…)
|
|
187
|
+
// OUT of the markdown as stable-keyed block siblings — exactly like
|
|
188
|
+
// BlockCard. Rendered INSIDE `<SimpleMarkdownRenderer>` they remount
|
|
189
|
+
// on every streaming re-parse (react-markdown rebuilds its subtree
|
|
190
|
+
// token-by-token), which closes any open menu/popover and re-fires
|
|
191
|
+
// the card's fetch. As a sibling keyed by the card key (`b-<key>`)
|
|
192
|
+
// the card survives streaming: appending more text/markers only adds
|
|
193
|
+
// NEW siblings, existing cards keep their React instance.
|
|
194
|
+
//
|
|
195
|
+
// We EXCLUDE the marker from the surrounding text (cursor jumps past
|
|
196
|
+
// it) so no inline pill renders — the full card IS the content.
|
|
197
|
+
// Hoist only the FIRST occurrence; a duplicate marker is dropped
|
|
198
|
+
// entirely (no inline pill for hoisted cards) so we never push a
|
|
199
|
+
// second sibling colliding on the same `b-<key>` React key.
|
|
200
|
+
parts.push({ kind: 'text', text: text.slice(cursor, match.index) })
|
|
201
|
+
if (!emittedBlockKeys.has(key)) {
|
|
202
|
+
emittedBlockKeys.add(key)
|
|
203
|
+
parts.push({ kind: 'block', key, node: rendered })
|
|
204
|
+
}
|
|
205
|
+
cursor = match.index + match[0].length
|
|
160
206
|
}
|
|
161
207
|
}
|
|
162
208
|
// Trailing text after the last block marker (or the entire
|
|
@@ -172,6 +218,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
172
218
|
partsBySegment.set(segIdx, parts)
|
|
173
219
|
}
|
|
174
220
|
})
|
|
221
|
+
// Drop cached nodes for markers no longer present so the cache can't
|
|
222
|
+
// grow unbounded as a long message's markers change.
|
|
223
|
+
if (cache.size > usedKeys.size) {
|
|
224
|
+
for (const k of cache.keys()) {
|
|
225
|
+
if (!usedKeys.has(k)) cache.delete(k)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
175
228
|
return { inlineByKey, partsBySegment }
|
|
176
229
|
}, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
|
|
177
230
|
|
|
@@ -263,18 +316,29 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
263
316
|
|
|
264
317
|
const getAvatarProps = () => {
|
|
265
318
|
const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
|
|
266
|
-
const initials = displayName.split(' ').map(n => n[0]).join('').toUpperCase()
|
|
267
319
|
const isMingo = assistantType === 'mingo'
|
|
268
|
-
|
|
320
|
+
|
|
269
321
|
return {
|
|
270
322
|
src: avatar || undefined,
|
|
271
323
|
alt: `${displayName} avatar`,
|
|
272
|
-
|
|
324
|
+
// Pass the FULL name — SquareAvatar derives first+last initials itself
|
|
325
|
+
// (passing pre-joined initials like "PS" would collapse to one letter,
|
|
326
|
+
// since getFirstLastInitials treats it as a single word).
|
|
327
|
+
fallback: displayName,
|
|
273
328
|
size: "sm" as const,
|
|
274
329
|
variant: "round" as const,
|
|
330
|
+
// User avatar: compact 20×20 with 2px padding and a subtle gray fill
|
|
331
|
+
// (`bg-ods-card`) so the `border-ods-border` ring stays visible — the
|
|
332
|
+
// brand fill reads poorly for a user. Assistant/Fae keep their brand
|
|
333
|
+
// fill. Initials are smaller + muted gray for the user placeholder.
|
|
334
|
+
...(isUser ? { initialsClassName: "text-[9px] text-ods-text-secondary" } : {}),
|
|
275
335
|
className: cn(
|
|
276
336
|
"flex-shrink-0",
|
|
277
|
-
|
|
337
|
+
isUser
|
|
338
|
+
? "h-5 w-5 p-0.5 bg-ods-card"
|
|
339
|
+
: isMingo
|
|
340
|
+
? "bg-ods-flamingo-cyan"
|
|
341
|
+
: "bg-ods-flamingo-pink"
|
|
278
342
|
)
|
|
279
343
|
}
|
|
280
344
|
}
|
|
@@ -315,9 +379,15 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
315
379
|
should be sized at ~50-60% of the wrapper (h-4 w-4 =
|
|
316
380
|
16px works well for a 32px circle). */}
|
|
317
381
|
<div className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
318
|
-
{
|
|
319
|
-
|
|
320
|
-
|
|
382
|
+
{/* All non-system rows render an avatar. User bubbles always show
|
|
383
|
+
the SquareAvatar (image, or an initials fallback on missing/
|
|
384
|
+
failed image). Assistant/Fae use the host brand icon when no
|
|
385
|
+
avatar is supplied, else the filled SquareAvatar. */}
|
|
386
|
+
{showAvatar && !isSystem && (
|
|
387
|
+
!isUser && assistantIcon && !avatar ? (
|
|
388
|
+
// Host-supplied brand icon (e.g. Mingo): render it directly,
|
|
389
|
+
// no filled pill — the icon carries its own brand accent.
|
|
390
|
+
<div className="flex items-center justify-center flex-shrink-0">
|
|
321
391
|
{assistantIcon}
|
|
322
392
|
</div>
|
|
323
393
|
) : (
|
|
@@ -411,7 +411,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
411
411
|
<ChatMessageListSkeleton
|
|
412
412
|
className={className}
|
|
413
413
|
showAvatars={showAvatars}
|
|
414
|
-
|
|
414
|
+
fullWidth={fullWidth}
|
|
415
415
|
contentClassName={contentClassName}
|
|
416
416
|
messageCount={6}
|
|
417
417
|
/>
|
|
@@ -6,54 +6,40 @@ interface ChatMessageSkeletonProps {
|
|
|
6
6
|
className?: string
|
|
7
7
|
showAvatar?: boolean
|
|
8
8
|
isUser?: boolean
|
|
9
|
-
assistantType?: 'mingo' | 'fae'
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
/**
|
|
12
|
+
* One message-row skeleton. Mirrors `ChatMessageEnhanced`'s real layout:
|
|
13
|
+
* full panel width, no card background, and an INLINE avatar in the name row
|
|
14
|
+
* (the 2025 chat pattern — Claude.ai / ChatGPT / Gemini), NOT the legacy
|
|
15
|
+
* `absolute -left-16` hanging avatar. Body lines use percentage widths so they
|
|
16
|
+
* reflow cleanly as the panel resizes.
|
|
17
|
+
*/
|
|
18
|
+
export function ChatMessageSkeleton({
|
|
19
|
+
className,
|
|
20
|
+
showAvatar = true,
|
|
21
|
+
isUser = false,
|
|
17
22
|
}: ChatMessageSkeletonProps) {
|
|
18
|
-
const isMingo = assistantType === 'mingo'
|
|
19
|
-
|
|
20
23
|
return (
|
|
21
|
-
<div
|
|
22
|
-
className=
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
isMingo
|
|
34
|
-
? "bg-gradient-to-br from-cyan-400/30 to-cyan-600/30"
|
|
35
|
-
: "bg-gradient-to-br from-pink-400/30 to-pink-600/30"
|
|
36
|
-
)} />
|
|
37
|
-
)}
|
|
38
|
-
|
|
39
|
-
{/* Message Content Skeleton */}
|
|
40
|
-
<div className="flex flex-col gap-1 min-w-0">
|
|
41
|
-
{/* Name and Timestamp Row Skeleton */}
|
|
42
|
-
<div className="flex items-center justify-between pr-2">
|
|
43
|
-
<div className="h-5 w-16 bg-ods-border rounded animate-pulse" />
|
|
44
|
-
<div className="h-4 w-12 bg-ods-border rounded animate-pulse" />
|
|
24
|
+
<div className={cn("relative py-[var(--spacing-system-s)]", className)}>
|
|
25
|
+
<div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
|
|
26
|
+
{/* Name row — inline avatar (assistant only) + name + timestamp,
|
|
27
|
+
matching ChatMessageEnhanced's inline-avatar header. Neutral
|
|
28
|
+
`bg-ods-border` so the placeholders are actually visible (a tinted
|
|
29
|
+
low-alpha fill reads as blank on the dark surface). */}
|
|
30
|
+
<div className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
31
|
+
{showAvatar && !isUser && (
|
|
32
|
+
<div className="h-8 w-8 flex-shrink-0 rounded-full bg-ods-border animate-pulse" />
|
|
33
|
+
)}
|
|
34
|
+
<div className="h-5 w-32 rounded bg-ods-border animate-pulse" />
|
|
35
|
+
<div className="ml-auto h-4 w-14 rounded bg-ods-border animate-pulse" />
|
|
45
36
|
</div>
|
|
46
|
-
|
|
47
|
-
{/*
|
|
48
|
-
<div className="flex flex-col gap-2">
|
|
49
|
-
<div className="
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{/* Second line - 80% width */}
|
|
53
|
-
<div className="h-4 w-4/5 bg-ods-border rounded animate-pulse" />
|
|
54
|
-
{/* Third line - 60% width (optional for variation) */}
|
|
55
|
-
<div className="h-4 w-3/5 bg-ods-border rounded animate-pulse" />
|
|
56
|
-
</div>
|
|
37
|
+
|
|
38
|
+
{/* Body lines — percentage widths so they reflow on panel resize. */}
|
|
39
|
+
<div className="flex flex-col gap-2 pt-[var(--spacing-system-xxs)]">
|
|
40
|
+
<div className="h-4 w-full rounded bg-ods-border animate-pulse" />
|
|
41
|
+
<div className="h-4 w-11/12 rounded bg-ods-border animate-pulse" />
|
|
42
|
+
<div className="h-4 w-3/5 rounded bg-ods-border animate-pulse" />
|
|
57
43
|
</div>
|
|
58
44
|
</div>
|
|
59
45
|
</div>
|
|
@@ -64,7 +50,16 @@ interface ChatMessageListSkeletonProps {
|
|
|
64
50
|
className?: string
|
|
65
51
|
messageCount?: number
|
|
66
52
|
showAvatars?: boolean
|
|
67
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Match `ChatMessageList`'s `fullWidth`: drop the centered
|
|
55
|
+
* `max-w-ods-content-narrow` column so the skeleton fills the panel and
|
|
56
|
+
* reflows on resize (instead of staying a fixed narrow column with large
|
|
57
|
+
* side gutters). Same semantics as the real list.
|
|
58
|
+
*/
|
|
59
|
+
fullWidth?: boolean
|
|
60
|
+
/** Extra classes on the content column (e.g. host-provided padding). The
|
|
61
|
+
* host wrapper is expected to own the panel padding; this stays empty by
|
|
62
|
+
* default so the skeleton sits flush. */
|
|
68
63
|
contentClassName?: string
|
|
69
64
|
}
|
|
70
65
|
|
|
@@ -72,13 +67,12 @@ export function ChatMessageListSkeleton({
|
|
|
72
67
|
className,
|
|
73
68
|
messageCount = 6,
|
|
74
69
|
showAvatars = true,
|
|
75
|
-
|
|
76
|
-
contentClassName
|
|
70
|
+
fullWidth = false,
|
|
71
|
+
contentClassName,
|
|
77
72
|
}: ChatMessageListSkeletonProps) {
|
|
78
73
|
const messages = Array.from({ length: messageCount }, (_, index) => ({
|
|
79
74
|
id: index,
|
|
80
75
|
isUser: index % 3 === 0,
|
|
81
|
-
assistantType: assistantType
|
|
82
76
|
}))
|
|
83
77
|
|
|
84
78
|
return (
|
|
@@ -88,23 +82,29 @@ export function ChatMessageListSkeleton({
|
|
|
88
82
|
"flex h-full w-full flex-col overflow-y-auto overflow-x-hidden flex-1",
|
|
89
83
|
"[scroll-behavior:smooth]",
|
|
90
84
|
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
|
|
91
|
-
className
|
|
85
|
+
className,
|
|
92
86
|
)}
|
|
93
87
|
>
|
|
94
|
-
<div
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
fullWidth
|
|
91
|
+
? "flex w-full flex-col min-w-0"
|
|
92
|
+
: "mx-auto flex w-full max-w-ods-content-narrow flex-col min-w-0",
|
|
93
|
+
contentClassName,
|
|
94
|
+
)}
|
|
95
|
+
style={{ minHeight: '100%' }}
|
|
96
|
+
>
|
|
97
|
+
{/* Bottom-anchor the rows like the real list. */}
|
|
95
98
|
<div className="flex-1" />
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/>
|
|
104
|
-
))}
|
|
105
|
-
</div>
|
|
99
|
+
{messages.map((message) => (
|
|
100
|
+
<ChatMessageSkeleton
|
|
101
|
+
key={message.id}
|
|
102
|
+
showAvatar={showAvatars}
|
|
103
|
+
isUser={message.isUser}
|
|
104
|
+
/>
|
|
105
|
+
))}
|
|
106
106
|
</div>
|
|
107
107
|
</div>
|
|
108
108
|
</div>
|
|
109
109
|
)
|
|
110
|
-
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Chevron02LeftIcon, ClockHistoryIcon } from '../icons-v2-generated'
|
|
4
|
+
import { Button } from '../ui/button'
|
|
5
|
+
import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
|
|
6
|
+
import { cn } from '../../utils/cn'
|
|
7
|
+
import type { ChatPanelHeaderProps } from './chat-panel-header'
|
|
8
|
+
|
|
9
|
+
export interface ChatPanelHeaderMobileProps extends ChatPanelHeaderProps {
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mobile chat panel header — Figma node `7363:85532`. A distinct layout from
|
|
15
|
+
* the desktop bar (which packs full-height divider cells into a fixed `h-14`):
|
|
16
|
+
*
|
|
17
|
+
* - Surface: `ods-card` with a bottom border; generous `l` side padding,
|
|
18
|
+
* `l` bottom padding, and `xl` top padding on the title block (so the row
|
|
19
|
+
* bottom-aligns under a tall header).
|
|
20
|
+
* - Title: `text-h2` (vs the desktop `h3`/`h4`), truncated.
|
|
21
|
+
* - Actions: 44px outlined icon buttons (`Button variant="outline"
|
|
22
|
+
* size="icon"`) — a back chevron (when applicable) and a single trailing
|
|
23
|
+
* `⋯` menu carrying the view-dependent actions. There is no Close action
|
|
24
|
+
* (the drawer is dismissed via its backdrop / the back chevron); when no
|
|
25
|
+
* action applies the `⋯` trigger is hidden entirely.
|
|
26
|
+
*
|
|
27
|
+
* Same prop contract as `ChatPanelHeader` (`onClose` is accepted for parity but
|
|
28
|
+
* unused here); the orchestrator renders this on mobile and the desktop bar on
|
|
29
|
+
* `md+`.
|
|
30
|
+
*/
|
|
31
|
+
export function ChatPanelHeaderMobile({
|
|
32
|
+
showBack = false,
|
|
33
|
+
title,
|
|
34
|
+
backAriaLabel = 'Back',
|
|
35
|
+
isArchivedView = false,
|
|
36
|
+
onBack,
|
|
37
|
+
onRestore,
|
|
38
|
+
onRename,
|
|
39
|
+
onArchive,
|
|
40
|
+
onOpenArchive,
|
|
41
|
+
className,
|
|
42
|
+
}: ChatPanelHeaderMobileProps) {
|
|
43
|
+
// The right-hand ⋯ menu carries the view-dependent actions. Close is not
|
|
44
|
+
// listed (the panel is dismissed elsewhere); the trigger is hidden entirely
|
|
45
|
+
// when no action applies (e.g. the archive list view).
|
|
46
|
+
const menuItems = [
|
|
47
|
+
!showBack && onOpenArchive && { id: 'open-archive', label: 'Chat archive', icon: <ClockHistoryIcon className="w-full h-full" />, onClick: onOpenArchive },
|
|
48
|
+
isArchivedView && onRestore && { id: 'unarchive', label: 'Unarchive chat', onClick: onRestore },
|
|
49
|
+
!isArchivedView && onRename && { id: 'rename', label: 'Rename chat', onClick: onRename },
|
|
50
|
+
!isArchivedView && onArchive && { id: 'archive', label: 'Archive chat', onClick: onArchive },
|
|
51
|
+
].filter(Boolean) as ActionsMenuItem[]
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className={cn(
|
|
56
|
+
'flex-shrink-0 flex flex-col border-b border-ods-border bg-ods-card',
|
|
57
|
+
'px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]',
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<div className="flex w-full items-end gap-[var(--spacing-system-sf)]">
|
|
62
|
+
{showBack && (
|
|
63
|
+
<Button
|
|
64
|
+
variant="outline"
|
|
65
|
+
size="icon"
|
|
66
|
+
aria-label={backAriaLabel}
|
|
67
|
+
onClick={onBack}
|
|
68
|
+
className="shrink-0"
|
|
69
|
+
>
|
|
70
|
+
<Chevron02LeftIcon />
|
|
71
|
+
</Button>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Title block — `xl` top padding pushes the title down so the row
|
|
75
|
+
bottom-aligns the title, back chevron, and ⋯ menu. */}
|
|
76
|
+
<div className="flex flex-1 min-w-0 items-end gap-[var(--spacing-system-m)] pt-[var(--spacing-system-xl)]">
|
|
77
|
+
<div className="flex flex-1 min-w-0 flex-col gap-[var(--spacing-system-xs)]">
|
|
78
|
+
<p className="min-w-0 truncate text-h2 text-ods-text-primary" title={title}>
|
|
79
|
+
{title}
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{menuItems.length > 0 && (
|
|
84
|
+
<ActionsMenuDropdown
|
|
85
|
+
groups={[{ items: menuItems }]}
|
|
86
|
+
triggerAriaLabel="Chat actions"
|
|
87
|
+
triggerClassName="shrink-0 bg-ods-card border-ods-border hover:bg-ods-bg-hover flex items-center justify-center focus-visible:ring-0"
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -9,7 +9,16 @@ import {
|
|
|
9
9
|
} from '../icons-v2-generated'
|
|
10
10
|
import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'
|
|
11
11
|
import { ChatHeaderIconButton } from './chat-header-icon-button'
|
|
12
|
-
import {
|
|
12
|
+
import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
|
|
13
|
+
import { ChatPanelHeaderMobile } from './chat-panel-header-mobile'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compact rounded header button used for the desktop back chevron. Borderless
|
|
17
|
+
* square with a hover background. (Mobile uses its own header component —
|
|
18
|
+
* `ChatPanelHeaderMobile`.)
|
|
19
|
+
*/
|
|
20
|
+
export const COMPACT_HEADER_BUTTON =
|
|
21
|
+
'inline-flex shrink-0 items-center justify-center size-8 rounded-md text-ods-text-secondary transition-colors hover:bg-ods-bg-hover hover:text-ods-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent'
|
|
13
22
|
|
|
14
23
|
export interface ChatPanelHeaderProps {
|
|
15
24
|
/** Show the back-chevron + bold (h3) title. When false, the static list
|
|
@@ -53,62 +62,81 @@ export function ChatPanelHeader({
|
|
|
53
62
|
onArchive,
|
|
54
63
|
onOpenArchive,
|
|
55
64
|
}: ChatPanelHeaderProps) {
|
|
65
|
+
// Desktop ⋯ menu (active, non-archived conversation only) — rename / archive.
|
|
56
66
|
const menuItems = [
|
|
57
|
-
onRename && { label: 'Rename chat', onClick: onRename },
|
|
58
|
-
onArchive && { label: 'Archive chat', onClick: onArchive },
|
|
59
|
-
].filter(Boolean) as
|
|
67
|
+
onRename && { id: 'rename', label: 'Rename chat', onClick: onRename },
|
|
68
|
+
onArchive && { id: 'archive', label: 'Archive chat', onClick: onArchive },
|
|
69
|
+
].filter(Boolean) as ActionsMenuItem[]
|
|
60
70
|
|
|
61
71
|
return (
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
<>
|
|
73
|
+
{/* Mobile (<md): a distinct layout (Figma node 7363:85532). */}
|
|
74
|
+
<ChatPanelHeaderMobile
|
|
75
|
+
className="flex md:hidden"
|
|
76
|
+
showBack={showBack}
|
|
77
|
+
title={title}
|
|
78
|
+
backAriaLabel={backAriaLabel}
|
|
79
|
+
isArchivedView={isArchivedView}
|
|
80
|
+
onBack={onBack}
|
|
81
|
+
onClose={onClose}
|
|
82
|
+
onRestore={onRestore}
|
|
83
|
+
onRename={onRename}
|
|
84
|
+
onArchive={onArchive}
|
|
85
|
+
onOpenArchive={onOpenArchive}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
{/* Desktop (md+): fixed-height bar with full-height divider action cells. */}
|
|
89
|
+
<div className="hidden md:flex flex-shrink-0 h-14 w-full overflow-hidden border-b border-ods-border bg-ods-card">
|
|
90
|
+
<div className="flex flex-1 min-w-0 items-center gap-2 px-4 py-3">
|
|
91
|
+
{showBack ? (
|
|
92
|
+
<>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={onBack}
|
|
96
|
+
aria-label={backAriaLabel}
|
|
97
|
+
className={`${COMPACT_HEADER_BUTTON} -ml-1`}
|
|
98
|
+
>
|
|
99
|
+
<Chevron01LeftIcon size={20} />
|
|
100
|
+
</button>
|
|
101
|
+
<span className="truncate text-h3 text-ods-text-primary">{title}</span>
|
|
102
|
+
</>
|
|
103
|
+
) : (
|
|
104
|
+
<p className="truncate text-h3 text-ods-text-primary">{title}</p>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Restore (refresh) — archived chat header (Figma node 7361:425441). */}
|
|
109
|
+
{isArchivedView && onRestore && (
|
|
110
|
+
<ChatHeaderIconButton onClick={onRestore} aria-label="Unarchive chat">
|
|
111
|
+
<Refresh01LeftIcon size={24} />
|
|
112
|
+
</ChatHeaderIconButton>
|
|
78
113
|
)}
|
|
79
|
-
</div>
|
|
80
114
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
{/* Rename / Archive menu — active (non-archived) conversation only. */}
|
|
116
|
+
{showBack && !isArchivedView && menuItems.length > 0 && (
|
|
117
|
+
<ActionsMenuDropdown
|
|
118
|
+
triggerAriaLabel="Chat actions"
|
|
119
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
120
|
+
groups={[{ items: menuItems }]}
|
|
121
|
+
customTrigger={
|
|
122
|
+
<ChatHeaderIconButton aria-label="Chat actions">
|
|
123
|
+
<Ellipsis01Icon size={24} />
|
|
124
|
+
</ChatHeaderIconButton>
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
87
128
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
trigger={
|
|
95
|
-
<ChatHeaderIconButton aria-label="Chat actions">
|
|
96
|
-
<Ellipsis01Icon size={24} />
|
|
97
|
-
</ChatHeaderIconButton>
|
|
98
|
-
}
|
|
99
|
-
/>
|
|
100
|
-
)}
|
|
129
|
+
{/* Chat Archive entry (Figma node 7532:225034) — list view only. */}
|
|
130
|
+
{!showBack && onOpenArchive && (
|
|
131
|
+
<ChatHeaderIconButton onClick={onOpenArchive} aria-label="Chat archive">
|
|
132
|
+
<ClockHistoryIcon size={24} />
|
|
133
|
+
</ChatHeaderIconButton>
|
|
134
|
+
)}
|
|
101
135
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<ChatHeaderIconButton onClick={onOpenArchive} aria-label="Chat archive">
|
|
105
|
-
<ClockHistoryIcon size={24} />
|
|
136
|
+
<ChatHeaderIconButton onClick={onClose} aria-label="Close">
|
|
137
|
+
<XmarkIcon size={24} />
|
|
106
138
|
</ChatHeaderIconButton>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<ChatHeaderIconButton onClick={onClose} aria-label="Close">
|
|
110
|
-
<XmarkIcon size={24} />
|
|
111
|
-
</ChatHeaderIconButton>
|
|
112
|
-
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</>
|
|
113
141
|
)
|
|
114
142
|
}
|