Files
openclaw-memory/frontend/src/components/FileDiff.js

259 lines
7.0 KiB
JavaScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { Spin, Alert, Tag, Button, Descriptions, Space, Tooltip, Badge } from 'antd';
import {
CheckCircleOutlined,
ExclamationCircleOutlined,
SyncOutlined,
ClockCircleOutlined,
FileTextOutlined,
} from '@ant-design/icons';
import { diffLines, ChangeType } from 'diff';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import api from '../api';
const STATUS_CONFIG = {
consistent: {
color: 'success',
icon: <CheckCircleOutlined />,
text: '内容一致',
description: '本地文件与数据库内容完全相同',
},
local_newer: {
color: 'warning',
icon: <SyncOutlined spin />,
text: '本地更新',
description: '本地文件比数据库更新',
},
db_newer: {
color: 'info',
icon: <SyncOutlined spin />,
text: '数据库更新',
description: '数据库文件比本地更新',
},
conflict: {
color: 'error',
icon: <ExclamationCircleOutlined />,
text: '存在冲突',
description: '本地与数据库内容不一致',
},
};
export default function FileDiff({ filePath, lobsterId }) {
const [loading, setLoading] = useState(false);
const [diffData, setDiffData] = useState(null);
const [error, setError] = useState(null);
const loadDiff = async () => {
setLoading(true);
setError(null);
try {
const response = await api.get('/diff/', {
params: {
lobster_id,
file_path: filePath,
chunked: 'true',
},
});
if (response.data.success) {
setDiffData(response.data.data);
} else {
setError(response.data.error || '加载失败');
}
} catch (err) {
setError(err.message || '网络错误');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (filePath) {
loadDiff();
}
}, [filePath, lobsterId]);
const renderDiff = () => {
if (!diffData) return null;
const { local_content, db_content, diff, status } = diffData;
if (!local_content || !db_content) {
return (
<Alert
message="文件不存在"
description={local_content ? '数据库中不存在此文件' : '本地不存在此文件'}
type="info"
showIcon
/>
);
}
// 使用 diff 库计算行级差异
const changes = diffLines(db_content || '', local_content || '');
return (
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
<Descriptions
bordered
size="small"
column={2}
style={{ marginBottom: 16 }}
>
<Descriptions.Item label="状态" span={2}>
<Badge
status={STATUS_CONFIG[status]?.color}
text={
<Space>
{STATUS_CONFIG[status]?.icon}
{STATUS_CONFIG[status]?.text}
</Space>
}
/>
</Descriptions.Item>
{diff.lines_changed !== 0 && (
<Descriptions.Item label="变动行数" span={2}>
<Tag color={diff.lines_changed > 0 ? 'green' : 'red'}>
{diff.lines_changed > 0 ? '+' : ''}{diff.lines_changed}
</Tag>
{diff.is_truncated && (
<Tooltip title="大文件,仅显示头尾差异">
<Tag color="orange">已截断</Tag>
</Tooltip>
)}
</Descriptions.Item>
)}
<Descriptions.Item label="本地哈希" span={1}>
<code style={{ fontSize: '12px' }}>
{diffData.local_hash?.slice(0, 16)}...
</code>
</Descriptions.Item>
<Descriptions.Item label="数据库哈希" span={1}>
<code style={{ fontSize: '12px' }}>
{diffData.db_hash?.slice(0, 16)}...
</code>
</Descriptions.Item>
</Descriptions>
<div className="diff-container">
{changes.map((change, index) => {
const lineStyle = {
paddingLeft: '16px',
paddingRight: '16px',
margin: '2px 0',
fontSize: '13px',
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
lineHeight: '1.6',
};
if (change.type === ChangeType.Insert) {
return (
<div
key={index}
style={{
...lineStyle,
backgroundColor: '#e6fffb',
borderLeft: '3px solid #52c41a',
}}
>
<span style={{ color: '#52c41a', marginRight: '8px' }}>+</span>
{change.value}
</div>
);
} else if (change.type === ChangeType.Delete) {
return (
<div
key={index}
style={{
...lineStyle,
backgroundColor: '#fff1f0',
borderLeft: '3px solid #ff4d4f',
textDecoration: 'line-through',
opacity: 0.7,
}}
>
<span style={{ color: '#ff4d4f', marginRight: '8px' }}>-</span>
{change.value}
</div>
);
} else {
return (
<div key={index} style={{ ...lineStyle }}>
<span style={{ color: '#d9d9d9', marginRight: '8px' }}> </span>
{change.value}
</div>
);
}
})}
</div>
</div>
);
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '60px 0' }}>
<Spin size="large" tip="加载中..." />
</div>
);
}
if (error) {
return (
<Alert
message="加载失败"
description={error}
type="error"
showIcon
action={
<Button size="small" onClick={loadDiff}>
重试
</Button>
}
/>
);
}
if (!diffData) {
return (
<Alert
message="请选择文件"
description="点击左侧文件树中的文件查看差异"
type="info"
showIcon
/>
);
}
return (
<div className="file-diff">
<div style={{ marginBottom: 16 }}>
<Space>
<Button
size="small"
icon={<ClockCircleOutlined />}
onClick={loadDiff}
>
刷新
</Button>
</Space>
</div>
{STATUS_CONFIG[diffData.status] && (
<Alert
message={STATUS_CONFIG[diffData.status].text}
description={STATUS_CONFIG[diffData.status].description}
type={STATUS_CONFIG[diffData.status].color}
showIcon
icon={STATUS_CONFIG[diffData.status].icon}
style={{ marginBottom: 16 }}
closable
/>
)}
{renderDiff()}
</div>
);
}