mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
feat(insight): polish insight page UI
- Add share card theme selection (light/dark) with contextual export controls - Update heatmap colors to GitHub green palette and fix time ranges - Limit bar charts to 10 items, use full Qwen Code name Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
f47bef1ded
commit
eea5daae74
6 changed files with 466 additions and 171 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Header, StatsRow } from './Header';
|
||||
import { StatsRow } from './Header';
|
||||
import {
|
||||
AtAGlance,
|
||||
NavToc,
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
FutureOpportunities,
|
||||
MemorableMoment,
|
||||
} from './Qualitative';
|
||||
import { ShareCard } from './ShareCard';
|
||||
import { ShareCard, type Theme } from './ShareCard';
|
||||
import './styles.css';
|
||||
import { InsightData } from './types';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
|
@ -20,6 +20,62 @@ import React from 'react';
|
|||
|
||||
// Main App Component
|
||||
function InsightApp({ data }: { data: InsightData }) {
|
||||
const [cardTheme, setCardTheme] = useState<Theme>('dark');
|
||||
const pendingExport = useRef(false);
|
||||
|
||||
const performExport = async () => {
|
||||
const card = document.getElementById('share-card');
|
||||
if (!card || !window.html2canvas) {
|
||||
alert('Export functionality is not available.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const clone = card.cloneNode(true) as HTMLElement;
|
||||
clone.style.position = 'fixed';
|
||||
clone.style.left = '-9999px';
|
||||
clone.style.top = '0';
|
||||
clone.style.pointerEvents = 'none';
|
||||
document.body.appendChild(clone);
|
||||
|
||||
const canvas = await window.html2canvas(clone, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
width: 1200,
|
||||
height: clone.scrollHeight,
|
||||
});
|
||||
|
||||
document.body.removeChild(clone);
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const link = document.createElement('a');
|
||||
link.href = imgData;
|
||||
link.download = `qwen-insights-card-${new Date().toISOString().slice(0, 10)}.png`;
|
||||
link.click();
|
||||
} catch (error) {
|
||||
console.error('Export card error:', error);
|
||||
alert('Failed to export card. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
// Export after React re-renders the card with the new theme
|
||||
useEffect(() => {
|
||||
if (pendingExport.current) {
|
||||
pendingExport.current = false;
|
||||
performExport();
|
||||
}
|
||||
}, [cardTheme]);
|
||||
|
||||
const handleExportWithTheme = (theme: Theme) => {
|
||||
if (theme === cardTheme) {
|
||||
performExport();
|
||||
} else {
|
||||
pendingExport.current = true;
|
||||
setCardTheme(theme);
|
||||
}
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="text-center text-slate-600">
|
||||
|
|
@ -42,10 +98,22 @@ function InsightApp({ data }: { data: InsightData }) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="header-with-action">
|
||||
<Header data={data} dateRangeStr={dateRangeStr} />
|
||||
<ShareButton />
|
||||
</div>
|
||||
{/* Elegant Header */}
|
||||
<header className="insights-header">
|
||||
<div className="header-content">
|
||||
<div className="header-title-section">
|
||||
<h1 className="header-title">Qwen Code Insights</h1>
|
||||
<p className="header-subtitle">
|
||||
{data.totalMessages
|
||||
? `${data.totalMessages.toLocaleString()} messages across ${data.totalSessions?.toLocaleString()} sessions`
|
||||
: 'Your personalized coding journey and patterns'}
|
||||
{dateRangeStr && ` · ${dateRangeStr}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ExportCardButton onExport={handleExportWithTheme} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{data.qualitative && (
|
||||
<>
|
||||
|
|
@ -90,73 +158,118 @@ function InsightApp({ data }: { data: InsightData }) {
|
|||
</>
|
||||
)}
|
||||
|
||||
<ShareCard data={data} />
|
||||
<ShareCard data={data} theme={cardTheme} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Share Button Component
|
||||
function ShareButton() {
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
// Export Card Button with theme dropdown
|
||||
function ExportCardButton({ onExport }: { onExport: (theme: Theme) => void }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleExport = async () => {
|
||||
const card = document.getElementById('share-card');
|
||||
if (!card || !window.html2canvas) {
|
||||
alert('Export functionality is not available.');
|
||||
return;
|
||||
}
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (
|
||||
wrapperRef.current &&
|
||||
!wrapperRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, [isOpen]);
|
||||
|
||||
setIsExporting(true);
|
||||
try {
|
||||
// Clone the card off-screen so it renders but isn't visible
|
||||
const clone = card.cloneNode(true) as HTMLElement;
|
||||
clone.style.position = 'fixed';
|
||||
clone.style.left = '-9999px';
|
||||
clone.style.top = '0';
|
||||
clone.style.pointerEvents = 'none';
|
||||
document.body.appendChild(clone);
|
||||
|
||||
const canvas = await window.html2canvas(clone, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
width: 1200,
|
||||
height: clone.scrollHeight,
|
||||
});
|
||||
|
||||
document.body.removeChild(clone);
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const link = document.createElement('a');
|
||||
link.href = imgData;
|
||||
link.download = `qwen-insights-card-${new Date().toISOString().slice(0, 10)}.png`;
|
||||
link.click();
|
||||
} catch (error) {
|
||||
console.error('Export card error:', error);
|
||||
alert('Failed to export card. Please try again.');
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
const handleSelect = (theme: Theme) => {
|
||||
setIsOpen(false);
|
||||
onExport(theme);
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={handleExport} disabled={isExporting} className="share-btn">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
||||
<polyline points="16 6 12 2 8 6" />
|
||||
<line x1="12" y1="2" x2="12" y2="15" />
|
||||
</svg>
|
||||
{isExporting ? 'Exporting...' : 'Share as Card'}
|
||||
</button>
|
||||
<div className="export-dropdown-wrapper" ref={wrapperRef}>
|
||||
<button className="export-card-btn" onClick={() => setIsOpen(!isOpen)}>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
||||
<polyline points="16 6 12 2 8 6" />
|
||||
<line x1="12" y1="2" x2="12" y2="15" />
|
||||
</svg>
|
||||
<span>Export Card</span>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={`export-chevron ${isOpen ? 'open' : ''}`}
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="export-dropdown">
|
||||
<button
|
||||
className="export-dropdown-item"
|
||||
onClick={() => handleSelect('light')}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
<line x1="12" y1="21" x2="12" y2="23" />
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||
<line x1="1" y1="12" x2="3" y2="12" />
|
||||
<line x1="21" y1="12" x2="23" y2="12" />
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</svg>
|
||||
<span>Light Theme</span>
|
||||
</button>
|
||||
<button
|
||||
className="export-dropdown-item"
|
||||
onClick={() => handleSelect('dark')}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
<span>Dark Theme</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,14 +49,14 @@ export function ActiveHoursChart({
|
|||
},
|
||||
{
|
||||
label: 'Evening',
|
||||
time: '18:00 - 00:00',
|
||||
hours: [18, 19, 20, 21, 22, 23],
|
||||
time: '18:00 - 22:00',
|
||||
hours: [18, 19, 20, 21],
|
||||
color: '#6366f1', // indigo-500
|
||||
},
|
||||
{
|
||||
label: 'Night',
|
||||
time: '00:00 - 06:00',
|
||||
hours: [0, 1, 2, 3, 4, 5],
|
||||
time: '22:00 - 06:00',
|
||||
hours: [22, 23, 0, 1, 2, 3, 4, 5],
|
||||
color: '#475569', // slate-600
|
||||
},
|
||||
];
|
||||
|
|
@ -127,15 +127,16 @@ export function HeatmapSection({
|
|||
|
||||
return (
|
||||
<div className={`${cardClass} mt-4 md:mt-6`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mb-3">
|
||||
<h3 className={sectionTitleClass}>Activity Heatmap</h3>
|
||||
<span className="text-xs font-semibold text-slate-500">Past year</span>
|
||||
<p className="text-xs text-slate-500">Showing past year of activity</p>
|
||||
</div>
|
||||
<div className="heatmap-container">
|
||||
<div className="min-w-[720px] rounded-xl bg-white/70">
|
||||
<ActivityHeatmap heatmapData={heatmap} />
|
||||
</div>
|
||||
</div>
|
||||
<HeatmapLegend />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -147,7 +148,7 @@ function ActivityHeatmap({
|
|||
heatmapData: Record<string, number>;
|
||||
}) {
|
||||
const width = 1000;
|
||||
const height = 150;
|
||||
const height = 130;
|
||||
const cellSize = 14;
|
||||
const cellPadding = 2;
|
||||
|
||||
|
|
@ -164,7 +165,8 @@ function ActivityHeatmap({
|
|||
}
|
||||
|
||||
const colorLevels = [0, 2, 4, 10, 20];
|
||||
const colors = ['#e2e8f0', '#a5d8ff', '#74c0fc', '#339af0', '#1c7ed6'];
|
||||
// GitHub contribution graph color palette (green)
|
||||
const colors = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'];
|
||||
|
||||
function getColor(value: number) {
|
||||
if (value === 0) return colors[0];
|
||||
|
|
@ -270,34 +272,29 @@ function ActivityHeatmap({
|
|||
{label.text}
|
||||
</text>
|
||||
))}
|
||||
|
||||
{/* Render legend */}
|
||||
<text x={startX} y={height - 40} fontSize="12" fill="#64748b">
|
||||
Less
|
||||
</text>
|
||||
{colors.map((color, index) => {
|
||||
const legendX = startX + 40 + index * (cellSize + 2);
|
||||
return (
|
||||
<rect
|
||||
key={index}
|
||||
x={legendX}
|
||||
y={height - 30}
|
||||
width="10"
|
||||
height="10"
|
||||
rx="2"
|
||||
fill={color}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<text
|
||||
x={startX + 40 + colors.length * (cellSize + 2) + 5}
|
||||
y={height - 21}
|
||||
fontSize="12"
|
||||
fill="#64748b"
|
||||
width={cellSize}
|
||||
>
|
||||
More
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Heatmap Legend Component (outside SVG)
|
||||
function HeatmapLegend() {
|
||||
const colors = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<span className="text-xs text-slate-500">Less</span>
|
||||
{colors.map((color, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block rounded"
|
||||
style={{
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<span className="text-xs text-slate-500">More</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function NavToc() {
|
|||
return (
|
||||
<nav className="nav-toc">
|
||||
<a href="#section-work">What You Work On</a>
|
||||
<a href="#section-usage">How You Use QC</a>
|
||||
<a href="#section-usage">How You Use Qwen Code</a>
|
||||
<a href="#section-wins">Impressive Things</a>
|
||||
<a href="#section-friction">Where Things Go Wrong</a>
|
||||
<a href="#section-features">Features to Try</a>
|
||||
|
|
@ -283,6 +283,9 @@ function HorizontalBarChart({
|
|||
}
|
||||
entries.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
// Limit to at most 10 items
|
||||
entries = entries.slice(0, 10);
|
||||
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
const maxValue = Math.max(...entries.map(([, count]) => count));
|
||||
|
|
@ -569,7 +572,7 @@ export function Improvements({
|
|||
id="section-features"
|
||||
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
|
||||
>
|
||||
Existing QC Features to Try
|
||||
Existing Qwen Code Features to Try
|
||||
</h2>
|
||||
|
||||
{/* QWEN.md Additions */}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,63 @@
|
|||
import React from 'react';
|
||||
import { InsightData } from './types';
|
||||
|
||||
/**
|
||||
* Theme configuration for the share card
|
||||
*/
|
||||
export type Theme = 'light' | 'dark';
|
||||
|
||||
interface ThemeConfig {
|
||||
background: string;
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
textMuted: string;
|
||||
cardBackground: string;
|
||||
cardBackgroundSecondary: string;
|
||||
borderColor: string;
|
||||
heatmapColors: string[];
|
||||
heatmapEmpty: string;
|
||||
}
|
||||
|
||||
const themes: Record<Theme, ThemeConfig> = {
|
||||
light: {
|
||||
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
|
||||
textPrimary: '#0f172a',
|
||||
textSecondary: '#475569',
|
||||
textMuted: '#64748b',
|
||||
cardBackground: 'rgba(255,255,255,0.7)',
|
||||
cardBackgroundSecondary: 'rgba(255,255,255,0.5)',
|
||||
borderColor: '#e2e8f0',
|
||||
// GitHub contribution graph color palette (light mode)
|
||||
heatmapColors: ['#9be9a8', '#40c463', '#30a14e', '#216e39'],
|
||||
heatmapEmpty: '#ebedf0',
|
||||
},
|
||||
dark: {
|
||||
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
|
||||
textPrimary: '#f8fafc',
|
||||
textSecondary: '#e2e8f0',
|
||||
textMuted: '#94a3b8',
|
||||
cardBackground: 'rgba(255,255,255,0.05)',
|
||||
cardBackgroundSecondary: 'rgba(255,255,255,0.04)',
|
||||
borderColor: 'rgba(255,255,255,0.08)',
|
||||
// GitHub contribution graph color palette (dark mode)
|
||||
heatmapColors: ['#0e4429', '#006d32', '#26a641', '#39d353'],
|
||||
heatmapEmpty: '#2d333b',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A hidden 1200x675 card optimized for Twitter/X sharing.
|
||||
* Rendered off-screen; captured by html2canvas when the user clicks "Share as Card".
|
||||
*/
|
||||
export function ShareCard({ data }: { data: InsightData }) {
|
||||
export function ShareCard({
|
||||
data,
|
||||
theme = 'light',
|
||||
}: {
|
||||
data: InsightData;
|
||||
theme?: Theme;
|
||||
}) {
|
||||
const t = themes[theme];
|
||||
|
||||
const {
|
||||
totalMessages = 0,
|
||||
totalSessions = 0,
|
||||
|
|
@ -38,15 +90,15 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
const truncatedHeadline = data.qualitative?.memorableMoment?.headline ?? null;
|
||||
|
||||
// Mini heatmap: last 52 weeks (simplified 7-row grid)
|
||||
const miniHeatmap = buildMiniHeatmap(data.heatmap || {});
|
||||
const miniHeatmap = buildMiniHeatmap(data.heatmap || {}, t);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="share-card"
|
||||
style={{
|
||||
width: '1200px',
|
||||
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
|
||||
color: '#f8fafc',
|
||||
background: t.background,
|
||||
color: t.textPrimary,
|
||||
fontFamily:
|
||||
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
|
||||
display: 'flex',
|
||||
|
|
@ -82,7 +134,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
color: '#94a3b8',
|
||||
color: t.textMuted,
|
||||
marginTop: '6px',
|
||||
}}
|
||||
>
|
||||
|
|
@ -92,7 +144,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: '#64748b',
|
||||
color: t.textMuted,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.15em',
|
||||
paddingTop: '8px',
|
||||
|
|
@ -111,16 +163,17 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
marginBottom: '32px',
|
||||
}}
|
||||
>
|
||||
<StatBox value={String(totalMessages)} label="Messages" />
|
||||
<StatBox value={String(totalSessions)} label="Sessions" />
|
||||
<StatBox value={String(totalMessages)} label="Messages" theme={t} />
|
||||
<StatBox value={String(totalSessions)} label="Sessions" theme={t} />
|
||||
<StatBox
|
||||
value={`+${totalLinesAdded}/-${totalLinesRemoved}`}
|
||||
label="Lines Changed"
|
||||
small
|
||||
theme={t}
|
||||
/>
|
||||
<StatBox value={String(totalFiles)} label="Files" />
|
||||
<StatBox value={`${currentStreak}d`} label="Streak" />
|
||||
<StatBox value={`${longestStreak}d`} label="Best Streak" />
|
||||
<StatBox value={String(totalFiles)} label="Files" theme={t} />
|
||||
<StatBox value={`${currentStreak}d`} label="Streak" theme={t} />
|
||||
<StatBox value={`${longestStreak}d`} label="Best Streak" theme={t} />
|
||||
</div>
|
||||
|
||||
{/* Body: Heatmap + Tools + Moment */}
|
||||
|
|
@ -135,7 +188,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
{/* Left: Mini Heatmap */}
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
background: t.cardBackground,
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
|
|
@ -146,7 +199,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: '#94a3b8',
|
||||
color: t.textMuted,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
marginBottom: '12px',
|
||||
|
|
@ -164,6 +217,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
>
|
||||
<MiniHeatmapGrid cells={miniHeatmap} />
|
||||
</div>
|
||||
<MiniHeatmapLegend theme={t} />
|
||||
</div>
|
||||
|
||||
{/* Right: Active Hours + Moment */}
|
||||
|
|
@ -177,7 +231,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
{/* Active Hours */}
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
background: t.cardBackground,
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
|
|
@ -188,7 +242,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: '#94a3b8',
|
||||
color: t.textMuted,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
marginBottom: '12px',
|
||||
|
|
@ -205,14 +259,14 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<ActiveHoursChart activeHours={activeHours} />
|
||||
<ActiveHoursChart activeHours={activeHours} theme={t} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Pattern + Memorable Moment */}
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
background: t.cardBackgroundSecondary,
|
||||
borderRadius: '12px',
|
||||
padding: '16px 16px',
|
||||
position: 'relative',
|
||||
|
|
@ -225,7 +279,10 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
left: '12px',
|
||||
fontSize: '64px',
|
||||
fontWeight: 700,
|
||||
color: 'rgba(99,102,241,0.2)',
|
||||
color:
|
||||
theme === 'light'
|
||||
? 'rgba(99,102,241,0.15)'
|
||||
: 'rgba(99,102,241,0.2)',
|
||||
lineHeight: 1,
|
||||
fontFamily: 'Georgia, "Times New Roman", serif',
|
||||
userSelect: 'none',
|
||||
|
|
@ -244,7 +301,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: '#e2e8f0',
|
||||
color: t.textSecondary,
|
||||
lineHeight: 1.6,
|
||||
marginBottom: truncatedHeadline ? '8px' : 0,
|
||||
}}
|
||||
|
|
@ -256,7 +313,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: '#94a3b8',
|
||||
color: t.textMuted,
|
||||
lineHeight: 1.5,
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
|
|
@ -277,14 +334,14 @@ export function ShareCard({ data }: { data: InsightData }) {
|
|||
alignItems: 'center',
|
||||
marginTop: 'auto',
|
||||
paddingTop: '24px',
|
||||
borderTop: '1px solid rgba(255,255,255,0.08)',
|
||||
borderTop: `1px solid ${t.borderColor}`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '12px', color: '#64748b' }}>
|
||||
<div style={{ fontSize: '12px', color: t.textMuted }}>
|
||||
Generated by Qwen Code · {new Date().toISOString().split('T')[0]}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#64748b' }}>
|
||||
<div style={{ fontSize: '12px', color: t.textMuted }}>
|
||||
github.com/QwenLM/qwen-code
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -296,10 +353,12 @@ function StatBox({
|
|||
value,
|
||||
label,
|
||||
small,
|
||||
theme,
|
||||
}: {
|
||||
value: string;
|
||||
label: string;
|
||||
small?: boolean;
|
||||
theme: ThemeConfig;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
|
|
@ -307,7 +366,7 @@ function StatBox({
|
|||
style={{
|
||||
fontSize: small ? '18px' : '28px',
|
||||
fontWeight: 700,
|
||||
color: '#f8fafc',
|
||||
color: theme.textPrimary,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
|
|
@ -316,7 +375,7 @@ function StatBox({
|
|||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: '#64748b',
|
||||
color: theme.textMuted,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
marginTop: '4px',
|
||||
|
|
@ -330,8 +389,10 @@ function StatBox({
|
|||
|
||||
function ActiveHoursChart({
|
||||
activeHours,
|
||||
theme,
|
||||
}: {
|
||||
activeHours: { [hour: number]: number };
|
||||
theme: ThemeConfig;
|
||||
}) {
|
||||
const phases = [
|
||||
{
|
||||
|
|
@ -348,14 +409,14 @@ function ActiveHoursChart({
|
|||
},
|
||||
{
|
||||
label: 'Evening',
|
||||
time: '18–00',
|
||||
hours: [18, 19, 20, 21, 22, 23],
|
||||
time: '18–22',
|
||||
hours: [18, 19, 20, 21],
|
||||
color: '#6366f1',
|
||||
},
|
||||
{
|
||||
label: 'Night',
|
||||
time: '00–06',
|
||||
hours: [0, 1, 2, 3, 4, 5],
|
||||
time: '22–06',
|
||||
hours: [22, 23, 0, 1, 2, 3, 4, 5],
|
||||
color: '#475569',
|
||||
},
|
||||
];
|
||||
|
|
@ -394,21 +455,21 @@ function ActiveHoursChart({
|
|||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: '#e2e8f0', fontWeight: 500 }}>
|
||||
<span style={{ color: theme.textSecondary, fontWeight: 500 }}>
|
||||
{item.label}
|
||||
</span>
|
||||
<span style={{ color: '#64748b', fontSize: '11px' }}>
|
||||
<span style={{ color: theme.textMuted, fontSize: '11px' }}>
|
||||
{item.time}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ color: '#94a3b8', fontWeight: 600 }}>
|
||||
<span style={{ color: theme.textMuted, fontWeight: 600 }}>
|
||||
{item.total}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: '6px',
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
background: theme.borderColor,
|
||||
borderRadius: '3px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
|
|
@ -432,6 +493,7 @@ function ActiveHoursChart({
|
|||
/** Build a 7x~26 grid of intensity values for the mini heatmap (last ~6 months). */
|
||||
function buildMiniHeatmap(
|
||||
heatmap: Record<string, number>,
|
||||
theme: ThemeConfig,
|
||||
): { color: string }[] {
|
||||
const today = new Date();
|
||||
const weeksToShow = 26;
|
||||
|
|
@ -451,20 +513,20 @@ function buildMiniHeatmap(
|
|||
while (d <= endDate) {
|
||||
const key = d.toISOString().split('T')[0];
|
||||
const val = heatmap[key] || 0;
|
||||
cells.push({ color: heatColor(val) });
|
||||
cells.push({ color: heatColor(val, theme) });
|
||||
d.setDate(d.getDate() + 1);
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
function heatColor(val: number): string {
|
||||
if (val === 0) return 'rgba(255,255,255,0.06)';
|
||||
if (val < 2) return '#1e3a5f';
|
||||
if (val < 4) return '#2563eb';
|
||||
if (val < 10) return '#3b82f6';
|
||||
if (val < 20) return '#60a5fa';
|
||||
return '#93c5fd';
|
||||
// GitHub contribution graph color palette - theme aware
|
||||
function heatColor(val: number, theme: ThemeConfig): string {
|
||||
if (val === 0) return theme.heatmapEmpty;
|
||||
if (val < 2) return theme.heatmapColors[0];
|
||||
if (val < 4) return theme.heatmapColors[1];
|
||||
if (val < 10) return theme.heatmapColors[2];
|
||||
return theme.heatmapColors[3];
|
||||
}
|
||||
|
||||
function MiniHeatmapGrid({ cells }: { cells: { color: string }[] }) {
|
||||
|
|
@ -499,3 +561,38 @@ function MiniHeatmapGrid({ cells }: { cells: { color: string }[] }) {
|
|||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Mini Heatmap Legend Component for ShareCard (theme aware)
|
||||
function MiniHeatmapLegend({ theme }: { theme: ThemeConfig }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '11px', color: theme.textMuted }}>Less</span>
|
||||
{[
|
||||
theme.heatmapEmpty,
|
||||
theme.heatmapColors[0],
|
||||
theme.heatmapColors[1],
|
||||
theme.heatmapColors[2],
|
||||
theme.heatmapColors[3],
|
||||
].map((color, index) => (
|
||||
<span
|
||||
key={index}
|
||||
style={{
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: '2px',
|
||||
backgroundColor: color,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<span style={{ fontSize: '11px', color: theme.textMuted }}>More</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1136,42 +1136,127 @@ body {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
/* Share Button */
|
||||
.share-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
/* Header Styles */
|
||||
.insights-header {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-title-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 0.375rem 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Export Card Dropdown */
|
||||
.export-dropdown-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.export-card-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
gap: 0.5rem;
|
||||
height: 36px;
|
||||
padding: 0 0.875rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
background: white;
|
||||
color: #334155;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.2);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.share-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.3);
|
||||
transform: translateY(-1px);
|
||||
.export-card-btn:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.share-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.2);
|
||||
.export-card-btn:active {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.share-btn:disabled {
|
||||
.export-chevron {
|
||||
transition: transform 0.15s ease;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.share-btn svg {
|
||||
opacity: 0.8;
|
||||
.export-chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.export-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
padding: 4px;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.export-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: none;
|
||||
color: #334155;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
|
||||
.export-dropdown-item:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.export-dropdown-item:active {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue