feat: add in site message comp

This commit is contained in:
Joel
2026-03-09 10:54:19 +08:00
parent 8c0875322e
commit 7ccdc30133
3 changed files with 173 additions and 3 deletions

View File

@ -1,4 +1,5 @@
'use client'
import InSiteMessage from '@/app/components/app/in-site-message'
import { MarkdownWithDirective } from '@/app/components/base/markdown-with-directive'
const markdown1 = `
@ -39,19 +40,60 @@ Dify swag
::::
`
const inSiteMessageMain = `
Were speaking with technical teams to better understand:
- How you discovered Dify
- What resonated — and what didnt
- How we can improve the experience
As a thank-you for your time:
::::withIconCardList
:::withIconCardItem {icon="https://assets.dify.ai/images/gift-card.png"}
$100 Amazon gift card
:::
:::withIconCardItem {icon="https://assets.dify.ai/images/dify-swag.png"}
Exclusive Dify swag
:::
::::
`
export default function RemarkDirectiveTestPage() {
return (
<main style={{ padding: 24 }}>
<h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 16 }}>
remark-directive test page
</h1>
<div className="markdown-body">
<div>
<MarkdownWithDirective markdown={markdown1} />
</div>
<div className="markdown-body !mt-5">
<div className="mt-5">
<MarkdownWithDirective markdown={markdown2} />
</div>
<InSiteMessage
title="Help Shape Dify"
subtitle="Wed love to hear how you evaluate and use Dify"
title_pic_url="https://www.figma.com/api/mcp/asset/ee1209b5-5df1-48a6-8052-ad4a2a08a653"
main={inSiteMessageMain}
actions={[
{ type: 'default', text: 'Not now', action: 'close' },
{
type: 'primary',
text: 'Schedule 30-min Chat',
action: 'link',
data: {
href: 'https://dify.ai',
target: '_blank',
},
},
]}
/>
</main>
)
}

View File

@ -0,0 +1,128 @@
'use client'
import { useMemo, useState } from 'react'
import Button from '@/app/components/base/button'
import { MarkdownWithDirective } from '@/app/components/base/markdown-with-directive'
import { cn } from '@/utils/classnames'
type InSiteMessageAction = 'link' | 'close'
type InSiteMessageButtonType = 'primary' | 'default'
export type InSiteMessageActionItem = {
action: InSiteMessageAction
data?: unknown
text: string
type: InSiteMessageButtonType
}
type InSiteMessageProps = {
actions: InSiteMessageActionItem[]
className?: string
main: string
subtitle: string
title: string
title_pic_url?: string
}
function normalizeLinkData(data: unknown): { href: string, rel?: string, target?: string } | null {
if (typeof data === 'string')
return { href: data, target: '_blank' }
if (!data || typeof data !== 'object')
return null
const candidate = data as { href?: unknown, rel?: unknown, target?: unknown }
if (typeof candidate.href !== 'string' || !candidate.href)
return null
return {
href: candidate.href,
rel: typeof candidate.rel === 'string' ? candidate.rel : undefined,
target: typeof candidate.target === 'string' ? candidate.target : '_blank',
}
}
function InSiteMessage({
actions,
className,
main,
subtitle,
title,
title_pic_url,
}: InSiteMessageProps) {
const [visible, setVisible] = useState(true)
const headerStyle = useMemo(() => {
if (!title_pic_url) {
return {
background: 'linear-gradient(180deg, #3268f4 0%, #194ccf 100%)',
}
}
return {
backgroundImage: `linear-gradient(180deg, rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.4) 100%), url(${title_pic_url})`,
backgroundPosition: 'center',
backgroundSize: 'cover',
}
}, [title_pic_url])
const handleAction = (item: InSiteMessageActionItem) => {
if (item.action === 'close') {
setVisible(false)
return
}
const linkData = normalizeLinkData(item.data)
if (!linkData)
return
const target = linkData.target ?? '_blank'
if (target === '_self') {
window.location.assign(linkData.href)
return
}
window.open(linkData.href, target, linkData.rel ?? 'noopener,noreferrer')
}
if (!visible)
return null
return (
<div
className={cn(
'fixed bottom-3 right-3 z-50 w-[360px] overflow-hidden rounded-xl border border-black/5 bg-background-default shadow-2xl',
className,
)}
>
<div className="flex min-h-[128px] flex-col justify-end gap-0.5 px-4 pb-3 pt-6 text-white" style={headerStyle}>
<div className="text-[20px] font-bold leading-6">
{title}
</div>
<div className="text-[14px] font-normal leading-5 text-white/95">
{subtitle}
</div>
</div>
<div className="markdown-body px-4 pb-2 pt-4 text-[14px] leading-5 text-text-secondary">
<MarkdownWithDirective markdown={main} />
</div>
<div className="flex items-center justify-end gap-2 px-4 pb-4 pt-2">
{actions.map(item => (
<Button
key={`${item.type}-${item.action}-${item.text}`}
variant={item.type === 'primary' ? 'primary' : 'ghost'}
size="small"
className={cn(item.type === 'default' && 'text-text-secondary')}
onClick={() => handleAction(item)}
>
{item.text}
</Button>
))}
</div>
</div>
)
}
export default InSiteMessage

View File

@ -9,7 +9,7 @@ type WithIconItemProps = WithIconCardItemProps & {
function WithIconCardItem({ icon, children }: WithIconItemProps) {
return (
<div className="flex h-11 items-center space-x-3 rounded-lg bg-background-section px-2">
<Image src={icon} className="!border-none object-contain" alt="icon" width={40} height={40} />
<Image src={decodeURIComponent(icon)} className="!border-none object-contain" alt="icon" width={40} height={40} />
<div className="min-w-0 grow overflow-hidden text-text-secondary system-sm-medium [&_p]:!m-0 [&_p]:block [&_p]:w-full [&_p]:overflow-hidden [&_p]:text-ellipsis [&_p]:whitespace-nowrap">
{children}
</div>