segment-comments.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. 'use client'
  2. import React, { useCallback, useState } from 'react'
  3. import { RiDeleteBinLine, RiSendPlane2Line, RiUser3Line } from '@remixicon/react'
  4. import useSWR from 'swr'
  5. import { Mention, MentionsInput } from 'react-mentions'
  6. import classNames from '@/utils/classnames'
  7. import { formatDistanceToNow } from '@/utils/time'
  8. import { useToastContext } from '@/app/components/base/toast'
  9. import { del, get, post } from '@/service/base'
  10. import styles from './segment-comments.module.css'
  11. type SegmentCommentsProps = {
  12. datasetId: string
  13. segmentId: string
  14. }
  15. type Comment = {
  16. id: string
  17. content: string
  18. account_id: string
  19. account_name: string
  20. account_avatar: string
  21. mentioned_account_ids: string[]
  22. created_at: string
  23. }
  24. const SegmentComments: React.FC<SegmentCommentsProps> = ({
  25. datasetId,
  26. segmentId,
  27. }) => {
  28. const { notify } = useToastContext()
  29. const [content, setContent] = useState('')
  30. const [mentionedIds, setMentionedIds] = useState<string[]>([])
  31. const [isSubmitting, setIsSubmitting] = useState(false)
  32. // 获取团队成员列表(用于@提及)
  33. const { data: membersData } = useSWR('/workspaces/current/members', get)
  34. const members = membersData?.accounts || []
  35. // 获取评论列表
  36. const { data: commentsData, mutate } = useSWR(
  37. `/datasets/${datasetId}/segments/${segmentId}/comments`,
  38. get,
  39. )
  40. const comments: Comment[] = commentsData?.comments || []
  41. // 提交评论
  42. const handleSubmit = useCallback(async () => {
  43. if (!content.trim() || isSubmitting)
  44. return
  45. setIsSubmitting(true)
  46. // 调试日志
  47. console.log('Submitting comment with mentioned_account_ids:', mentionedIds)
  48. console.log('Comment content:', content)
  49. try {
  50. await post(`/datasets/${datasetId}/segments/${segmentId}/comments`, {
  51. body: {
  52. content: content.trim(),
  53. mentioned_account_ids: mentionedIds,
  54. },
  55. })
  56. setContent('')
  57. setMentionedIds([])
  58. mutate() // 刷新评论列表
  59. notify({
  60. type: 'success',
  61. message: '评论发送成功',
  62. })
  63. }
  64. catch {
  65. notify({
  66. type: 'error',
  67. message: '评论发送失败',
  68. })
  69. }
  70. finally {
  71. setIsSubmitting(false)
  72. }
  73. }, [content, datasetId, segmentId, mentionedIds, mutate, notify, isSubmitting])
  74. // 删除评论
  75. const handleDelete = useCallback(async (commentId: string) => {
  76. try {
  77. await del(`/datasets/${datasetId}/segments/${segmentId}/comments/${commentId}`)
  78. mutate() // 刷新评论列表
  79. notify({
  80. type: 'success',
  81. message: '删除成功',
  82. })
  83. }
  84. catch {
  85. notify({
  86. type: 'error',
  87. message: '删除失败',
  88. })
  89. }
  90. }, [datasetId, segmentId, mutate, notify])
  91. // 处理@提及
  92. const handleMentionChange = useCallback((event: any, newValue: string, newPlainTextValue: string, mentions: any[]) => {
  93. console.log('Mention change - mentions:', mentions)
  94. console.log('Mention change - newValue:', newValue)
  95. setContent(newValue)
  96. setMentionedIds(mentions.map(m => m.id))
  97. }, [])
  98. return (
  99. <div className="border-t border-divider-subtle bg-background-section-burn">
  100. <div className="px-4 py-3">
  101. <div className="system-md-semibold mb-3 text-text-primary">
  102. 评论 ({comments.length})
  103. </div>
  104. {/* 评论输入框 */}
  105. <div className="border-components-input-border mb-4 rounded-lg border bg-components-input-bg-normal">
  106. <div className={styles.mentionsWrapper}>
  107. <MentionsInput
  108. value={content}
  109. onChange={handleMentionChange}
  110. placeholder="添加评论... 使用 @ 提及团队成员"
  111. >
  112. <Mention
  113. trigger="@"
  114. data={members.map((m: any) => ({
  115. id: m.id,
  116. display: m.name,
  117. }))}
  118. markup="@[__display__](__id__)"
  119. displayTransform={(_id, display) => `@${display}`}
  120. />
  121. </MentionsInput>
  122. </div>
  123. <div className="flex items-center justify-between border-t border-divider-subtle px-3 py-2">
  124. <div className="system-xs-regular text-text-tertiary">
  125. 使用 @ 提及团队成员
  126. </div>
  127. <button
  128. onClick={handleSubmit}
  129. disabled={!content.trim() || isSubmitting}
  130. className={classNames(
  131. 'flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
  132. content.trim() && !isSubmitting
  133. ? 'bg-util-colors-blue-blue-600 text-white hover:bg-util-colors-blue-blue-700'
  134. : 'cursor-not-allowed bg-components-button-tertiary-bg text-components-button-tertiary-text',
  135. )}
  136. >
  137. <RiSendPlane2Line className="h-4 w-4" />
  138. {isSubmitting ? '发送中...' : '发送'}
  139. </button>
  140. </div>
  141. </div>
  142. {/* 评论列表 */}
  143. <div className="space-y-3">
  144. {comments.length === 0
  145. ? (
  146. <div className="py-8 text-center">
  147. <p className="system-sm-regular text-text-quaternary">
  148. 暂无评论,快来发表第一条评论吧!
  149. </p>
  150. </div>
  151. )
  152. : (
  153. comments.map(comment => (
  154. <div
  155. key={comment.id}
  156. className="rounded-lg border border-components-panel-border bg-components-panel-bg p-3"
  157. >
  158. <div className="mb-2 flex items-start justify-between">
  159. <div className="flex items-center gap-2">
  160. {/* 头像 */}
  161. {comment.account_avatar
  162. ? (
  163. <img
  164. src={comment.account_avatar}
  165. alt={comment.account_name}
  166. className="h-6 w-6 rounded-full"
  167. />
  168. )
  169. : (
  170. <div className="flex h-6 w-6 items-center justify-center rounded-full bg-util-colors-blue-blue-100">
  171. <RiUser3Line className="h-3.5 w-3.5 text-util-colors-blue-blue-600" />
  172. </div>
  173. )}
  174. {/* 名称和时间 */}
  175. <div>
  176. <div className="system-sm-semibold text-text-primary">
  177. {comment.account_name}
  178. </div>
  179. <div className="system-xs-regular text-text-quaternary">
  180. {formatDistanceToNow(new Date(comment.created_at))}
  181. </div>
  182. </div>
  183. </div>
  184. {/* 删除按钮 */}
  185. <button
  186. onClick={() => handleDelete(comment.id)}
  187. className="flex h-6 w-6 items-center justify-center rounded hover:bg-state-destructive-hover"
  188. title="删除"
  189. >
  190. <RiDeleteBinLine className="h-3.5 w-3.5 text-text-tertiary hover:text-text-destructive" />
  191. </button>
  192. </div>
  193. {/* 评论内容 */}
  194. <div className="system-sm-regular whitespace-pre-wrap break-words text-text-secondary">
  195. {comment.content}
  196. </div>
  197. </div>
  198. ))
  199. )}
  200. </div>
  201. </div>
  202. </div>
  203. )
  204. }
  205. export default SegmentComments