// Sentiment Analyst - Main Application
const { useState, useEffect, useMemo } = React;
const {
Layout,
Row,
Col,
Card,
Button,
Input,
Select,
Slider,
Table,
Tag,
Statistic,
Space,
Typography,
Popconfirm,
message,
Progress,
Empty,
Tooltip,
ConfigProvider
} = antd;
const {
GlobalOutlined,
TwitterOutlined,
FileTextOutlined,
DeleteOutlined,
SaveOutlined,
SmileOutlined,
MehOutlined,
FrownOutlined,
BarChartOutlined,
LinkOutlined,
GoogleOutlined,
DesktopOutlined,
TeamOutlined,
QuestionCircleOutlined
} = icons;
const { Header, Content } = Layout;
const { Title, Text, Paragraph } = Typography;
// Initialize dayjs locale
dayjs.locale('ja');
// Constants
const STORAGE_KEY = 'sentiment_records';
const SOURCE_OPTIONS = [
{ value: 'Yahoo', label: 'Yahoo!ニュース', color: 'red' },
{ value: 'Google', label: 'Google News', color: 'blue' },
{ value: 'SNS', label: 'SNS', color: 'cyan' },
{ value: 'TV', label: 'TV', color: 'purple' },
{ value: 'Work', label: '仕事関連', color: 'orange' },
{ value: 'Other', label: 'その他', color: 'default' }
];
const NEWS_LINKS = [
{ name: 'Yahoo!ニュース', url: 'https://news.yahoo.co.jp/topics', icon: },
{ name: 'Google News', url: 'https://news.google.com/topstories?hl=ja&gl=JP&ceid=JP:ja', icon: },
{ name: '日本経済新聞', url: 'https://www.nikkei.com/', icon: },
{ name: 'NHK NEWS', url: 'https://www3.nhk.or.jp/news/', icon: }
];
// Utility functions
const generateId = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
const getScoreColor = (score) => {
if (score > 10) return '#52c41a';
if (score < -10) return '#ff4d4f';
return '#faad14';
};
const getScoreClass = (score) => {
if (score > 10) return 'score-positive';
if (score < -10) return 'score-negative';
return 'score-neutral';
};
const getSliderClass = (score) => {
if (score > 10) return 'positive-slider';
if (score < -10) return 'negative-slider';
return 'neutral-slider';
};
const getScoreIcon = (score) => {
if (score > 10) return ;
if (score < -10) return ;
return ;
};
// Main App Component
const App = () => {
const [records, setRecords] = useState([]);
const [headline, setHeadline] = useState('');
const [source, setSource] = useState('Yahoo');
const [score, setScore] = useState(0);
const [messageApi, contextHolder] = message.useMessage();
// Load records from localStorage on mount
useEffect(() => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
setRecords(parsed);
}
} catch (error) {
console.error('Failed to load records:', error);
}
}, []);
// Save records to localStorage whenever they change
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(records));
} catch (error) {
console.error('Failed to save records:', error);
}
}, [records]);
// Calculate average score
const averageScore = useMemo(() => {
if (records.length === 0) return 0;
const sum = records.reduce((acc, r) => acc + r.score, 0);
return Math.round(sum / records.length);
}, [records]);
// Handle form submission
const handleSubmit = () => {
if (!headline.trim()) {
messageApi.warning('見出しを入力してください');
return;
}
const newRecord = {
id: generateId(),
timestamp: new Date().toISOString(),
headline: headline.trim(),
source,
score
};
setRecords(prev => [newRecord, ...prev]);
setHeadline('');
setScore(0);
messageApi.success('記録を保存しました');
};
// Handle record deletion
const handleDelete = (id) => {
setRecords(prev => prev.filter(r => r.id !== id));
messageApi.success('記録を削除しました');
};
// Table columns configuration
const columns = [
{
title: '日付',
dataIndex: 'timestamp',
key: 'timestamp',
width: 140,
render: (timestamp) => (
{dayjs(timestamp).format('MM/DD HH:mm')}
),
sorter: (a, b) => new Date(b.timestamp) - new Date(a.timestamp),
defaultSortOrder: 'descend'
},
{
title: 'ソース',
dataIndex: 'source',
key: 'source',
width: 110,
render: (source) => {
const opt = SOURCE_OPTIONS.find(o => o.value === source);
return {source};
},
filters: SOURCE_OPTIONS.map(o => ({ text: o.label, value: o.value })),
onFilter: (value, record) => record.source === value
},
{
title: '見出し',
dataIndex: 'headline',
key: 'headline',
ellipsis: {
showTitle: false,
},
render: (text) => (
{text}
)
},
{
title: 'スコア',
dataIndex: 'score',
key: 'score',
width: 140,
align: 'center',
render: (score) => (
{getScoreIcon(score)}
{score > 0 ? `+${score}` : score}
),
sorter: (a, b) => a.score - b.score
},
{
title: '',
key: 'action',
width: 70,
align: 'center',
render: (_, record) => (
handleDelete(record.id)}
okText="削除"
cancelText="キャンセル"
okButtonProps={{ danger: true }}
>
}
size="middle"
/>
)
}
];
// Slider marks
const sliderMarks = {
'-50': { style: { color: '#ff4d4f' }, label: 'Bad' },
'0': { style: { color: '#faad14' }, label: 'Neutral' },
'50': { style: { color: '#52c41a' }, label: 'Good' }
};
return (
{contextHolder}
{/* Header */}
Sentiment Analyst
{getScoreIcon(averageScore)}
0 ? '+' : ''}
valueStyle={{
color: getScoreColor(averageScore),
fontSize: 24,
fontWeight: 'bold'
}}
/>
({records.length} 件の記録)
{/* Main Content */}
{/* Left Column - News Dock */}
News Dock
}
style={{ height: '100%' }}
>
下のリンクからニュースサイトにアクセスし、気になる見出しをコピーして戻ってください。
{NEWS_LINKS.map((link) => (
))}
{/* Right Column - Analyze Console */}
Analyze Console
}
>
{/* Headline Input */}
ニュース見出し
setHeadline(e.target.value)}
placeholder="見出しをここにペースト..."
autoSize={{ minRows: 2, maxRows: 4 }}
maxLength={500}
showCount
/>
{/* Source Select */}
ソース
{/* Submit Button */}
}
onClick={handleSubmit}
size="large"
block
>
記録する
{/* Records Table */}
記録一覧
{records.length} 件
}
style={{ marginTop: 24 }}
>
{records.length === 0 ? (
上のフォームからニュースの感情分析を記録してみましょう
) : (
`全 ${total} 件`,
pageSizeOptions: ['5', '10', '20', '50']
}}
scroll={{ x: 600 }}
size="middle"
rowClassName={() => 'table-row'}
style={{ fontSize: 14 }}
/>
)}
);
};
// Render the app
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render();