diff --git a/src/apps/chat/components/message/fragments-content/BlockPartModelAnnotations.tsx b/src/apps/chat/components/message/fragments-content/BlockPartModelAnnotations.tsx
new file mode 100644
index 000000000..2f8f14836
--- /dev/null
+++ b/src/apps/chat/components/message/fragments-content/BlockPartModelAnnotations.tsx
@@ -0,0 +1,185 @@
+import * as React from 'react';
+
+import { Box, Button, List, ListItem, ListItemButton } from '@mui/joy';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+
+import { useScaledTypographySx } from '~/modules/blocks/blocks.styles';
+
+import type { ContentScaling } from '~/common/app.theme';
+import type { DVoidWebCitation } from '~/common/stores/chat/chat.fragments';
+import { AvatarDomainFavicon } from '~/common/components/AvatarDomainFavicon';
+import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
+import { TooltipOutlined } from '~/common/components/TooltipOutlined';
+import { urlExtractDomain, urlPrettyHref } from '~/common/util/urlUtils';
+
+
+// configuration
+const MAX_ICONS = 10;
+const COLOR = 'neutral';
+
+
+const styles = {
+
+ iconRowButton: {
+ minHeight: '2.5rem',
+ gap: 0.5,
+ px: 1.5,
+ border: 'none',
+ transition: 'transform 0.14s ease',
+ '&:hover': { transform: 'translateY(-1px)' } as const,
+ } as const,
+
+ citationsList: {
+ boxShadow: `inset 1px 1px 3px -3px var(--joy-palette-${COLOR}-solidBg)`,
+ borderRadius: 'sm',
+ border: '1px solid',
+ borderColor: `${COLOR}.outlinedBorder`,
+ backgroundColor: `rgb(var(--joy-palette-${COLOR}-lightChannel) / 10%)`,
+ mt: 1,
+ overflow: 'hidden',
+ } as const,
+
+ citationItem: {
+ // py: 0.75,
+ gap: 1.5,
+ borderRadius: 0,
+ borderBottom: '1px solid',
+ borderBottomColor: 'divider',
+ } as const,
+
+ citationItemLast: {
+ // py: 0.75,
+ gap: 1.5,
+ border: 'none',
+ } as const,
+
+ citationNumber: {
+ minWidth: 26,
+ textAlign: 'end',
+ // color: 'text.tertiary',
+ // fontWeight: 'lg',
+ } as const,
+
+ line: {
+ flex: 1,
+ display: 'flex',
+ alignItems: 'center',
+ gap: 1,
+ } as const,
+
+ lineContent: {
+ display: 'flex',
+ flexDirection: 'column',
+ overflow: 'hidden',
+ } as const,
+
+ lineLink: {
+ fontStyle: 'italic',
+ fontSize: 'xs',
+ opacity: 0.5,
+ } as const,
+
+} as const;
+
+
+export function BlockPartModelAnnotations(props: {
+ itemsName?: string;
+ annotations: DVoidWebCitation[];
+ contentScaling: ContentScaling;
+}) {
+
+ // state
+ const [expanded, setExpanded] = React.useState(false);
+
+ // external state
+ const scaledTypographySx = useScaledTypographySx(props.contentScaling, false, false);
+
+ // derived
+ const annotationsCount = props.annotations.length;
+ const moreIcons = annotationsCount - MAX_ICONS;
+
+ const handleToggleExpanded = React.useCallback(() => setExpanded(on => !on), []);
+
+ if (!annotationsCount)
+ return null;
+
+ return (
+
+
+ {/* Row of favicons */}
+
+
+ {/* Expanded citations list */}
+
+
+
+ {props.annotations.map((citation, index) => {
+ const domain = urlExtractDomain(citation.url);
+
+ return (
+
+
+
+ {citation.refNumber ? `[${citation.refNumber}]` : index + 1}
+
+
+
+ {expanded && }
+
+
+ {citation.title || domain}
+
+
+ {urlPrettyHref(citation.url)}
+
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/apps/chat/components/message/fragments-content/ContentFragments.tsx b/src/apps/chat/components/message/fragments-content/ContentFragments.tsx
index 0638e3564..54d105edc 100644
--- a/src/apps/chat/components/message/fragments-content/ContentFragments.tsx
+++ b/src/apps/chat/components/message/fragments-content/ContentFragments.tsx
@@ -13,6 +13,7 @@ import { BlockEdit_TextFragment } from './BlockEdit_TextFragment';
import { BlockOpEmpty } from './BlockOpEmpty';
import { BlockPartError } from './BlockPartError';
import { BlockPartImageRef } from './BlockPartImageRef';
+import { BlockPartModelAnnotations } from './BlockPartModelAnnotations';
import { BlockPartModelAux } from './BlockPartModelAux';
import { BlockPartPlaceholder } from './BlockPartPlaceholder';
import { BlockPartText_AutoBlocks } from './BlockPartText_AutoBlocks';
@@ -113,8 +114,13 @@ export function ContentFragments(props: {
const { fId, part } = fragment;
switch (part.pt) {
case 'annotations':
- // TODO: render
- return null;
+ return (
+
+ );
case 'ma':
return (