✨ feat: implemented ui for references (#332)#339
✨ feat: implemented ui for references (#332)#339maifeeulasad wants to merge 5 commits intoant-design:mainfrom
Conversation
|
Tested with: const references = [
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'a' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'bc' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'bd' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'bb' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'bb' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'bb' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'bb' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'bb' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'bb' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
{ url: 'https://www.a.com', description: 'lots of meawww', title: 'bb' },
{ url: 'https://www.b.com', title: 'bb' },
{ url: 'http://woof.com', description: 'ghew ghew', title: 'dog' },
]; |
📝 WalkthroughWalkthrough该PR为聊天消息添加了参考资源渲染功能。通过类型定义、样式定义和组件更新,引入了一个新的 Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR implements a UI feature to display references/sources for chat messages, addressing issue #332. The implementation adds a new optional references field to chat messages, which displays as interactive cards below message content.
Key changes:
- Adds
ChatReferenceinterface withtitle,url, and optionaldescriptionfields - Threads the
referencesprop through the component hierarchy (ChatList → ChatItem → MessageContent) - Implements a card-based grid layout with clickable icons to open reference URLs in new tabs
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/message.ts | Defines the ChatReference interface and adds optional references field to ChatMessage |
| src/ChatItem/type.ts | Adds references prop to ChatItemProps interface |
| src/ChatList/index.tsx | Extracts and passes references from message items to ChatItem |
| src/ChatItem/index.tsx | Forwards references prop to MessageContent component |
| src/ChatItem/style.ts | Adds CSS styling for the references container with flex layout and horizontal scrolling |
| src/ChatItem/components/MessageContent.tsx | Implements the references UI with Card grid, ExternalLink icon component, and rendering logic |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return ( | ||
| <> | ||
| <div> | ||
| <strong>References:</strong> |
There was a problem hiding this comment.
The text "References:" is hardcoded in English, which breaks internationalization support for this component. The codebase appears to support multiple languages (Chinese comments exist throughout). Consider extracting this string to a localization system or using a translation key to support multiple languages.
| </div> | ||
| <div className={`${cx(styles.messageReferences, `${prefixClass}-message-references`)}`}> | ||
| <List | ||
| grid={{ gutter: 16, column: 4 }} |
There was a problem hiding this comment.
The fixed 4-column grid layout will not work well on mobile or smaller screens, potentially causing layout issues or very narrow cards. Consider using responsive grid configurations, such as { gutter: 16, xs: 1, sm: 2, md: 3, lg: 4 } to adjust the number of columns based on screen size for better mobile experience.
| grid={{ gutter: 16, column: 4 }} | |
| grid={{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 4 }} |
| interface ExternalLinkProps { | ||
| url: string; | ||
| } | ||
|
|
||
| const ExternalLink: React.FC<ExternalLinkProps> = ({ url }) => { | ||
| const handleClick = () => { | ||
| window.open(url, '_blank', 'noopener noreferrer'); | ||
| }; | ||
|
|
||
| return ( | ||
| <Tooltip title={`Open in new tab: ${url}`}> | ||
| <InfoCircleOutlined | ||
| style={{ | ||
| cursor: 'pointer', | ||
| transition: 'color 0.3s ease', | ||
| }} | ||
| onClick={handleClick} | ||
| /> | ||
| </Tooltip> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
The ExternalLink component is defined inside the MessageContent file but outside the main component. While this prevents re-creation on each render, it would be better organized in a separate file (e.g., components/ExternalLink.tsx) for better code maintainability and reusability, following the component structure pattern used in this codebase.
| renderItem={({ title, description, url }) => ( | ||
| <List.Item> | ||
| <Card hoverable title={title} extra={<ExternalLink url={url} />}> | ||
| {description} | ||
| </Card> | ||
| </List.Item> | ||
| )} |
There was a problem hiding this comment.
The Card component will render an empty body when description is undefined or empty, which may look odd. Consider adding a condition to handle this case, such as only rendering the Card body content when description exists, or providing a fallback message like "No description available".
| renderItem={({ title, description, url }) => ( | |
| <List.Item> | |
| <Card hoverable title={title} extra={<ExternalLink url={url} />}> | |
| {description} | |
| </Card> | |
| </List.Item> | |
| )} | |
| renderItem={({ title, description, url }) => { | |
| const hasDescription = | |
| typeof description === 'string' && description.trim().length > 0; | |
| return ( | |
| <List.Item> | |
| <Card hoverable title={title} extra={<ExternalLink url={url} />}> | |
| {hasDescription ? description : 'No description available'} | |
| </Card> | |
| </List.Item> | |
| ); | |
| }} |
| ); | ||
| }; | ||
|
|
||
| const referencesContent = useMemo(() => renderReferences(), [references]); |
There was a problem hiding this comment.
The useMemo hook is missing cx, styles, and prefixClass in its dependency array. The renderReferences function uses these values but they're not listed as dependencies, which could lead to stale closures and incorrect rendering. Add these dependencies to ensure the memoized value updates correctly when these values change.
| const referencesContent = useMemo(() => renderReferences(), [references]); | |
| const referencesContent = useMemo(() => renderReferences(), [references, cx, styles, prefixClass]); |
| <InfoCircleOutlined | ||
| style={{ | ||
| cursor: 'pointer', | ||
| transition: 'color 0.3s ease', | ||
| }} | ||
| onClick={handleClick} | ||
| /> |
There was a problem hiding this comment.
The ExternalLink icon is only clickable via mouse and lacks keyboard accessibility. Users navigating with keyboard cannot open these links. Add proper keyboard event handling (onKeyDown/onKeyPress for Enter/Space keys) and set tabIndex={0} to make the icon keyboard accessible and meet WCAG guidelines.
| messageReferences: css` | ||
| margin-top: 8px; | ||
| display: flex; | ||
| overflow-x: scroll; |
There was a problem hiding this comment.
The overflow-x: scroll property always shows a horizontal scrollbar even when content doesn't overflow, which creates unnecessary visual clutter. Use overflow-x: auto instead to only show the scrollbar when the content actually overflows the container width.
| overflow-x: scroll; | |
| overflow-x: auto; |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/ChatItem/index.tsx (1)
77-116: 在 useMemo 依赖数组中缺少references
messageContentDom的useMemo钩子在 JSX 中使用了references(第100行),但依赖数组(第104-116行)中缺少该依赖项。这会导致当references更新时,记忆化的 DOM 不会重新计算,用户界面将显示过时的引用数据。🔧 建议的修复
}, [ error, message, messageExtra, renderMessage, placement, primary, text, type, editing, errorMessage, originData, + references, ]);src/ChatList/index.tsx (1)
98-98: 移除调试用的 console.log 语句代码中遗留了调试用的
console.log语句,应在合并到生产环境之前移除。🐛 建议的修复
- console.log('renderItems', renderItems); - const historyLength = data.length;
🧹 Nitpick comments (4)
src/ChatList/index.tsx (1)
106-123: 可以简化 references 的传递第106行的中间变量
references是冗余的,可以直接在第123行传递item.references,使代码更简洁。♻️ 建议的重构
- const references = item.references; - return ( <ShouldUpdateItem key={item.id} {...itemProps} {...item} shouldUpdate={itemShouldUpdate} > <Fragment> <HistoryDivider enable={enableHistoryDivider} text={text?.history} /> <ChatItem {...itemProps} {...item} originData={item} chatItemRenderConfig={chatItemRenderConfig} markdownProps={markdownProps} - references={references} + references={item.references} />src/ChatItem/style.ts (1)
176-182: 建议使用overflow-x: auto替代overflow-x: scroll当前使用
overflow-x: scroll会在某些浏览器上始终显示滚动条,即使内容未溢出。使用overflow-x: auto可以只在需要时显示滚动条,提供更好的用户体验。♻️ 建议的改进
messageReferences: css` margin-top: 8px; display: flex; - overflow-x: scroll; + overflow-x: auto; width: 100%; gap: 8px; `,src/ChatItem/components/MessageContent.tsx (2)
99-99: 可选的代码优化建议当前的
useMemo模式是有效的,但可以进一步优化:renderReferences函数在每次渲染时都会重新创建,然后在useMemo中调用。♻️ 可选的优化方案
方案 1:直接内联 JSX 到 useMemo
-const renderReferences = () => { - if (!references || references.length === 0) return null; - return ( - <> - <div> - <strong>References:</strong> - </div> - <div className={`${cx(styles.messageReferences, `${prefixClass}-message-references`)}`}> - <List - grid={{ gutter: 16, column: 4 }} - dataSource={references} - renderItem={({ title, description, url }) => ( - <List.Item> - <Card hoverable title={title} extra={<ExternalLink url={url} />}> - {description} - </Card> - </List.Item> - )} - /> - </div> - </> - ); -}; - -const referencesContent = useMemo(() => renderReferences(), [references]); +const referencesContent = useMemo(() => { + if (!references || references.length === 0) return null; + return ( + <> + <div> + <strong>References:</strong> + </div> + <div className={`${cx(styles.messageReferences, `${prefixClass}-message-references`)}`}> + <List + grid={{ gutter: 16, column: 4 }} + dataSource={references} + renderItem={({ title, description, url }) => ( + <List.Item> + <Card hoverable title={title} extra={<ExternalLink url={url} />}> + {description} + </Card> + </List.Item> + )} + /> + </div> + </> + ); +}, [references, cx, styles.messageReferences, prefixClass]);注意:需要将
cx、styles.messageReferences和prefixClass添加到依赖数组中。
128-128: 引用内容渲染位置合理,注意样式配置
referencesContent在组件树中的位置正确,放置在messageExtra之后。渲染逻辑通过useMemo的条件判断自动处理空值情况。不过需要注意:从
style.ts中可以看到messageReferences样式使用了overflow-x: scroll,这在某些浏览器中会始终显示滚动条。建议在样式文件中改用overflow-x: auto。
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
src/ChatItem/components/MessageContent.tsxsrc/ChatItem/index.tsxsrc/ChatItem/style.tssrc/ChatItem/type.tssrc/ChatList/index.tsxsrc/types/message.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/ChatItem/type.ts (1)
src/types/message.ts (1)
ChatReference(13-17)
src/ChatItem/components/MessageContent.tsx (2)
src/ChatItem/type.ts (1)
ChatItemProps(16-131)src/ChatItem/style.ts (1)
useStyles(3-198)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Agent
🔇 Additional comments (6)
src/types/message.ts (2)
13-17: LGTM!
ChatReference接口定义清晰合理,必填字段title和url符合引用的基本需求,可选的description字段提供了灵活性。
38-38: LGTM!为
ChatMessage添加可选的references字段是向后兼容的改动,不会影响现有代码。src/ChatItem/type.ts (1)
6-6: LGTM!正确导入了
ChatReference类型,并将可选的references属性添加到ChatItemProps接口中。类型定义清晰且与其他文件保持一致。Also applies to: 129-130
src/ChatItem/components/MessageContent.tsx (3)
2-2: 导入语句合理新增的导入(
useMemo、antd 组件和图标)都已正确使用,符合功能需求。Also applies to: 7-7, 12-12
50-50: Props 类型定义正确
references属性正确引用了ChatItemProps['references']类型,保持了类型一致性。
68-68: Props 解构正确
references属性正确地从组件 props 中解构,并在后续逻辑中使用。
| interface ExternalLinkProps { | ||
| url: string; | ||
| } | ||
|
|
||
| const ExternalLink: React.FC<ExternalLinkProps> = ({ url }) => { | ||
| const handleClick = () => { | ||
| window.open(url, '_blank', 'noopener noreferrer'); | ||
| }; | ||
|
|
||
| return ( | ||
| <Tooltip title={`Open in new tab: ${url}`}> | ||
| <InfoCircleOutlined | ||
| style={{ | ||
| cursor: 'pointer', | ||
| transition: 'color 0.3s ease', | ||
| }} | ||
| onClick={handleClick} | ||
| /> | ||
| </Tooltip> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
添加无障碍属性并优化 Tooltip 文本
当前实现存在以下问题:
InfoCircleOutlined缺少aria-label属性,屏幕阅读器无法识别其用途- Tooltip 中显示完整 URL 可能过长,影响用户体验
- 定义了
color过渡效果但未实际改变颜色
♿ 建议的改进方案
const ExternalLink: React.FC<ExternalLinkProps> = ({ url }) => {
const handleClick = () => {
window.open(url, '_blank', 'noopener noreferrer');
};
return (
- <Tooltip title={`Open in new tab: ${url}`}>
+ <Tooltip title="在新标签页中打开">
<InfoCircleOutlined
+ aria-label="打开外部链接"
style={{
cursor: 'pointer',
transition: 'color 0.3s ease',
}}
onClick={handleClick}
+ onMouseEnter={(e) => (e.currentTarget.style.color = '#1890ff')}
+ onMouseLeave={(e) => (e.currentTarget.style.color = '')}
/>
</Tooltip>
);
};Committable suggestion skipped: line range outside the PR's diff.
| const renderReferences = () => { | ||
| if (!references || references.length === 0) return null; | ||
| return ( | ||
| <> | ||
| <div> | ||
| <strong>References:</strong> | ||
| </div> | ||
| <div className={`${cx(styles.messageReferences, `${prefixClass}-message-references`)}`}> | ||
| <List | ||
| grid={{ gutter: 16, column: 4 }} | ||
| dataSource={references} | ||
| renderItem={({ title, description, url }) => ( | ||
| <List.Item> | ||
| <Card hoverable title={title} extra={<ExternalLink url={url} />}> | ||
| {description} | ||
| </Card> | ||
| </List.Item> | ||
| )} | ||
| /> | ||
| </div> | ||
| </> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
修复响应式布局问题
当前实现存在以下问题:
- 响应式设计缺失(严重):
List组件的grid配置固定为 4 列,在移动设备和平板上会导致布局问题 - 空内容处理:当
description为空时,Card主体区域会显示为空白 - 语义化标签:
<strong>References:</strong>应使用语义化的标题标签
📱 建议的响应式改进方案
const renderReferences = () => {
if (!references || references.length === 0) return null;
return (
<>
<div>
- <strong>References:</strong>
+ <h4 style={{ margin: 0, fontSize: '14px', fontWeight: 600 }}>References:</h4>
</div>
<div className={`${cx(styles.messageReferences, `${prefixClass}-message-references`)}`}>
<List
- grid={{ gutter: 16, column: 4 }}
+ grid={{
+ gutter: 16,
+ xs: 1,
+ sm: 2,
+ md: 3,
+ lg: 4,
+ xl: 4,
+ xxl: 4,
+ }}
dataSource={references}
renderItem={({ title, description, url }) => (
<List.Item>
<Card hoverable title={title} extra={<ExternalLink url={url} />}>
- {description}
+ {description || <span style={{ color: '#00000040' }}>暂无描述</span>}
</Card>
</List.Item>
)}
/>
</div>
</>
);
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const renderReferences = () => { | |
| if (!references || references.length === 0) return null; | |
| return ( | |
| <> | |
| <div> | |
| <strong>References:</strong> | |
| </div> | |
| <div className={`${cx(styles.messageReferences, `${prefixClass}-message-references`)}`}> | |
| <List | |
| grid={{ gutter: 16, column: 4 }} | |
| dataSource={references} | |
| renderItem={({ title, description, url }) => ( | |
| <List.Item> | |
| <Card hoverable title={title} extra={<ExternalLink url={url} />}> | |
| {description} | |
| </Card> | |
| </List.Item> | |
| )} | |
| /> | |
| </div> | |
| </> | |
| ); | |
| }; | |
| const renderReferences = () => { | |
| if (!references || references.length === 0) return null; | |
| return ( | |
| <> | |
| <div> | |
| <h4 style={{ margin: 0, fontSize: '14px', fontWeight: 600 }}>References:</h4> | |
| </div> | |
| <div className={`${cx(styles.messageReferences, `${prefixClass}-message-references`)}`}> | |
| <List | |
| grid={{ | |
| gutter: 16, | |
| xs: 1, | |
| sm: 2, | |
| md: 3, | |
| lg: 4, | |
| xl: 4, | |
| xxl: 4, | |
| }} | |
| dataSource={references} | |
| renderItem={({ title, description, url }) => ( | |
| <List.Item> | |
| <Card hoverable title={title} extra={<ExternalLink url={url} />}> | |
| {description || <span style={{ color: '#00000040' }}>暂无描述</span>} | |
| </Card> | |
| </List.Item> | |
| )} | |
| /> | |
| </div> | |
| </> | |
| ); | |
| }; |
💻 变更类型 | Change Type
🔀 变更说明 | Description of Change
closes #332
📝 补充信息 | Additional Information
Here is a snap:

Open for further modification and discussion.
Summary by CodeRabbit
新功能
✏️ Tip: You can customize this high-level summary in your review settings.