import type { IssueChatComment } from '../lib/messages' import type { Issue } from '../types' import { ExternalLink, Loader2, MessageCircle, RotateCw, Send } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { onMessage, postMessage } from '../lib/vscode' export interface IssueChatPanelProps { issue: Issue | null me?: string avatars?: Record onOpenInBrowser: (url: string) => void } function formatCommentTime(isoStr: string): string { try { const d = new Date(isoStr) if (Number.isNaN(d.getTime())) return isoStr const now = new Date() const isToday = d.toDateString() === now.toDateString() const pad = (n: number) => String(n).padStart(2, '0') const time = `${pad(d.getHours())}:${pad(d.getMinutes())}` if (isToday) return time return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${time}` } catch { return isoStr } } function SystemMessagePill({ comment }: { comment: IssueChatComment }) { const timeStr = formatCommentTime(comment.createdAt) const authorPrefix = comment.author && comment.author !== '未知' ? `${comment.author} ` : '' const displayText = `${authorPrefix}${comment.systemText}` return (
{displayText} {timeStr}
) } function ChatMessageBubble({ comment, isMine, avatarSrc, }: { comment: IssueChatComment isMine: boolean avatarSrc?: string }) { const timeStr = formatCommentTime(comment.createdAt) if (isMine) { return (
{timeStr} {comment.author} {' '} (我)
{comment.body}
{avatarSrc ? ( ) : ( '我' )}
) } return (
{avatarSrc ? ( ) : ( comment.author.slice(0, 1).toUpperCase() )}
{comment.authorName ? `${comment.authorName} (${comment.author})` : comment.author} {timeStr}
{comment.body}
) } export function IssueChatPanel({ issue, me, avatars, onOpenInBrowser, }: IssueChatPanelProps) { const [comments, setComments] = useState([]) const [loading, setLoading] = useState(false) const [sending, setSending] = useState(false) const [input, setInput] = useState('') const streamEndRef = useRef(null) const issueNumber = issue?.number useEffect(() => { if (issueNumber == null) { setComments([]) setLoading(false) return } setLoading(true) postMessage({ type: 'issue/comments-query', issueNumber, }) }, [issueNumber]) useEffect(() => { const dispose = onMessage((msg) => { if (msg.type === 'issue/comments-result') { if (issueNumber != null && msg.issueNumber === issueNumber) { setLoading(false) setSending(false) setComments(msg.comments) } } }) return dispose }, [issueNumber]) useEffect(() => { streamEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [comments]) const handleRefresh = () => { if (issueNumber == null || loading) return setLoading(true) postMessage({ type: 'issue/comments-query', issueNumber, }) } const handleSend = () => { const text = input.trim() if (!text || issueNumber == null || sending) return setSending(true) postMessage({ type: 'issue/comment-post', issueNumber, body: text, }) setInput('') } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { if (e.metaKey || e.ctrlKey || !e.shiftKey) { e.preventDefault() handleSend() } } } if (!issue) { return (
按方向键或点击卡片查看讨论
) } return (
{/* 标题栏:对齐 IssueDetailPanel */}

{issue.source === 'youtrack' ? issue.externalId : `#${issue.number}`} {issue.source === 'youtrack' && ( YouTrack )} {' '} {issue.title}

{/* 评论消息流 */}
{comments.length === 0 && !loading && (
还没有讨论 在此发起对此工单的讨论,评论将双向同步至 Gitea
)} {comments.map((comment) => { if (comment.systemText) { return } const isMine = !!(me && comment.author === me) const avatarSrc = avatars?.[comment.author] || comment.avatarUrl return ( ) })}
{/* 底部输入栏 */}