mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
Redesign AI Insights panel for usefulness
- Remove broken T5 summaries (model too weak for abstractive summarization) - Focus on multi-source confirmed stories (2+ sources) - Show fast-moving and alert stories - Add stats bar: multi-source count, fast-moving count, alerts - Improve sentiment visualization with bar and overall tone - Filter out single-source noise
This commit is contained in:
@@ -4,12 +4,6 @@ import { isMobileDevice } from '@/utils';
|
|||||||
import { escapeHtml } from '@/utils/sanitize';
|
import { escapeHtml } from '@/utils/sanitize';
|
||||||
import type { ClusteredEvent } from '@/types';
|
import type { ClusteredEvent } from '@/types';
|
||||||
|
|
||||||
interface NEREntity {
|
|
||||||
text: string;
|
|
||||||
type: string;
|
|
||||||
confidence: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InsightsPanel extends Panel {
|
export class InsightsPanel extends Panel {
|
||||||
private isHidden = false;
|
private isHidden = false;
|
||||||
|
|
||||||
@@ -21,9 +15,9 @@ export class InsightsPanel extends Panel {
|
|||||||
infoTooltip: `
|
infoTooltip: `
|
||||||
<strong>AI-Powered Analysis</strong><br>
|
<strong>AI-Powered Analysis</strong><br>
|
||||||
Uses local ML models for:<br>
|
Uses local ML models for:<br>
|
||||||
• <strong>Themes</strong>: Top story clusters<br>
|
• <strong>Breaking Stories</strong>: Multi-source confirmed<br>
|
||||||
• <strong>Entities</strong>: People, orgs, locations<br>
|
|
||||||
• <strong>Sentiment</strong>: News tone analysis<br>
|
• <strong>Sentiment</strong>: News tone analysis<br>
|
||||||
|
• <strong>Velocity</strong>: Fast-moving stories<br>
|
||||||
<em>Desktop only • Models run in browser</em>
|
<em>Desktop only • Models run in browser</em>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
@@ -43,19 +37,39 @@ export class InsightsPanel extends Panel {
|
|||||||
this.showLoading();
|
this.showLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const topClusters = clusters.slice(0, 5);
|
// Filter to only important stories: multi-source OR fast-moving OR alerts
|
||||||
const titles = topClusters.map(c => c.primaryTitle);
|
const importantStories = clusters.filter(c =>
|
||||||
|
c.sourceCount >= 2 ||
|
||||||
|
(c.velocity && c.velocity.level !== 'normal') ||
|
||||||
|
c.isAlert
|
||||||
|
);
|
||||||
|
|
||||||
const [summaries, sentiments] = await Promise.all([
|
// Sort by importance: multi-source first, then velocity
|
||||||
mlWorker.summarize(titles).catch(() => null),
|
const sortedClusters = importantStories.sort((a, b) => {
|
||||||
mlWorker.classifySentiment(titles).catch(() => null),
|
// Alerts first
|
||||||
]);
|
if (a.isAlert !== b.isAlert) return a.isAlert ? -1 : 1;
|
||||||
|
// Then multi-source
|
||||||
|
if (a.sourceCount !== b.sourceCount) return b.sourceCount - a.sourceCount;
|
||||||
|
// Then by velocity
|
||||||
|
const velA = a.velocity?.sourcesPerHour ?? 0;
|
||||||
|
const velB = b.velocity?.sourcesPerHour ?? 0;
|
||||||
|
return velB - velA;
|
||||||
|
});
|
||||||
|
|
||||||
const allTitles = clusters.slice(0, 20).map(c => c.primaryTitle).join('. ');
|
// Take top 8 for sentiment analysis
|
||||||
const entitiesResult = await mlWorker.extractEntities([allTitles]).catch(() => null);
|
const importantClusters = sortedClusters.slice(0, 8);
|
||||||
const entities = entitiesResult?.[0] ?? [];
|
|
||||||
|
|
||||||
this.renderInsights(topClusters, summaries, sentiments, entities);
|
if (importantClusters.length === 0) {
|
||||||
|
this.setContent('<div class="insights-empty">No breaking or multi-source stories yet</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titles = importantClusters.map(c => c.primaryTitle);
|
||||||
|
|
||||||
|
// Only get sentiment - skip T5 summarization (too weak for real summaries)
|
||||||
|
const sentiments = await mlWorker.classifySentiment(titles).catch(() => null);
|
||||||
|
|
||||||
|
this.renderInsights(importantClusters, sentiments);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[InsightsPanel] Error:', error);
|
console.error('[InsightsPanel] Error:', error);
|
||||||
this.setContent('<div class="insights-error">Analysis failed</div>');
|
this.setContent('<div class="insights-error">Analysis failed</div>');
|
||||||
@@ -64,142 +78,127 @@ export class InsightsPanel extends Panel {
|
|||||||
|
|
||||||
private renderInsights(
|
private renderInsights(
|
||||||
clusters: ClusteredEvent[],
|
clusters: ClusteredEvent[],
|
||||||
summaries: string[] | null,
|
sentiments: Array<{ label: string; score: number }> | null
|
||||||
sentiments: Array<{ label: string; score: number }> | null,
|
|
||||||
entities: NEREntity[]
|
|
||||||
): void {
|
): void {
|
||||||
const themesHtml = this.renderThemes(clusters, summaries, sentiments);
|
|
||||||
const entitiesHtml = this.renderEntities(entities);
|
|
||||||
const sentimentOverview = this.renderSentimentOverview(sentiments);
|
const sentimentOverview = this.renderSentimentOverview(sentiments);
|
||||||
|
const breakingHtml = this.renderBreakingStories(clusters, sentiments);
|
||||||
|
const statsHtml = this.renderStats(clusters);
|
||||||
|
|
||||||
this.setContent(`
|
this.setContent(`
|
||||||
${sentimentOverview}
|
${sentimentOverview}
|
||||||
|
${statsHtml}
|
||||||
<div class="insights-section">
|
<div class="insights-section">
|
||||||
<div class="insights-section-title">TOP THEMES</div>
|
<div class="insights-section-title">BREAKING & CONFIRMED</div>
|
||||||
${themesHtml}
|
${breakingHtml}
|
||||||
</div>
|
|
||||||
<div class="insights-section">
|
|
||||||
<div class="insights-section-title">KEY ENTITIES</div>
|
|
||||||
${entitiesHtml}
|
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderThemes(
|
private renderBreakingStories(
|
||||||
clusters: ClusteredEvent[],
|
clusters: ClusteredEvent[],
|
||||||
summaries: string[] | null,
|
|
||||||
sentiments: Array<{ label: string; score: number }> | null
|
sentiments: Array<{ label: string; score: number }> | null
|
||||||
): string {
|
): string {
|
||||||
|
// Show multi-source and fast-moving stories
|
||||||
return clusters.map((cluster, i) => {
|
return clusters.map((cluster, i) => {
|
||||||
const summary = summaries?.[i];
|
|
||||||
const sentiment = sentiments?.[i];
|
const sentiment = sentiments?.[i];
|
||||||
const sentimentClass = sentiment?.label === 'negative' ? 'negative' :
|
const sentimentClass = sentiment?.label === 'negative' ? 'negative' :
|
||||||
sentiment?.label === 'positive' ? 'positive' : 'neutral';
|
sentiment?.label === 'positive' ? 'positive' : 'neutral';
|
||||||
|
|
||||||
|
const badges: string[] = [];
|
||||||
|
|
||||||
|
// Multi-source badge
|
||||||
|
if (cluster.sourceCount >= 3) {
|
||||||
|
badges.push(`<span class="insight-badge confirmed">✓ ${cluster.sourceCount} sources</span>`);
|
||||||
|
} else if (cluster.sourceCount >= 2) {
|
||||||
|
badges.push(`<span class="insight-badge multi">${cluster.sourceCount} sources</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Velocity badge
|
||||||
|
if (cluster.velocity && cluster.velocity.level !== 'normal') {
|
||||||
|
const velIcon = cluster.velocity.trend === 'rising' ? '↑' : '';
|
||||||
|
badges.push(`<span class="insight-badge velocity ${cluster.velocity.level}">${velIcon}+${cluster.velocity.sourcesPerHour}/hr</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert badge
|
||||||
|
if (cluster.isAlert) {
|
||||||
|
badges.push('<span class="insight-badge alert">⚠ ALERT</span>');
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="insight-theme">
|
<div class="insight-story">
|
||||||
<div class="insight-theme-title">
|
<div class="insight-story-header">
|
||||||
<span class="insight-sentiment-dot ${sentimentClass}"></span>
|
<span class="insight-sentiment-dot ${sentimentClass}"></span>
|
||||||
${escapeHtml(cluster.primaryTitle.slice(0, 80))}${cluster.primaryTitle.length > 80 ? '...' : ''}
|
<span class="insight-story-title">${escapeHtml(cluster.primaryTitle.slice(0, 100))}${cluster.primaryTitle.length > 100 ? '...' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
${summary ? `<div class="insight-summary">${escapeHtml(summary)}</div>` : ''}
|
${badges.length > 0 ? `<div class="insight-badges">${badges.join('')}</div>` : ''}
|
||||||
<div class="insight-meta">${cluster.sourceCount} sources</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderEntities(entities: NEREntity[]): string {
|
|
||||||
if (!entities.length) {
|
|
||||||
return '<div class="insights-empty">No entities detected</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const grouped = this.groupEntities(entities);
|
|
||||||
const sections: string[] = [];
|
|
||||||
|
|
||||||
if (grouped.PER.length > 0) {
|
|
||||||
sections.push(`
|
|
||||||
<div class="entity-group">
|
|
||||||
<span class="entity-group-label">People:</span>
|
|
||||||
${grouped.PER.slice(0, 5).map(e =>
|
|
||||||
`<span class="entity-pill person">${escapeHtml(e.text)}</span>`
|
|
||||||
).join('')}
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grouped.ORG.length > 0) {
|
|
||||||
sections.push(`
|
|
||||||
<div class="entity-group">
|
|
||||||
<span class="entity-group-label">Organizations:</span>
|
|
||||||
${grouped.ORG.slice(0, 5).map(e =>
|
|
||||||
`<span class="entity-pill organization">${escapeHtml(e.text)}</span>`
|
|
||||||
).join('')}
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grouped.LOC.length > 0) {
|
|
||||||
sections.push(`
|
|
||||||
<div class="entity-group">
|
|
||||||
<span class="entity-group-label">Locations:</span>
|
|
||||||
${grouped.LOC.slice(0, 5).map(e =>
|
|
||||||
`<span class="entity-pill location">${escapeHtml(e.text)}</span>`
|
|
||||||
).join('')}
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections.join('') || '<div class="insights-empty">No entities detected</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private groupEntities(entities: NEREntity[]): { PER: NEREntity[]; ORG: NEREntity[]; LOC: NEREntity[]; MISC: NEREntity[] } {
|
|
||||||
const grouped = { PER: [] as NEREntity[], ORG: [] as NEREntity[], LOC: [] as NEREntity[], MISC: [] as NEREntity[] };
|
|
||||||
const seen = new Set<string>();
|
|
||||||
|
|
||||||
for (const entity of entities) {
|
|
||||||
if (!entity.type || !entity.text || entity.confidence < 0.7) continue;
|
|
||||||
const key = `${entity.type}:${entity.text.toLowerCase()}`;
|
|
||||||
if (seen.has(key)) continue;
|
|
||||||
seen.add(key);
|
|
||||||
|
|
||||||
const type = entity.type.toUpperCase() as keyof typeof grouped;
|
|
||||||
if (type in grouped) {
|
|
||||||
grouped[type].push(entity);
|
|
||||||
} else {
|
|
||||||
grouped.MISC.push(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return grouped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSentimentOverview(sentiments: Array<{ label: string; score: number }> | null): string {
|
private renderSentimentOverview(sentiments: Array<{ label: string; score: number }> | null): string {
|
||||||
if (!sentiments?.length) return '';
|
if (!sentiments || sentiments.length === 0) {
|
||||||
|
return '';
|
||||||
const counts = { positive: 0, neutral: 0, negative: 0 };
|
|
||||||
for (const s of sentiments) {
|
|
||||||
if (s.label === 'positive') counts.positive++;
|
|
||||||
else if (s.label === 'negative') counts.negative++;
|
|
||||||
else counts.neutral++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const negative = sentiments.filter(s => s.label === 'negative').length;
|
||||||
|
const positive = sentiments.filter(s => s.label === 'positive').length;
|
||||||
|
const neutral = sentiments.length - negative - positive;
|
||||||
|
|
||||||
const total = sentiments.length;
|
const total = sentiments.length;
|
||||||
const dominant = counts.negative > counts.positive ? 'negative' :
|
const negPct = Math.round((negative / total) * 100);
|
||||||
counts.positive > counts.negative ? 'positive' : 'neutral';
|
const neuPct = Math.round((neutral / total) * 100);
|
||||||
|
const posPct = 100 - negPct - neuPct;
|
||||||
|
|
||||||
|
// Determine overall tone
|
||||||
|
let toneLabel = 'Mixed';
|
||||||
|
let toneClass = 'neutral';
|
||||||
|
if (negative > positive + neutral) {
|
||||||
|
toneLabel = 'Negative';
|
||||||
|
toneClass = 'negative';
|
||||||
|
} else if (positive > negative + neutral) {
|
||||||
|
toneLabel = 'Positive';
|
||||||
|
toneClass = 'positive';
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="insights-sentiment-overview ${dominant}">
|
<div class="insights-sentiment-bar">
|
||||||
<div class="sentiment-bar">
|
<div class="sentiment-bar-track">
|
||||||
<div class="sentiment-segment negative" style="width: ${(counts.negative / total) * 100}%"></div>
|
<div class="sentiment-bar-negative" style="width: ${negPct}%"></div>
|
||||||
<div class="sentiment-segment neutral" style="width: ${(counts.neutral / total) * 100}%"></div>
|
<div class="sentiment-bar-neutral" style="width: ${neuPct}%"></div>
|
||||||
<div class="sentiment-segment positive" style="width: ${(counts.positive / total) * 100}%"></div>
|
<div class="sentiment-bar-positive" style="width: ${posPct}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sentiment-labels">
|
<div class="sentiment-bar-labels">
|
||||||
<span class="negative">${counts.negative}</span>
|
<span class="sentiment-label negative">${negative}</span>
|
||||||
<span class="neutral">${counts.neutral}</span>
|
<span class="sentiment-label neutral">${neutral}</span>
|
||||||
<span class="positive">${counts.positive}</span>
|
<span class="sentiment-label positive">${positive}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sentiment-tone ${toneClass}">Overall: ${toneLabel}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderStats(clusters: ClusteredEvent[]): string {
|
||||||
|
const multiSource = clusters.filter(c => c.sourceCount >= 2).length;
|
||||||
|
const fastMoving = clusters.filter(c => c.velocity && c.velocity.level !== 'normal').length;
|
||||||
|
const alerts = clusters.filter(c => c.isAlert).length;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="insights-stats">
|
||||||
|
<div class="insight-stat">
|
||||||
|
<span class="insight-stat-value">${multiSource}</span>
|
||||||
|
<span class="insight-stat-label">Multi-source</span>
|
||||||
|
</div>
|
||||||
|
<div class="insight-stat">
|
||||||
|
<span class="insight-stat-value">${fastMoving}</span>
|
||||||
|
<span class="insight-stat-label">Fast-moving</span>
|
||||||
|
</div>
|
||||||
|
${alerts > 0 ? `
|
||||||
|
<div class="insight-stat alert">
|
||||||
|
<span class="insight-stat-value">${alerts}</span>
|
||||||
|
<span class="insight-stat-label">Alerts</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9114,37 +9114,104 @@ body.playback-mode .status-dot {
|
|||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Insight Themes */
|
/* Insights Panel - Stats */
|
||||||
.insight-theme {
|
.insights-stats {
|
||||||
padding: 6px 0;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-stat {
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-stat-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-stat-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-stat.alert .insight-stat-value {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Insights Panel - Stories */
|
||||||
|
.insight-story {
|
||||||
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-theme:last-child {
|
.insight-story:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-theme-title {
|
.insight-story-header {
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text);
|
|
||||||
margin-bottom: 2px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-summary {
|
.insight-story-title {
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
color: var(--text-dim);
|
color: var(--text);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
padding-left: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-meta {
|
.insight-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-badge {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--surface);
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
padding-left: 14px;
|
}
|
||||||
margin-top: 2px;
|
|
||||||
|
.insight-badge.confirmed {
|
||||||
|
background: rgba(74, 222, 128, 0.15);
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-badge.multi {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-badge.velocity {
|
||||||
|
background: rgba(251, 191, 36, 0.15);
|
||||||
|
color: var(--yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-badge.velocity.elevated {
|
||||||
|
background: rgba(251, 146, 60, 0.15);
|
||||||
|
color: var(--orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-badge.velocity.high {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-badge.alert {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sentiment Dots */
|
/* Sentiment Dots */
|
||||||
@@ -9153,7 +9220,7 @@ body.playback-mode .status-dot {
|
|||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: 4px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-sentiment-dot.positive {
|
.insight-sentiment-dot.positive {
|
||||||
@@ -9168,64 +9235,67 @@ body.playback-mode .status-dot {
|
|||||||
background: var(--text-dim);
|
background: var(--text-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Entity Groups */
|
/* Sentiment Bar */
|
||||||
.entity-group {
|
.insights-sentiment-bar {
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entity-group-label {
|
|
||||||
font-size: 9px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sentiment Overview */
|
|
||||||
.insights-sentiment-overview {
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-bar {
|
.sentiment-bar-track {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 4px;
|
height: 6px;
|
||||||
border-radius: 2px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-segment {
|
.sentiment-bar-negative {
|
||||||
height: 100%;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sentiment-segment.negative {
|
|
||||||
background: var(--red);
|
background: var(--red);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-segment.neutral {
|
.sentiment-bar-neutral {
|
||||||
background: var(--text-dim);
|
background: var(--text-dim);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-segment.positive {
|
.sentiment-bar-positive {
|
||||||
background: var(--green);
|
background: var(--green);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-labels {
|
.sentiment-bar-labels {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 9px;
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-labels .negative {
|
.sentiment-label.negative {
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-labels .neutral {
|
.sentiment-label.neutral {
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-labels .positive {
|
.sentiment-label.positive {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sentiment-tone {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sentiment-tone.negative {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sentiment-tone.positive {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user