/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useMemo, useCallback } from 'react'; import { Button, Tooltip, Toast, Collapse, Badge, Typography, } from '@douyinfe/semi-ui'; import { Copy, ChevronDown, ChevronUp, Zap, CheckCircle, XCircle, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { copy } from '../../helpers'; /** * SSEViewer component for displaying Server-Sent Events in an interactive format * @param {Object} props - Component props * @param {Array} props.sseData - Array of SSE messages to display * @returns {JSX.Element} Rendered SSE viewer component */ const SSEViewer = ({ sseData }) => { const { t } = useTranslation(); const [expandedKeys, setExpandedKeys] = useState([]); const [copied, setCopied] = useState(false); const parsedSSEData = useMemo(() => { if (!sseData || !Array.isArray(sseData)) { return []; } return sseData.map((item, index) => { let parsed = null; let error = null; let isDone = false; if (item === '[DONE]') { isDone = true; } else { try { parsed = typeof item === 'string' ? JSON.parse(item) : item; } catch (e) { error = e.message; } } return { index, raw: item, parsed, error, isDone, key: `sse-${index}`, }; }); }, [sseData]); const stats = useMemo(() => { const total = parsedSSEData.length; const errors = parsedSSEData.filter((item) => item.error).length; const done = parsedSSEData.filter((item) => item.isDone).length; const valid = total - errors - done; return { total, errors, done, valid }; }, [parsedSSEData]); const handleToggleAll = useCallback(() => { setExpandedKeys((prev) => { if (prev.length === parsedSSEData.length) { return []; } else { return parsedSSEData.map((item) => item.key); } }); }, [parsedSSEData]); const handleCopyAll = useCallback(async () => { try { const allData = parsedSSEData .map((item) => item.parsed ? JSON.stringify(item.parsed, null, 2) : item.raw, ) .join('\n\n'); await copy(allData); setCopied(true); Toast.success(t('已复制全部数据')); setTimeout(() => setCopied(false), 2000); } catch (err) { Toast.error(t('复制失败')); console.error('Copy failed:', err); } }, [parsedSSEData, t]); const handleCopySingle = useCallback( async (item) => { try { const textToCopy = item.parsed ? JSON.stringify(item.parsed, null, 2) : item.raw; await copy(textToCopy); Toast.success(t('已复制')); } catch (err) { Toast.error(t('复制失败')); } }, [t], ); const renderSSEItem = (item) => { if (item.isDone) { return (
{t('流式响应完成')} [DONE]
); } if (item.error) { return (
{t('解析错误')}: {item.error}
{item.raw}
); } return (
{/* JSON 格式化显示 */}
            {JSON.stringify(item.parsed, null, 2)}
          
{/* 关键信息摘要 */} {item.parsed?.choices?.[0] && (
{item.parsed.choices[0].delta?.content && ( )} {item.parsed.choices[0].delta?.reasoning_content && ( )} {item.parsed.choices[0].finish_reason && ( )} {item.parsed.usage && ( )}
)}
); }; if (!parsedSSEData || parsedSSEData.length === 0) { return (
{t('暂无SSE响应数据')}
); } return (
{/* 头部工具栏 */}
{t('SSE数据流')} {stats.errors > 0 && ( )}
{/* SSE 数据列表 */}
{parsedSSEData.map((item) => ( {item.isDone ? ( [DONE] ) : item.error ? ( {t('解析错误')} ) : ( <> {item.parsed?.id || item.parsed?.object || t('SSE 事件')} {item.parsed?.choices?.[0]?.delta && ( •{' '} {Object.keys(item.parsed.choices[0].delta) .filter((k) => item.parsed.choices[0].delta[k]) .join(', ')} )} )}
} > {renderSSEItem(item)} ))}
); }; export default SSEViewer;