diff --git a/packages/web/src/components/chat/EmbedRenderer.tsx b/packages/web/src/components/chat/EmbedRenderer.tsx new file mode 100644 index 00000000..440941b5 --- /dev/null +++ b/packages/web/src/components/chat/EmbedRenderer.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import type { Embed } from '@backspace/shared'; +import { GenericEmbed } from './embeds/GenericEmbed'; +import { VideoEmbed } from './embeds/VideoEmbed'; +import { ImageEmbed } from './embeds/ImageEmbed'; +import { RichEmbed } from './embeds/RichEmbed'; + +interface EmbedRendererProps { + embed: Embed; +} + +export function EmbedRenderer({ embed }: EmbedRendererProps) { + switch (embed.embedType) { + case 'video': + return ; + + case 'image': + return ; + + case 'audio': + return ( +
+ {embed.title && ( + + {embed.title} + + )} +
+ ); + + case 'rich': + return ; + + case 'generic': + default: + return ; + } +} diff --git a/packages/web/src/components/chat/embeds/GenericEmbed.tsx b/packages/web/src/components/chat/embeds/GenericEmbed.tsx new file mode 100644 index 00000000..586ac8bd --- /dev/null +++ b/packages/web/src/components/chat/embeds/GenericEmbed.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import type { Embed } from '@backspace/shared'; + +interface GenericEmbedProps { + embed: Embed; +} + +function hostnameFromUrl(url: string): string | null { + try { + return new URL(url).hostname.replace(/^www\./, ''); + } catch { + return null; + } +} + +export function GenericEmbed({ embed }: GenericEmbedProps) { + if (!embed.title) return null; + + const providerName = embed.provider ?? hostnameFromUrl(embed.url); + + return ( +
+
+ {providerName && ( +
+ {providerName} +
+ )} + + {embed.title} + + {embed.description && ( +
+ {embed.description} +
+ )} +
+ {embed.image && ( +
+ +
+ )} +
+ ); +} diff --git a/packages/web/src/components/chat/embeds/ImageEmbed.tsx b/packages/web/src/components/chat/embeds/ImageEmbed.tsx new file mode 100644 index 00000000..a0ff8075 --- /dev/null +++ b/packages/web/src/components/chat/embeds/ImageEmbed.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import type { Embed } from '@backspace/shared'; +import { useUIStore } from '../../../stores/uiStore'; + +interface ImageEmbedProps { + embed: Embed; +} + +export function ImageEmbed({ embed }: ImageEmbedProps) { + const openImagePreview = useUIStore((s) => s.openImagePreview); + const imageUrl = embed.image ?? embed.url; + + return ( +
+ {embed.title openImagePreview(imageUrl)} + /> +
+ ); +} diff --git a/packages/web/src/components/chat/embeds/RichEmbed.tsx b/packages/web/src/components/chat/embeds/RichEmbed.tsx new file mode 100644 index 00000000..eb0aa7f9 --- /dev/null +++ b/packages/web/src/components/chat/embeds/RichEmbed.tsx @@ -0,0 +1,90 @@ +import React, { useState } from 'react'; +import type { Embed } from '@backspace/shared'; + +interface RichEmbedProps { + embed: Embed; +} + +const PROVIDER_HEIGHTS: Record = { + spotify: 152, +}; + +function getIframeHeight(embed: Embed): number { + if (embed.height != null) return embed.height; + if (embed.provider != null) { + const providerHeight = PROVIDER_HEIGHTS[embed.provider]; + if (providerHeight != null) return providerHeight; + } + return 200; +} + +export function RichEmbed({ embed }: RichEmbedProps) { + const [isLoaded, setIsLoaded] = useState(false); + const iframeHeight = getIframeHeight(embed); + + const providerLabel = embed.provider + ? embed.provider.charAt(0).toUpperCase() + embed.provider.slice(1) + : null; + + if (!embed.embedUrl) return null; + + return ( +
+ {isLoaded ? ( +