diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index b618d35b..d0c2168a 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -29,9 +29,9 @@ import { stateMachineManager } from '@/flow_chat/state-machine'; import { SessionExecutionState } from '@/flow_chat/state-machine/types'; import './SessionsSection.scss'; -const MAX_VISIBLE_SESSIONS = 8; -const INACTIVE_WORKSPACE_COLLAPSED_SESSIONS = 3; -const INACTIVE_WORKSPACE_EXPANDED_SESSIONS = 7; +/** Top-level parent sessions shown at each expand step (children still nest under visible parents). */ +const SESSIONS_LEVEL_0 = 5; +const SESSIONS_LEVEL_1 = 10; const log = createLogger('SessionsSection'); const AGENT_SCENE: SceneTabId = 'session'; @@ -71,7 +71,7 @@ const SessionsSection: React.FC = ({ ); const [editingSessionId, setEditingSessionId] = useState(null); const [editingTitle, setEditingTitle] = useState(''); - const [showAll, setShowAll] = useState(false); + const [expandLevel, setExpandLevel] = useState<0 | 1 | 2>(0); const [openMenuSessionId, setOpenMenuSessionId] = useState(null); const [sessionMenuPosition, setSessionMenuPosition] = useState<{ top: number; left: number } | null>(null); const [runningSessionIds, setRunningSessionIds] = useState>(new Set()); @@ -111,8 +111,8 @@ const SessionsSection: React.FC = ({ }, [editingSessionId]); useEffect(() => { - setShowAll(false); - }, [workspaceId, workspacePath, isActiveWorkspace]); + setExpandLevel(0); + }, [workspaceId, workspacePath]); useEffect(() => { if (!openMenuSessionId) return; @@ -167,16 +167,11 @@ const SessionsSection: React.FC = ({ }, [sessions]); const sessionDisplayLimit = useMemo(() => { - if (isActiveWorkspace) { - return showAll || topLevelSessions.length <= MAX_VISIBLE_SESSIONS - ? topLevelSessions.length - : MAX_VISIBLE_SESSIONS; - } - - return showAll - ? Math.min(topLevelSessions.length, INACTIVE_WORKSPACE_EXPANDED_SESSIONS) - : Math.min(topLevelSessions.length, INACTIVE_WORKSPACE_COLLAPSED_SESSIONS); - }, [isActiveWorkspace, topLevelSessions.length, showAll]); + const total = topLevelSessions.length; + if (expandLevel === 2 || total <= SESSIONS_LEVEL_0) return total; + if (expandLevel === 1) return Math.min(total, SESSIONS_LEVEL_1); + return SESSIONS_LEVEL_0; + }, [topLevelSessions.length, expandLevel]); const visibleItems = useMemo(() => { const visibleParents = topLevelSessions.slice(0, sessionDisplayLimit); @@ -189,11 +184,6 @@ const SessionsSection: React.FC = ({ return out; }, [childrenByParent, sessionDisplayLimit, topLevelSessions]); - const toggleThreshold = isActiveWorkspace - ? MAX_VISIBLE_SESSIONS - : INACTIVE_WORKSPACE_COLLAPSED_SESSIONS; - const hiddenCount = Math.max(0, topLevelSessions.length - toggleThreshold); - const activeSessionId = flowChatState.activeSessionId; const handleSwitch = useCallback( @@ -493,19 +483,43 @@ const SessionsSection: React.FC = ({ }) )} - {topLevelSessions.length > toggleThreshold && ( + {topLevelSessions.length > SESSIONS_LEVEL_0 && ( )} diff --git a/src/web-ui/src/app/layout/WorkspaceBody.scss b/src/web-ui/src/app/layout/WorkspaceBody.scss index 7864fcf8..fa5a486e 100644 --- a/src/web-ui/src/app/layout/WorkspaceBody.scss +++ b/src/web-ui/src/app/layout/WorkspaceBody.scss @@ -68,6 +68,8 @@ $_nav-collapsed-width: 80px; height: 40px; } +// ── Nav divider ──────────────────────────────────────── + .bitfun-workspace-body__nav-divider { position: absolute; top: 0; @@ -76,28 +78,20 @@ $_nav-collapsed-width: 80px; height: 100%; cursor: ew-resize; z-index: 10; + background: transparent; &::after { - content: ''; - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - width: 2px; - height: 100%; - background: var(--color-primary); - opacity: 0; - border-radius: 1px; - transition: opacity $motion-fast $easing-standard; - } - - &:hover::after { - opacity: 0.35; + content: none; } } +// Active resizing state — keep line visible while dragging .bitfun-is-resizing-nav .bitfun-workspace-body__nav-divider::after { - opacity: 0.6; + content: ''; + opacity: 1; + width: 3px; + background: var(--color-accent-500); + box-shadow: 0 0 10px rgba(96, 165, 250, 0.50); } // ── Right column: rounded scene card (SceneBar + SceneViewport) ─── diff --git a/src/web-ui/src/app/layout/WorkspaceBody.tsx b/src/web-ui/src/app/layout/WorkspaceBody.tsx index e2d43eed..5eb67f0e 100644 --- a/src/web-ui/src/app/layout/WorkspaceBody.tsx +++ b/src/web-ui/src/app/layout/WorkspaceBody.tsx @@ -49,6 +49,17 @@ const WorkspaceBody: React.FC = ({ const { state, toggleLeftPanel } = useApp(); const isNavCollapsed = state.layout.leftPanelCollapsed; const [navWidth, setNavWidth] = useState(NAV_DEFAULT_WIDTH); + const [isDividerHovered, setIsDividerHovered] = useState(false); + + const handleDividerMouseEnter = useCallback(() => { + setIsDividerHovered(true); + document.body.classList.add('bitfun-divider-hovered'); + }, []); + + const handleDividerMouseLeave = useCallback(() => { + setIsDividerHovered(false); + document.body.classList.remove('bitfun-divider-hovered'); + }, []); const handleNavCollapseDragStart = useCallback((event: React.MouseEvent) => { if (event.button !== 0 || isNavCollapsed) return; @@ -64,6 +75,7 @@ const WorkspaceBody: React.FC = ({ const cleanup = () => { document.body.classList.remove('bitfun-is-dragging-nav-collapse'); document.body.classList.remove('bitfun-is-resizing-nav'); + document.body.classList.remove('bitfun-divider-hovered'); window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; @@ -91,7 +103,7 @@ const WorkspaceBody: React.FC = ({ }, [isNavCollapsed, navWidth, toggleLeftPanel]); return ( -
+
{isNavCollapsed && (
@@ -113,6 +125,8 @@ const WorkspaceBody: React.FC = ({ className="bitfun-workspace-body__nav-divider" style={{ '--nav-width': `${navWidth}px` } as React.CSSProperties} onMouseDown={handleNavCollapseDragStart} + onMouseEnter={handleDividerMouseEnter} + onMouseLeave={handleDividerMouseLeave} role="separator" aria-hidden="true" /> diff --git a/src/web-ui/src/app/scenes/SceneViewport.scss b/src/web-ui/src/app/scenes/SceneViewport.scss index 11449911..78f86b16 100644 --- a/src/web-ui/src/app/scenes/SceneViewport.scss +++ b/src/web-ui/src/app/scenes/SceneViewport.scss @@ -12,6 +12,9 @@ border-radius: $size-radius-base; border: 1px solid var(--border-subtle); background: var(--color-bg-scene); + transition: + border-color 240ms cubic-bezier(0.25, 1, 0.5, 1), + box-shadow 240ms cubic-bezier(0.25, 1, 0.5, 1); // ── Welcome overlay (app start) ────────────────────── @@ -49,3 +52,27 @@ } } } + +// ── Drag handle hover / active glow effect ────────────────────────────── + +body.bitfun-divider-hovered .bitfun-scene-viewport { + border-color: var(--border-accent-strong); + box-shadow: + inset 6px 0 32px -4px rgba(96, 165, 250, 0.22), + inset 0 0 60px -16px rgba(96, 165, 250, 0.10), + -3px 0 20px -4px rgba(96, 165, 250, 0.30); +} + +body.bitfun-is-resizing-nav .bitfun-scene-viewport { + border-color: var(--border-accent-strong); + box-shadow: + inset 4px 0 24px -4px rgba(96, 165, 250, 0.20), + inset 0 0 48px -16px rgba(96, 165, 250, 0.08), + -2px 0 18px -4px rgba(96, 165, 250, 0.28); +} + +@media (prefers-reduced-motion: reduce) { + .bitfun-scene-viewport { + transition: none; + } +} diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.scss b/src/web-ui/src/app/scenes/agents/AgentsScene.scss index 662859db..accaac24 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.scss +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.scss @@ -1,20 +1,6 @@ @use '../../../component-library/styles/tokens' as *; .bitfun-agents-scene { - .gallery-page-header__title { - margin: 0; - font-size: clamp(24px, 3vw, 32px); - line-height: 1.08; - letter-spacing: -0.03em; - } - - .gallery-page-header__subtitle { - margin-top: $size-gap-2; - max-width: 720px; - font-size: $font-size-sm; - color: var(--color-text-secondary); - } - .gallery-zone__subtitle { font-size: $font-size-sm; color: var(--color-text-secondary); diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx index a2d56624..43da6cf1 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx @@ -104,6 +104,10 @@ const AgentsHomeView: React.FC = () => { const [selectedTeamId, setSelectedTeamId] = React.useState(null); const [toolsEditing, setToolsEditing] = React.useState(false); const [skillsEditing, setSkillsEditing] = React.useState(false); + const [pendingTools, setPendingTools] = React.useState(null); + const [pendingSkills, setPendingSkills] = React.useState(null); + const [savingTools, setSavingTools] = React.useState(false); + const [savingSkills, setSavingSkills] = React.useState(false); const { allAgents, @@ -198,25 +202,31 @@ const AgentsHomeView: React.FC = () => { .slice(0, 3); }, [allAgents, selectedTeam]); - const openAgentDetails = useCallback((agent: AgentWithCapabilities) => { - setSelectedTeamId(null); - setSelectedAgentId(agent.id); + const resetEditState = useCallback(() => { setToolsEditing(false); setSkillsEditing(false); + setPendingTools(null); + setPendingSkills(null); + setSavingTools(false); + setSavingSkills(false); }, []); + const openAgentDetails = useCallback((agent: AgentWithCapabilities) => { + setSelectedTeamId(null); + setSelectedAgentId(agent.id); + resetEditState(); + }, [resetEditState]); + const closeAgentDetails = useCallback(() => { setSelectedAgentId(null); - setToolsEditing(false); - setSkillsEditing(false); - }, []); + resetEditState(); + }, [resetEditState]); const openTeamDetails = useCallback((teamId: string) => { setSelectedAgentId(null); - setToolsEditing(false); - setSkillsEditing(false); + resetEditState(); setSelectedTeamId(teamId); - }, []); + }, [resetEditState]); return ( @@ -512,25 +522,78 @@ const AgentsHomeView: React.FC = () => { {t('agentsOverview.tools', '工具')} {selectedAgent.agentKind === 'mode' - ? `${selectedAgentTools.length}/${availableTools.length}` + ? `${(toolsEditing ? (pendingTools ?? selectedAgentTools) : selectedAgentTools).length}/${availableTools.length}` : `${selectedAgentTools.length}`}
{selectedAgent.agentKind === 'mode' ? (
- {toolsEditing ? ( - + { + await handleResetTools(selectedAgent.id); + setToolsEditing(false); + setPendingTools(null); + }} + > + + + + + + ) : ( + + )}
) : null}
@@ -539,21 +602,30 @@ const AgentsHomeView: React.FC = () => {
{[...availableTools] .sort((a, b) => { - const aOn = selectedAgentTools.includes(a.name); - const bOn = selectedAgentTools.includes(b.name); + const draft = pendingTools ?? selectedAgentTools; + const aOn = draft.includes(a.name); + const bOn = draft.includes(b.name); if (aOn && !bOn) return -1; if (!aOn && bOn) return 1; return 0; }) .map((tool) => { - const isOn = selectedAgentTools.includes(tool.name); + const draft = pendingTools ?? selectedAgentTools; + const isOn = draft.includes(tool.name); return ( @@ -579,13 +651,64 @@ const AgentsHomeView: React.FC = () => { {t('agentsOverview.skills', 'Skills')} - {`${selectedAgentSkills.length}/${availableSkills.length}`} + {`${(skillsEditing ? (pendingSkills ?? selectedAgentSkills) : selectedAgentSkills).length}/${availableSkills.length}`}
- + {skillsEditing ? ( + <> + + + + ) : ( + + )}
@@ -593,21 +716,30 @@ const AgentsHomeView: React.FC = () => {
{[...availableSkills] .sort((a, b) => { - const aOn = selectedAgentSkills.includes(a.name); - const bOn = selectedAgentSkills.includes(b.name); + const draft = pendingSkills ?? selectedAgentSkills; + const aOn = draft.includes(a.name); + const bOn = draft.includes(b.name); if (aOn && !bOn) return -1; if (!aOn && bOn) return 1; return 0; }) .map((skill) => { - const isOn = selectedAgentSkills.includes(skill.name); + const draft = pendingSkills ?? selectedAgentSkills; + const isOn = draft.includes(skill.name); return ( diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss index e0f94dde..4d130c65 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss +++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss @@ -223,6 +223,185 @@ } } +// ───────────────────────────────────────────────────────────────────────────── +// Detail modal content styles (rendered inside GalleryDetailModal) +// These classes are used in AgentsScene.tsx for the agent detail popup +// ───────────────────────────────────────────────────────────────────────────── + +.agent-card { + // ── Capability grid ────────────────────────────────────────────────────── + &__cap-grid { + display: flex; + flex-direction: column; + gap: $size-gap-2; + } + + &__cap-row { + display: flex; + align-items: center; + gap: $size-gap-3; + } + + &__cap-label { + flex-shrink: 0; + width: 80px; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__cap-bar { + flex: 1; + display: flex; + align-items: center; + gap: 3px; + } + + &__cap-pip { + width: 100%; + height: 5px; + border-radius: 2px; + background: var(--element-bg-medium); + transition: background $motion-fast $easing-standard; + } + + &__cap-level { + flex-shrink: 0; + width: 28px; + text-align: right; + font-size: $font-size-xs; + color: var(--color-text-muted); + font-variant-numeric: tabular-nums; + } + + // ── Content sections ───────────────────────────────────────────────────── + &__section { + display: flex; + flex-direction: column; + gap: $size-gap-2; + } + + &__section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: $size-gap-2; + min-height: 28px; + } + + &__section-title { + display: inline-flex; + align-items: center; + gap: $size-gap-2; + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &__section-count { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 1px 6px; + border-radius: $size-radius-full; + background: var(--element-bg-medium); + font-size: 10px; + font-weight: $font-weight-medium; + color: var(--color-text-muted); + font-variant-numeric: tabular-nums; + text-transform: none; + letter-spacing: 0; + } + + &__section-actions { + display: flex; + align-items: center; + gap: $size-gap-2; + flex-shrink: 0; + } + + // ── Read-only chip grid ────────────────────────────────────────────────── + &__chip-grid { + display: flex; + flex-wrap: wrap; + gap: $size-gap-1; + } + + &__chip { + display: inline-flex; + align-items: center; + padding: 3px 9px; + border-radius: $size-radius-full; + border: 1px solid var(--border-subtle); + background: var(--element-bg-subtle); + font-size: 11px; + color: var(--color-text-secondary); + white-space: nowrap; + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + } + + // ── Editable token grid ────────────────────────────────────────────────── + &__token-grid { + display: flex; + flex-wrap: wrap; + gap: $size-gap-1; + } + + &__token { + display: inline-flex; + align-items: center; + padding: 3px 9px; + border-radius: $size-radius-full; + border: 1px solid var(--border-subtle); + background: var(--element-bg-subtle); + font-size: 11px; + font-family: $font-family-mono; + color: var(--color-text-muted); + cursor: pointer; + transition: + border-color $motion-fast $easing-standard, + background $motion-fast $easing-standard, + color $motion-fast $easing-standard; + + &:hover { + border-color: var(--border-medium); + background: var(--element-bg-base); + color: var(--color-text-secondary); + } + + &.is-on { + border-color: rgba(96, 165, 250, 0.4); + background: rgba(96, 165, 250, 0.08); + color: var(--color-accent-500); + + &:hover { + border-color: rgba(96, 165, 250, 0.6); + background: rgba(96, 165, 250, 0.12); + } + } + } + + &__token-name { + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + // ── Empty inline state ─────────────────────────────────────────────────── + &__empty-inline { + font-size: $font-size-xs; + color: var(--color-text-muted); + font-style: italic; + } +} + // ── Animations ── @keyframes agent-card-in { from { diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss index e88366fc..8d25c2a4 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss +++ b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss @@ -254,6 +254,62 @@ } } +// ───────────────────────────────────────────────────────────────────────────── +// Detail modal content styles (rendered inside GalleryDetailModal) +// These classes are used in AgentsScene.tsx for the team detail popup +// ───────────────────────────────────────────────────────────────────────────── + +.agent-team-card { + &__section { + display: flex; + flex-direction: column; + gap: $size-gap-2; + } + + &__section-title { + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &__member-list { + display: flex; + flex-wrap: wrap; + gap: $size-gap-1; + } + + &__member-chip { + display: inline-flex; + align-items: center; + gap: $size-gap-1; + padding: 4px 10px; + border-radius: $size-radius-full; + border: 1px solid var(--border-subtle); + background: var(--element-bg-subtle); + font-size: 11px; + color: var(--color-text-secondary); + white-space: nowrap; + } + + &__member-name { + font-weight: $font-weight-medium; + color: var(--color-text-primary); + } + + &__member-role { + font-size: 10px; + color: var(--color-text-muted); + padding-left: 2px; + + &::before { + content: "·"; + margin-right: 2px; + } + } +} + // ── Animations ── @keyframes agent-team-card-in { from { diff --git a/src/web-ui/src/app/scenes/my-agent/InsightsScene.scss b/src/web-ui/src/app/scenes/my-agent/InsightsScene.scss index 38fa5150..1d1eecf0 100644 --- a/src/web-ui/src/app/scenes/my-agent/InsightsScene.scss +++ b/src/web-ui/src/app/scenes/my-agent/InsightsScene.scss @@ -1,583 +1,779 @@ @use '../../../component-library/styles/tokens.scss' as *; -// ============ Animation System (minimal) ============ - -@keyframes insights-fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes insights-fade-up { - from { - opacity: 0; - transform: translateY(12px); - } - to { - opacity: 1; - transform: translateY(0); - } +// ============================================================ +// Design tokens +// ============================================================ + +$ins-blue: var(--color-accent-500); // #60a5fa +$ins-green: #6eb88c; +$ins-red: var(--color-error); +$ins-orange: #c9944d; + +$ins-blue-bg: var(--color-accent-100); // rgba(96,165,250,0.08) +$ins-green-bg: rgba(110, 184, 140, 0.08); +$ins-red-bg: var(--color-error-bg); + +$ins-surface: var(--color-bg-quaternary); // #202024, clearly above scene #16161a +$ins-surface-hover: var(--color-bg-quaternary); // same base, border differentiates hover +$ins-border: rgba(255, 255, 255, 0.10); +$ins-border-hover: rgba(255, 255, 255, 0.22); + +// Spacing: 4 / 8 / 14 / 22 / 40 +$ins-xs: 4px; +$ins-sm: 8px; +$ins-md: 14px; +$ins-lg: 22px; +$ins-xl: 40px; + +// Radius +$ins-r: 8px; +$ins-r-sm: 5px; +$ins-r-lg: 12px; + +// Type scale +$ins-title: 20px; +$ins-section: 12px; // section headings (small caps style) +$ins-body: 14px; +$ins-small: 13px; +$ins-label: 12px; + +// ============================================================ +// Keyframes +// ============================================================ + +@keyframes ins-fade-in { + from { opacity: 0; transform: translateY(3px); } + to { opacity: 1; transform: translateY(0); } } -@keyframes insights-spin { - from { transform: rotate(0deg); } +@keyframes ins-spin { to { transform: rotate(360deg); } } -// Reduced motion support @media (prefers-reduced-motion: reduce) { - .insights-scene, - .insights-scene * { - animation: none !important; - transition-duration: 0.15s !important; - } + .insights-scene, .insights-scene * { animation: none !important; } } -// ============ Scene Container ============ +// ============================================================ +// Scene base - shared between list and report views +// ============================================================ .insights-scene { display: flex; flex-direction: column; height: 100%; overflow: hidden; - animation: insights-fade-in 0.25s ease-out; + animation: ins-fade-in 0.18s ease-out both; + font-family: $font-family-sans; + font-size: $ins-body; + line-height: 1.6; + color: var(--color-text-primary); + -webkit-font-smoothing: antialiased; +} - // Max width constraint - max-width: 900px; +// ============================================================ +// List view - constrained width, centered +// ============================================================ + +.insights-scene:not(.insights-scene--report) { + max-width: 680px; margin: 0 auto; width: 100%; +} - &__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: $size-gap-4 $size-gap-4 0; - flex-shrink: 0; - } - - &__header-left { - display: flex; - align-items: center; - gap: $size-gap-2; - - h2 { - font-size: $font-size-lg; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - margin: 0; - } - - svg { - color: $color-accent-500; - } - } - - &__error { - display: flex; - align-items: center; - gap: $size-gap-2; - margin: $size-gap-3 $size-gap-4 0; - padding: $size-gap-2 $size-gap-3; - background: $color-error-bg; - border: 1px solid $color-error-border; - border-radius: $size-radius-base; - font-size: $font-size-sm; - color: $color-error; - - span { flex: 1; } - - button { - background: none; - border: none; - color: inherit; - cursor: pointer; - font-size: $font-size-lg; - line-height: 1; - padding: 0; - opacity: 0.7; - - &:hover { opacity: 1; } - } - } - - &__generate { - padding: $size-gap-4 $size-gap-4 0; - flex-shrink: 0; - } +// List header - &__generate-label, - &__history-label { - font-size: $font-size-xs; - font-weight: $font-weight-semibold; - text-transform: uppercase; - color: var(--color-text-muted); - margin-bottom: $size-gap-2; - letter-spacing: 0.5px; - } +.insights-scene__header { + padding: $ins-lg $ins-lg $ins-sm; +} - &__history-hint { - font-weight: $font-weight-normal; - text-transform: none; - font-size: 10px; - color: var(--color-text-muted); - opacity: 0.7; - letter-spacing: 0; - margin-left: $size-gap-2; - } +.insights-scene__header-title { + font-size: $ins-title; + font-weight: 600; + color: var(--color-text-primary); + margin: 0 0 2px; + letter-spacing: -0.02em; +} - &__generate-row { - display: flex; - align-items: center; - gap: $size-gap-3; - flex-wrap: wrap; - } +.insights-scene__header-subtitle { + font-size: $ins-small; + color: var(--color-text-muted); + margin: 0 0 $ins-md; +} - &__day-selector { - display: flex; - border: 1px solid $border-base; - border-radius: $size-radius-base; - overflow: hidden; - } +.insights-scene__header-meta { + display: flex; + flex-direction: column; + gap: $ins-sm; +} - &__day-btn { - padding: $size-gap-1 $size-gap-3; - border: none; - background: transparent; - color: var(--color-text-muted); - font-size: $font-size-xs; - font-weight: $font-weight-medium; - cursor: pointer; - border-right: 1px solid $border-base; - transition: background 0.15s, color 0.15s; +.insights-scene__header-actions { + display: flex; + align-items: center; + gap: $ins-sm; + flex-wrap: wrap; +} - &:last-child { border-right: none; } +// Day selector +.insights-scene__day-selector { + display: flex; + background: $ins-surface; + border-radius: $ins-r-sm; + padding: 2px; + gap: 2px; +} - &:hover { - background: $element-bg-soft; - color: var(--color-text-primary); - } +.insights-scene__day-btn { + padding: 3px 10px; + border: none; + background: transparent; + color: var(--color-text-muted); + font-size: $ins-small; + font-weight: 500; + cursor: pointer; + border-radius: 3px; + transition: background 0.12s, color 0.12s; - &.is-active { - background: $color-accent-100; - color: $color-accent-500; - font-weight: $font-weight-semibold; - } - } + &:hover { background: $ins-surface-hover; color: var(--color-text-primary); } + &.is-active { background: $ins-blue-bg; color: $ins-blue; font-weight: 600; } +} - &__generate-btn { - display: flex; - align-items: center; - gap: $size-gap-1; - padding: $size-gap-1 $size-gap-4; - border: 1px solid $color-accent-600; - border-radius: $size-radius-base; - background: $color-accent-100; - color: $color-accent-500; - font-size: $font-size-sm; - font-weight: $font-weight-medium; - cursor: pointer; - transition: background 0.15s; +// Shared button base +%ins-btn { + display: inline-flex; + align-items: center; + gap: $ins-xs; + padding: 4px 11px; + border: none; + border-radius: $ins-r-sm; + font-size: $ins-small; + font-weight: 500; + cursor: pointer; + transition: background 0.12s, opacity 0.12s; + &:disabled { opacity: 0.4; cursor: not-allowed; } +} - &:hover { - background: $color-accent-200; - } +.insights-scene__generate-btn { + @extend %ins-btn; + background: $ins-blue-bg; + color: $ins-blue; + &:hover { background: rgba(96, 165, 250, 0.15); } +} - &:active { - background: $color-accent-300; - } +.insights-scene__cancel-btn { + @extend %ins-btn; + background: $ins-red-bg; + color: $ins-red; + &:hover { background: rgba(199, 112, 112, 0.15); } +} - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } +// Error / progress +.insights-scene__error { + display: flex; + align-items: center; + gap: $ins-sm; + margin: $ins-sm $ins-lg 0; + padding: 5px $ins-md; + background: $ins-red-bg; + border-radius: $ins-r-sm; + font-size: $ins-small; + color: $ins-red; + + span { flex: 1; } + button { + background: none; border: none; color: inherit; + cursor: pointer; font-size: 14px; padding: 0; opacity: 0.6; + &:hover { opacity: 1; } } +} - &__cancel-btn { - display: flex; - align-items: center; - gap: $size-gap-1; - padding: $size-gap-1 $size-gap-4; - border: 1px solid $color-error-border; - border-radius: $size-radius-base; - background: $color-error-bg; - color: $color-error; - font-size: $font-size-sm; - font-weight: $font-weight-medium; - cursor: pointer; - transition: background 0.15s, border-color 0.15s; +.insights-scene__progress-info { + display: flex; + align-items: center; + gap: $ins-sm; + margin: $ins-xs $ins-lg 0; + font-size: $ins-small; + color: var(--color-text-muted); +} - &:hover { - background: rgba(199, 112, 112, 0.18); - border-color: $color-error; - } - } +.insights-scene__progress-count { + font-weight: 600; + color: $ins-blue; +} - &__progress-info { - display: flex; - align-items: center; - gap: $size-gap-2; - font-size: $font-size-xs; - color: var(--color-text-muted); - min-width: 0; - flex: 1; - min-width: 200px; - } +// History list - &__progress-count { - font-weight: $font-weight-semibold; - color: $color-accent-600; - white-space: nowrap; - } +.insights-scene__history { + padding: 0 $ins-lg $ins-lg; + flex: 1; + overflow-y: auto; + min-height: 0; +} - &__progress-message { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } +.insights-scene__history-label { + font-size: $ins-label; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--color-text-muted); + margin-bottom: $ins-sm; +} - &__history { - padding: $size-gap-4; - flex: 1; - overflow-y: auto; - min-height: 0; - } +.insights-scene__history-hint { + font-weight: 400; + text-transform: none; + font-size: 10px; + opacity: 0.55; + letter-spacing: 0; + margin-left: $ins-sm; +} - &__loading { - display: flex; - justify-content: center; - padding: $size-gap-6; - color: var(--color-text-muted); - } +.insights-scene__loading { + display: flex; + justify-content: center; + padding: $ins-xl; + color: var(--color-text-muted); +} - &__spinner { - animation: insights-spin 1s linear infinite; - } +.insights-scene__spinner { animation: ins-spin 0.9s linear infinite; } - &__empty { - text-align: center; - padding: $size-gap-6 0; - color: var(--color-text-muted); - font-size: $font-size-sm; - } +.insights-scene__empty { + text-align: center; + padding: $ins-xl; + color: var(--color-text-muted); + font-size: $ins-small; +} - &__report-list { - display: flex; - flex-direction: column; - gap: $size-gap-2; - } +.insights-scene__report-list { + display: flex; + flex-direction: column; + gap: 4px; } -// ============ Report Meta Card ============ +// Meta cards (report list items) .insights-meta-card { - @include card-base; - display: flex; - flex-direction: column; - gap: $size-gap-2; - padding: $size-gap-3; + background: $ins-surface; + border-radius: $ins-r; + padding: $ins-md; cursor: pointer; text-align: left; width: 100%; + border: 1px solid transparent; + transition: + background 0.2s cubic-bezier(0.25, 1, 0.5, 1), + border-color 0.2s cubic-bezier(0.25, 1, 0.5, 1), + box-shadow 0.2s cubic-bezier(0.25, 1, 0.5, 1), + transform 0.2s cubic-bezier(0.25, 1, 0.5, 1); + will-change: transform, box-shadow; &:hover { - @include card-hover; - border-color: $border-accent-soft; + background: color-mix(in srgb, var(--color-bg-quaternary) 100%, white 4%); + border-color: $ins-border-hover; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18), 0 1px 3px rgba(0, 0, 0, 0.12); + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); + transition-duration: 0.08s; + } + + &:hover &__date { + color: var(--color-text-primary); + } + + &:hover &__stat { + color: var(--color-text-secondary); } &__top { display: flex; - align-items: center; + align-items: baseline; justify-content: space-between; + margin-bottom: $ins-xs; + gap: $ins-sm; } &__date { - font-size: $font-size-sm; - font-weight: $font-weight-medium; + font-size: $ins-body; + font-weight: 500; color: var(--color-text-primary); + transition: color 0.2s; } &__range { - font-size: 11px; + font-size: $ins-small; color: var(--color-text-muted); - white-space: nowrap; + flex-shrink: 0; } &__stats-row { display: flex; flex-wrap: wrap; - gap: $size-gap-2; + gap: 6px; + margin-bottom: $ins-xs; } &__stat { display: inline-flex; align-items: center; gap: 3px; - font-size: 11px; + font-size: $ins-label; color: var(--color-text-muted); - background: $element-bg-subtle; - padding: 2px $size-gap-2; - border-radius: $size-radius-sm; + transition: color 0.2s; } &__tags { display: flex; flex-wrap: wrap; - gap: $size-gap-1; + gap: $ins-xs; + margin-top: $ins-xs; } &__tag { font-size: 10px; - padding: 1px $size-gap-2; - border-radius: $size-radius-sm; - background: $color-accent-100; - color: $color-accent-600; - border: 1px solid $border-accent-soft; + padding: 1px 6px; + border-radius: 3px; + background: $ins-blue-bg; + color: $ins-blue; + transition: background 0.2s, opacity 0.2s; + + .insights-meta-card:hover & { opacity: 0.9; } &--lang { - background: $color-success-bg; - color: $color-success; - border-color: $color-success-border; + background: $ins-green-bg; + color: $ins-green; } } } -// ============ Report View ============ +@media (prefers-reduced-motion: reduce) { + .insights-meta-card { + transition: background 0.01ms, border-color 0.01ms !important; + transform: none !important; + } +} + +// ============================================================ +// Report view - layout shell +// ============================================================ + +.insights-scene--report { + overflow: hidden; + max-width: none; + margin: 0; + width: 100%; +} + +// ── Top bar: back + meta + actions (title lives in body hero) .insights-report-header { display: flex; align-items: center; - justify-content: space-between; - padding: $size-gap-2 $size-gap-4; - border-bottom: 1px solid $border-subtle; + gap: $ins-md; + padding: 0 $ins-lg; + height: 40px; + border-bottom: 1px solid $ins-border; flex-shrink: 0; &__back { - display: flex; + display: inline-flex; align-items: center; - gap: $size-gap-1; - padding: $size-gap-1 $size-gap-2; + gap: $ins-xs; + padding: 3px $ins-sm; border: none; - border-radius: $size-radius-sm; + border-radius: $ins-r-sm; background: transparent; - color: var(--color-text-secondary); - font-size: $font-size-sm; + color: var(--color-text-muted); + font-size: $ins-small; cursor: pointer; - transition: background 0.15s, color 0.15s; + transition: background 0.12s, color 0.12s; + flex-shrink: 0; + white-space: nowrap; - &:hover { - background: $element-bg-soft; - color: var(--color-text-primary); - } + &:hover { background: $ins-surface-hover; color: var(--color-text-primary); } } - &__actions { - display: flex; - align-items: center; - gap: $size-gap-2; + // Thin vertical separator after back button + &__back::after { + content: ''; + display: block; + width: 1px; + height: 14px; + background: $ins-border; + margin-left: $ins-md; + flex-shrink: 0; } - &__html-btn { + &__meta { display: flex; align-items: center; - gap: $size-gap-1; - padding: $size-gap-1 $size-gap-3; - border: 1px solid $border-base; - border-radius: $size-radius-sm; - background: transparent; - color: var(--color-text-secondary); - font-size: $font-size-xs; - cursor: pointer; - transition: background 0.15s, color 0.15s; - - &:hover:not(:disabled) { - background: $element-bg-soft; - color: var(--color-text-primary); - } + gap: $ins-lg; + margin-left: auto; + font-size: $ins-small; + color: var(--color-text-muted); + flex-shrink: 0; - &:disabled { - opacity: $opacity-disabled; - cursor: not-allowed; + span { + display: inline-flex; + align-items: center; + gap: 3px; } } -} -.insights-report-subtitle { - display: flex; - align-items: center; - gap: $size-gap-4; - padding: $size-gap-2 $size-gap-4; - font-size: $font-size-xs; - color: var(--color-text-muted); - border-bottom: 1px solid $border-subtle; - flex-shrink: 0; - flex-wrap: wrap; + &__actions { flex-shrink: 0; } - span { - display: flex; + &__html-btn { + display: inline-flex; align-items: center; - gap: $size-gap-1; + gap: $ins-xs; + padding: 3px 10px; + border: none; + border-radius: $ins-r-sm; + background: $ins-surface; + color: var(--color-text-muted); + font-size: $ins-small; + cursor: pointer; + transition: background 0.12s, color 0.12s; + + &:hover:not(:disabled) { background: $ins-surface-hover; color: var(--color-text-primary); } + &:disabled { opacity: 0.35; cursor: not-allowed; } } } -.insights-report-body { +// Legacy title/subtitle - hidden (merged into header chrome) +.insights-report-title, +.insights-report-subtitle { display: none; } + +// Scroll root - single overflow container; scrollbar at the right edge + +.insights-report-content { flex: 1; - overflow-y: auto; - padding: $size-gap-4; + display: flex; + flex-direction: row; min-height: 0; + overflow-y: auto; + overflow-x: hidden; - &::-webkit-scrollbar { width: 6px; } + &::-webkit-scrollbar { width: 5px; } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { - background: $border-subtle; + background: rgba(255, 255, 255, 0.10); border-radius: 3px; } + &::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.20); } } -.insights-scene--report { - overflow: hidden; - max-width: 100%; +// Report body column + +.insights-report-body { + flex: 1; + min-width: 0; + padding: $ins-lg $ins-xl $ins-md; } -// ============ At a Glance ============ +.insights-report-body-inner { + max-width: 680px; + margin: 0 auto; + // Bottom space must live on the inner wrapper so scroll height includes it + // (flex-stretched body alone can make bottom padding feel “eaten”). + padding-bottom: $ins-xl * 2 + $ins-lg; +} -.insights-glance { - background: $color-accent-100; - border: 1px solid $border-accent-soft; - border-radius: $size-radius-lg; - padding: $size-gap-3; - margin-bottom: $size-gap-4; +// Report title — centered in body, larger than header chrome +.insights-report-hero { + text-align: center; + padding: $ins-md 0 $ins-xl; &__title { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; - color: $color-accent-500; - margin-bottom: $size-gap-2; + margin: 0; + font-size: 26px; + font-weight: 700; + letter-spacing: -0.035em; + line-height: 1.2; + color: var(--color-text-primary); } +} - &__sections { - display: flex; - flex-direction: column; - gap: $size-gap-2; +// ============================================================ +// TOC navigation - right sidebar +// ============================================================ +// Clean vertical list with active dot indicator, no icons by default + +.insights-report-nav { + position: sticky; + top: 0; + align-self: flex-start; + width: 136px; + flex-shrink: 0; + padding: $ins-lg $ins-md $ins-lg 0; + display: flex; + flex-direction: column; + gap: 0; + box-sizing: border-box; + + // Section group label (reserved) - keeps structure flexible + &__group-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--color-text-muted); + opacity: 0.5; + padding: $ins-sm $ins-sm $ins-xs; } &__item { - font-size: $font-size-sm; - color: var(--color-text-secondary); - line-height: $line-height-relaxed; + position: relative; + display: flex; + align-items: center; + gap: $ins-sm; + padding: 5px $ins-sm 5px $ins-md; + border: none; + background: transparent; + color: var(--color-text-muted); + font-size: $ins-small; + text-align: left; + cursor: pointer; + transition: color 0.15s; + border-radius: $ins-r-sm; + line-height: 1.3; + + // Active indicator dot on left edge + &::before { + content: ''; + position: absolute; + left: 4px; + top: 50%; + transform: translateY(-50%) scale(0); + width: 4px; + height: 4px; + border-radius: 50%; + background: $ins-blue; + transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.15s; + opacity: 0; + } - strong { - color: $color-accent-500; + svg { display: none; } // icons hidden - text-only minimal nav + + &:hover { + color: var(--color-text-secondary); + background: $ins-surface; } + + &.is-active { + color: var(--color-text-primary); + + &::before { + transform: translateY(-50%) scale(1); + opacity: 1; + } + } + } + + &__label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: $ins-small; } } -// ============ Interaction Style ============ +// ============================================================ +// Section heading system - clear hierarchy, minimal chrome +// ============================================================ -.insights-interaction { - @include card-base; - padding: $size-gap-3; - margin-bottom: $size-gap-4; +.insights-section { + margin-bottom: 0; + padding: $ins-xl 0 0; +} - &__narrative { - font-size: $font-size-sm; - color: var(--color-text-secondary); - line-height: 1.6; - margin: 0 0 $size-gap-2; +%ins-section-heading { + display: flex; + align-items: baseline; + gap: $ins-sm; + font-size: 18px; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--color-text-primary); + margin: 0 0 $ins-lg; + padding: 0; + + &::before { display: none; } +} + +.insights-section h3 { @extend %ins-section-heading; } +.insights-section h4 { + font-size: $ins-body; + font-weight: 600; + color: var(--color-text-secondary); + margin: $ins-lg 0 $ins-sm; + letter-spacing: -0.01em; +} + +.insights-section-intro { + font-size: $ins-body; + color: var(--color-text-muted); + margin: -$ins-sm 0 $ins-lg; + line-height: 1.7; +} + +// ============================================================ +// At a Glance +// ============================================================ + +.insights-glance { + box-sizing: border-box; + border-radius: $ins-r-lg; + // Whole block reads as one framed panel (theme border, stronger than $ins-border) + border: 1px solid var(--border-base); + background: transparent; + padding: $ins-lg; + margin-bottom: $ins-xl; + overflow: hidden; + transition: border-color 0.15s ease; + + &:hover { border-color: var(--border-medium); } + + &__title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.6px; + color: $ins-blue; + margin: 0 0 $ins-md; } - &__patterns { + &__sections { display: flex; flex-direction: column; - gap: $size-gap-1; + gap: 0; } - &__pattern { - background: $color-success-bg; - border: 1px solid $color-success-border; - border-radius: $size-radius-sm; - padding: $size-gap-1 $size-gap-2; - font-size: $font-size-xs; - color: $color-success; + &__item { + font-size: $ins-body; + color: var(--color-text-secondary); + line-height: 1.7; + padding: $ins-md 0; + border-bottom: 1px solid $ins-border; + + &:first-child { padding-top: 0; } + &:last-child { border-bottom: none; padding-bottom: 0; } + + strong { + color: var(--color-text-primary); + font-weight: 600; + letter-spacing: -0.01em; + } } } -// ============ Stats ============ +// ============================================================ +// Stats row - number grid +// ============================================================ -.insights-stats-row { +.insights-stats { display: flex; - gap: $size-gap-3; - padding: $size-gap-3 0; - border-top: 1px solid $border-subtle; - border-bottom: 1px solid $border-subtle; - margin-bottom: $size-gap-4; - flex-wrap: wrap; + flex-direction: column; + gap: $ins-sm; + margin-bottom: $ins-xl; + // outer frame + inner gaps so each cell reads as its own tile + padding: $ins-sm; + border-radius: $ins-r-lg; + border: 1px solid $ins-border; + background: var(--element-bg-subtle); + transition: border-color 0.15s ease; + + &:hover { border-color: $ins-border-hover; } + + &__row { + display: flex; + flex-direction: row; + width: 100%; + align-items: stretch; + gap: $ins-sm; + } } .insights-stat { display: flex; flex-direction: column; align-items: center; - flex: 1; - min-width: 70px; + justify-content: center; + padding: $ins-md $ins-sm; + flex: 1 1 0; + min-width: 0; + text-align: center; + border-radius: $ins-r; + border: 1px solid $ins-border; + background: $ins-surface; + transition: border-color 0.15s ease, background 0.15s ease; - &__icon { - color: var(--color-text-muted); - margin-bottom: $size-gap-1; + &:hover { + border-color: $ins-border-hover; + background: color-mix(in srgb, var(--color-bg-quaternary) 100%, white 4%); } &__value { - font-size: $font-size-xl; - font-weight: $font-weight-bold; + font-size: 20px; + font-weight: 600; color: var(--color-text-primary); + letter-spacing: -0.03em; + line-height: 1.2; } &__label { - font-size: 10px; + font-size: 11px; color: var(--color-text-muted); text-transform: uppercase; - text-align: center; + letter-spacing: 0.4px; + margin-top: 4px; } } -// ============ Charts ============ +// ============================================================ +// Charts: no card wrapper, bare flow +// ============================================================ .insights-charts-row { display: grid; grid-template-columns: 1fr 1fr; - gap: $size-gap-3; - margin-bottom: $size-gap-4; - - &--full { - grid-template-columns: 1fr; - } + gap: $ins-xl; + margin-bottom: $ins-lg; - @media (max-width: 600px) { - grid-template-columns: 1fr; - } + &--full { grid-template-columns: 1fr; } } +// No background or border - chart sits in document flow .insights-chart-card { - @include card-base; - padding: $size-gap-3; + background: none; + border: none; + padding: 0; &__title { font-size: 11px; - font-weight: $font-weight-semibold; - color: var(--color-text-muted); + font-weight: 500; text-transform: uppercase; - margin-bottom: $size-gap-2; - letter-spacing: 0.3px; + letter-spacing: 0.6px; + color: var(--color-text-muted); + margin-bottom: $ins-md; + opacity: 0.6; } } .insights-bar-row { display: flex; align-items: center; - margin-bottom: $size-gap-1; + margin-bottom: 10px; + + &:last-child { margin-bottom: 0; } &__label { - width: 90px; - font-size: 11px; - color: var(--color-text-secondary); + width: 88px; + font-size: $ins-small; + color: var(--color-text-muted); flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; @@ -586,365 +782,611 @@ &__track { flex: 1; - height: 6px; - background: $element-bg-subtle; - border-radius: 3px; - margin: 0 $size-gap-2; + height: 8px; + background: rgba(255, 255, 255, 0.06); + border-radius: 4px; + margin: 0 $ins-sm; overflow: hidden; } &__fill { height: 100%; - border-radius: 3px; - transition: width 0.4s ease-out; + border-radius: 4px; + transition: width 0.4s cubic-bezier(0.22, 1, 0.36, 1); + opacity: 0.65; } &__value { - width: 32px; - font-size: 11px; - font-weight: $font-weight-medium; + width: 30px; + font-size: $ins-small; + font-weight: 500; color: var(--color-text-muted); text-align: right; } } -// ============ Section common ============ +// ============================================================ +// Content cards - shared shape +// ============================================================ -.insights-section { - margin-bottom: $size-gap-4; +%ins-card { + border-radius: $ins-r-lg; + padding: $ins-lg; + border: 1px solid $ins-border; + background: $ins-surface; + transition: border-color 0.15s ease, background 0.15s ease; - h3 { - font-size: $font-size-base; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - margin-bottom: $size-gap-2; + &:hover { + border-color: $ins-border-hover; + background: color-mix(in srgb, var(--color-bg-quaternary) 100%, white 3%); } +} + +// Interaction style - no card, plain text blocks - h4 { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; +.insights-interaction { + margin-bottom: $ins-lg; + + &__narrative { + font-size: $ins-body; color: var(--color-text-secondary); - margin-bottom: $size-gap-2; - margin-top: $size-gap-3; + line-height: 1.7; + margin: 0 0 $ins-md; + } + + &__patterns { + display: flex; + flex-direction: column; + gap: 0; + } + + &__pattern { + font-size: $ins-small; + color: var(--color-text-secondary); + line-height: 1.6; + padding: $ins-sm 0; + border-bottom: 1px solid $ins-border; + + &:first-child { border-top: 1px solid $ins-border; } + + // "· " bullet prefix via pseudo + &::before { + content: '· '; + color: var(--color-text-muted); + } } } -// ============ Project Areas ============ +// Project areas - row list, no card background .insights-areas { display: flex; flex-direction: column; - gap: $size-gap-2; + gap: 0; } .insights-area-card { - @include card-base; - padding: $size-gap-2; + padding: $ins-md 0; + border-bottom: 1px solid $ins-border; + + &:first-child { border-top: 1px solid $ins-border; } &__header { display: flex; justify-content: space-between; - align-items: center; - margin-bottom: $size-gap-1; + align-items: baseline; + margin-bottom: 4px; + gap: $ins-sm; } &__name { - font-weight: $font-weight-medium; - font-size: $font-size-sm; + font-weight: 600; + font-size: $ins-body; color: var(--color-text-primary); + letter-spacing: -0.01em; } &__count { - font-size: 11px; + font-size: $ins-small; color: var(--color-text-muted); - background: $element-bg-subtle; - padding: 1px $size-gap-2; - border-radius: $size-radius-sm; + flex-shrink: 0; } &__desc { - font-size: $font-size-xs; + font-size: $ins-small; color: var(--color-text-muted); - line-height: $line-height-base; + line-height: 1.6; margin: 0; } } -// ============ Big Wins ============ +// Big wins - row list, no card background -.insights-wins { - display: flex; - flex-direction: column; - gap: $size-gap-2; -} +.insights-wins { display: flex; flex-direction: column; gap: 0; } .insights-win-card { - background: $color-success-bg; - border: 1px solid $color-success-border; - border-radius: $size-radius-base; - padding: $size-gap-2; + padding: $ins-md 0; + border-bottom: 1px solid $ins-border; + + &:first-child { border-top: 1px solid $ins-border; } &__title { - font-weight: $font-weight-medium; - font-size: $font-size-sm; - color: $color-success; - margin-bottom: $size-gap-1; + font-weight: 600; + font-size: $ins-body; + color: var(--color-text-primary); + letter-spacing: -0.01em; + margin: 0 0 4px; } &__desc { - font-size: $font-size-xs; - color: $color-success; - opacity: 0.85; - line-height: $line-height-base; + font-size: $ins-small; + color: var(--color-text-secondary); + line-height: 1.65; margin: 0; } + // "→ impact" — action-oriented, stands out &__impact { - font-size: 11px; - color: $color-success; - opacity: 0.7; - margin: $size-gap-1 0 0; - font-style: italic; + display: flex; + gap: 6px; + font-size: $ins-small; + font-weight: 500; + color: var(--color-text-primary); + margin: $ins-sm 0 0; + line-height: 1.5; + + &::before { + content: '→'; + color: $ins-blue; + flex-shrink: 0; + font-weight: 400; + } } } -// ============ Friction ============ +// Friction - row list, no card background -.insights-friction { - display: flex; - flex-direction: column; - gap: $size-gap-2; -} +.insights-friction { display: flex; flex-direction: column; gap: 0; } .insights-friction-card { - background: $color-error-bg; - border: 1px solid $color-error-border; - border-radius: $size-radius-base; - padding: $size-gap-2; + padding: $ins-md 0; + border-bottom: 1px solid $ins-border; + + &:first-child { border-top: 1px solid $ins-border; } &__title { - font-weight: $font-weight-medium; - font-size: $font-size-sm; - color: $color-error; - margin-bottom: $size-gap-1; + font-weight: 600; + font-size: $ins-body; + color: var(--color-text-primary); + letter-spacing: -0.01em; + margin: 0 0 4px; } &__desc { - font-size: $font-size-xs; - color: $color-error; - opacity: 0.85; - margin: 0 0 $size-gap-1; + font-size: $ins-small; + color: var(--color-text-secondary); + line-height: 1.65; + margin: 0; } &__examples { - margin: 0 0 $size-gap-1 $size-gap-4; - font-size: 11px; + margin: $ins-xs 0 0 $ins-md; + font-size: $ins-small; color: var(--color-text-muted); + list-style: none; + padding: 0; - li { margin-bottom: 2px; } + li { + margin-bottom: 2px; + &::before { content: '· '; color: var(--color-text-muted); } + } } + // "→ suggestion" — actionable advice &__suggestion { - background: $element-bg-subtle; - border: 1px solid $color-error-border; - border-radius: $size-radius-sm; - padding: $size-gap-1 $size-gap-2; - font-size: $font-size-xs; - color: $color-error; + display: flex; + gap: 6px; + margin-top: $ins-sm; + font-size: $ins-small; + font-weight: 500; + color: var(--color-text-primary); + line-height: 1.55; + + &::before { + content: '→'; + color: $ins-blue; + flex-shrink: 0; + font-weight: 400; + } } } -// ============ Suggestions ============ +// ============================================================ +// Emphasis system - by importance, not content type +// ============================================================ + +// Highlight block: must-read conclusions, key findings +// Use sparingly - 1-2 per report maximum +.insights-highlight-block { + background: rgba(255, 255, 255, 0.05); + border-radius: $ins-r; + padding: $ins-md $ins-lg; + margin-bottom: $ins-md; + font-size: $ins-small; + color: var(--color-text-primary); + line-height: 1.7; +} -.insights-md-section { - background: $color-info-bg; - border: 1px solid $color-info-border; - border-radius: $size-radius-base; - padding: $size-gap-2; - margin-bottom: $size-gap-2; +// Conclusion line: single-sentence summary at end of each section +.insights-conclusion-line { + font-size: $ins-small; + font-weight: 500; + color: var(--color-text-primary); + padding-top: $ins-sm; + border-top: 1px solid $ins-border; + margin-top: $ins-md; + line-height: 1.6; } -.insights-md-item { - padding: $size-gap-2 0; - border-bottom: 1px solid $color-info-border; +// Key metric inline: used inside paragraphs, not standalone +// Apply to wrapping a number + unit +.insights-key-metric { + display: inline-flex; + align-items: baseline; + gap: 3px; - &:last-child { border-bottom: none; } + &__value { + font-size: 17px; + font-weight: 600; + letter-spacing: -0.03em; + color: $ins-blue; + line-height: 1; + } - &__section { - font-size: 10px; - font-weight: $font-weight-semibold; - text-transform: uppercase; + &__unit { + font-size: $ins-label; color: var(--color-text-muted); - margin-bottom: $size-gap-1; } +} - &__rationale { - font-size: 11px; +// ============================================================ +// MarkdownInline - report-wide typography +// ============================================================ + +.insights-report-body { + strong { + font-weight: 500; + color: var(--color-text-primary); + } + + em { + font-style: italic; color: var(--color-text-muted); - margin: $size-gap-1 0 0; } } -.insights-feature-card, -.insights-pattern-card { - background: $color-success-bg; - border: 1px solid $color-success-border; - border-radius: $size-radius-base; - padding: $size-gap-2; - margin-bottom: $size-gap-1; +// ── Suggestions: md additions ──────────────────────────────── + +.insights-md-list { + display: flex; + flex-direction: column; + gap: 0; + margin-bottom: $ins-md; } -.insights-pattern-card { - background: $color-info-bg; - border-color: $color-info-border; +.insights-md-row { + padding: $ins-md 0; + border-bottom: 1px solid $ins-border; + + &:first-of-type { border-top: 1px solid $ins-border; } + + &__header { + display: flex; + align-items: center; + gap: $ins-sm; + margin-bottom: $ins-xs; + min-height: $ins-md; // reserve space even when no badge + } + + &__badge { + display: inline-block; + font-size: 11px; + font-weight: 500; + color: var(--color-text-muted); + background: rgba(255, 255, 255, 0.06); + border-radius: $ins-r-sm; + padding: 2px 7px; + letter-spacing: 0.02em; + } + + &__rationale { + font-size: $ins-small; + color: var(--color-text-muted); + line-height: 1.6; + margin: $ins-xs 0 0; + font-style: italic; + } +} + +// ── Suggestions: features to try ───────────────────────────── + +.insights-feature-list { + display: flex; + flex-direction: column; + gap: 0; + margin-bottom: $ins-md; } -.insights-feature-card { +.insights-feature-row { + display: flex; + gap: $ins-md; + padding: $ins-md 0; + border-bottom: 1px solid $ins-border; + + &:first-of-type { border-top: 1px solid $ins-border; } + + &__index { + font-size: 13px; + font-weight: 600; + color: $ins-blue; + line-height: 1.5; + min-width: 18px; + flex-shrink: 0; + opacity: 0.7; + padding-top: 1px; + } + + &__body { + flex: 1; + min-width: 0; + } + &__title { - font-weight: $font-weight-medium; - font-size: $font-size-sm; + font-weight: 600; + font-size: $ins-body; color: var(--color-text-primary); - margin-bottom: $size-gap-1; + letter-spacing: -0.01em; + margin: 0 0 3px; } &__desc { - font-size: $font-size-xs; - color: var(--color-text-muted); - margin: 0 0 $size-gap-1; + font-size: $ins-small; + color: var(--color-text-secondary); + line-height: 1.65; + margin: 0; } &__benefit { - font-size: 11px; + font-size: $ins-small; color: var(--color-text-muted); - margin: 0; + line-height: 1.55; + margin: 4px 0 0; + font-style: italic; } } -.insights-pattern-card { +// ── Suggestions: usage patterns ────────────────────────────── + +.insights-pattern-list { + display: flex; + flex-direction: column; + gap: 0; +} + +.insights-pattern-row { + padding: $ins-md 0; + border-bottom: 1px solid $ins-border; + + &:first-of-type { border-top: 1px solid $ins-border; } + &__title { - font-weight: $font-weight-medium; - font-size: $font-size-sm; + font-weight: 600; + font-size: $ins-body; color: var(--color-text-primary); - margin-bottom: $size-gap-1; + letter-spacing: -0.01em; + margin: 0 0 3px; } &__desc { - font-size: $font-size-xs; - color: var(--color-text-muted); + font-size: $ins-small; + color: var(--color-text-secondary); + line-height: 1.65; margin: 0; } } -// ============ Copyable ============ +// Copyable code block .insights-copyable { - margin-top: $size-gap-2; + margin-top: $ins-sm; &__label { - font-size: 10px; - font-weight: $font-weight-semibold; + font-size: $ins-label; + font-weight: 500; text-transform: uppercase; + letter-spacing: 0.5px; color: var(--color-text-muted); - margin-bottom: $size-gap-1; + margin-bottom: $ins-xs; + opacity: 0.7; } + // row wraps code + button as a single unit &__row { - display: flex; - align-items: flex-start; - gap: $size-gap-2; + position: relative; + display: block; + + // show button on hover of the whole row + &:hover .insights-copyable__btn { opacity: 1; } } &__code { - flex: 1; - background: $element-bg-subtle; - padding: $size-gap-2; - border-radius: $size-radius-sm; - font-size: 11px; + display: block; + width: 100%; + box-sizing: border-box; + background: $ins-surface; + border: 1px solid $ins-border; + // right padding reserves space for the button + padding: $ins-sm 40px $ins-sm $ins-md; + border-radius: $ins-r-sm; + font-size: $ins-small; font-family: $font-family-mono; color: var(--color-text-primary); - border: 1px solid $border-base; white-space: pre-wrap; word-break: break-word; + line-height: 1.6; + transition: border-color 0.15s; + + .insights-copyable__row:hover & { border-color: $ins-border-hover; } } + // button floats inside the code block, top-right &__btn { + position: absolute; + top: 6px; + right: 6px; display: flex; align-items: center; justify-content: center; - width: 28px; - height: 28px; + width: 24px; + height: 24px; border: none; - border-radius: $size-radius-sm; - background: $element-bg-soft; + border-radius: $ins-r-sm; + background: rgba(255, 255, 255, 0.08); color: var(--color-text-muted); cursor: pointer; - flex-shrink: 0; - transition: background 0.15s, color 0.15s; + opacity: 0; // hidden until row hover + transition: opacity 0.15s, background 0.15s, color 0.15s; &:hover { - background: $element-bg-medium; + background: rgba(255, 255, 255, 0.14); color: var(--color-text-primary); } } } -// ============ Horizon ============ +// ── Horizon — numbered cards with action block ──────────────── .insights-horizon { display: flex; flex-direction: column; - gap: $size-gap-2; + gap: $ins-sm; } .insights-horizon-card { - background: $color-purple-200; - border: 1px solid $border-purple; - border-radius: $size-radius-base; - padding: $size-gap-2; + display: flex; + gap: $ins-md; + padding: $ins-lg; + border-radius: $ins-r-lg; + border: 1px solid $ins-border; + background: $ins-surface; + transition: border-color 0.15s ease; + + &:hover { border-color: $ins-border-hover; } + + // left: large muted index number + &__index { + font-size: 22px; + font-weight: 700; + letter-spacing: -0.04em; + color: var(--color-text-muted); + opacity: 0.25; + line-height: 1; + flex-shrink: 0; + width: 24px; + text-align: right; + padding-top: 2px; + } + + &__body { + flex: 1; + min-width: 0; + } &__title { - font-weight: $font-weight-medium; - font-size: $font-size-sm; - color: $color-purple-500; - margin-bottom: $size-gap-1; + font-weight: 600; + font-size: $ins-body; + color: var(--color-text-primary); + letter-spacing: -0.01em; + margin: 0 0 5px; } &__desc { - font-size: $font-size-xs; + font-size: $ins-small; color: var(--color-text-secondary); - line-height: $line-height-base; - margin: 0 0 $size-gap-1; + line-height: 1.65; + margin: 0; } - &__steps { - margin: 0 0 0 $size-gap-4; - font-size: 11px; - color: $color-purple-500; + // how-to-try: inline label + text, visually separated + &__how { + display: flex; + align-items: baseline; + gap: $ins-sm; + margin-top: $ins-md; + padding-top: $ins-md; + border-top: 1px solid $ins-border; + font-size: $ins-small; + color: var(--color-text-secondary); + line-height: 1.55; + } - li { margin-bottom: 2px; } + &__how-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: $ins-blue; + flex-shrink: 0; + opacity: 0.8; } } -// ============ Fun Ending ============ +// ── Fun ending — left-aligned card ─────────────────────────── .insights-fun-ending { - background: $color-warning-bg; - border: 1px solid $color-warning-border; - border-radius: $size-radius-lg; - padding: $size-gap-4; - margin-top: $size-gap-4; - text-align: center; + margin-top: $ins-xl + $ins-md; + margin-bottom: $ins-xl; + padding-top: $ins-xl + $ins-sm; + // Clear divider from section above (stronger than $ins-border) + border-top: 1px solid var(--border-base); + text-align: left; + + &__blob, &__blob-2 { display: none; } + + &__bg { + display: block; + width: 100%; + box-sizing: border-box; + padding: $ins-lg $ins-xl; + border-radius: $ins-r-lg; + border: 1px solid $ins-border; + // subtle accent wash — distinct from stats / other cards, still theme-token + background: var(--color-accent-100); + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04); + + &:hover { + border-color: $ins-border-hover; + background: color-mix(in srgb, var(--color-accent-100) 100%, white 6%); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.18); + } + } &__headline { - font-size: $font-size-base; - font-weight: $font-weight-semibold; - color: $color-warning; - margin-bottom: $size-gap-1; + font-size: 18px; + font-weight: 600; + color: var(--color-text-primary); + margin: 0 0 $ins-sm; + letter-spacing: -0.02em; + line-height: 1.25; } &__message { - font-size: $font-size-sm; + font-size: $ins-body; color: var(--color-text-secondary); + line-height: 1.7; margin: 0; + max-width: none; + text-align: left; } } diff --git a/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx b/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx index 90a74a14..09e87329 100644 --- a/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx +++ b/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useState, useRef } from 'react'; import { ExternalLink, Copy, Check, ArrowLeft, Loader2, AlertTriangle, - BarChart3, Clock, MessageSquare, Calendar, TrendingUp, X, - FileCode, FolderEdit, + BarChart3, MessageSquare, Calendar, Clock, X, Target, Zap, Trophy, + AlertCircle, Lightbulb, Rocket, } from 'lucide-react'; import { openPath } from '@tauri-apps/plugin-opener'; import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; @@ -14,6 +14,18 @@ import './InsightsScene.scss'; const log = createLogger('InsightsScene'); +// Report section ids for TOC / scroll targets +const SECTIONS = [ + { id: 'overview', labelKey: 'overview', icon: Target }, + { id: 'stats', labelKey: 'stats', icon: BarChart3 }, + { id: 'work-on', labelKey: 'workOn', icon: Target }, + { id: 'usage', labelKey: 'usage', icon: Zap }, + { id: 'wins', labelKey: 'wins', icon: Trophy }, + { id: 'friction', labelKey: 'friction', icon: AlertCircle }, + { id: 'suggestions', labelKey: 'suggestions', icon: Lightbulb }, + { id: 'horizon', labelKey: 'horizon', icon: Rocket }, +] as const; + const DAY_OPTIONS = [7, 14, 30, 90] as const; const InsightsScene: React.FC = () => { @@ -35,9 +47,41 @@ const InsightsScene: React.FC = () => { return (
-
- -

{t('insights.title')}

+
+

{t('insights.title')}

+
+

{t('insights.subtitle')}

+
+
+ {DAY_OPTIONS.map((d) => ( + + ))} +
+ {generating ? ( + + ) : ( + + )} +
+
@@ -49,47 +93,14 @@ const InsightsScene: React.FC = () => {
)} -
-
{t('insights.generateNew')}
-
-
- {DAY_OPTIONS.map((d) => ( - - ))} -
- {generating ? ( - - ) : ( - - )} - {generating && progress.message && ( -
- {progress.current > 0 && progress.total > 0 && ( - {progress.current}/{progress.total} - )} - {progress.message} -
+ {generating && progress.message && ( +
+ {progress.current > 0 && progress.total > 0 && ( + {progress.current}/{progress.total} )} + {progress.message}
-
+ )}
@@ -166,8 +177,83 @@ const ReportMetaCard: React.FC<{ // ============ Report View ============ +// Report view: right-hand TOC / section nav +const ReportNav: React.FC<{ report: InsightsReport; scrollContainerRef: React.RefObject }> = ({ report, scrollContainerRef }) => { + const { t } = useI18n('common'); + const [activeSection, setActiveSection] = useState('overview'); + + // Sections shown in the nav (skip empty blocks) + const visibleSections = [ + { id: 'overview', label: t('insights.atAGlance'), hasContent: true }, + { id: 'stats', label: t('insights.stats'), hasContent: true }, + { id: 'work-on', label: t('insights.projectAreas'), hasContent: report.project_areas.length > 0 }, + { id: 'usage', label: t('insights.interactionStyle'), hasContent: report.interaction_style.narrative || true }, + { id: 'wins', label: t('insights.bigWins'), hasContent: report.big_wins.length > 0 }, + { id: 'friction', label: t('insights.friction'), hasContent: report.friction_categories.length > 0 }, + { id: 'suggestions', label: t('insights.suggestions'), hasContent: true }, + { id: 'horizon', label: t('insights.horizon'), hasContent: report.on_the_horizon.length > 0 }, + ].filter(s => s.hasContent); + + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + const handleScroll = () => { + const scrollTop = container.scrollTop; + const containerTop = container.getBoundingClientRect().top; + const sections = container.querySelectorAll('[data-section]'); + let current = 'overview'; + + sections.forEach((section) => { + const el = section as HTMLElement; + const elTop = el.getBoundingClientRect().top - containerTop; + // Active section: heading within 80px of the scroll container top + if (scrollTop > 0 ? elTop < 80 : elTop <= 0) { + current = el.dataset.section || 'overview'; + } + }); + + setActiveSection(current); + }; + + container.addEventListener('scroll', handleScroll, { passive: true }); + handleScroll(); + return () => container.removeEventListener('scroll', handleScroll); + }, [scrollContainerRef]); + + const scrollToSection = (id: string) => { + const container = scrollContainerRef.current; + if (!container) return; + const element = container.querySelector(`[data-section="${id}"]`) as HTMLElement | null; + if (!element) return; + const containerTop = container.getBoundingClientRect().top; + const elTop = element.getBoundingClientRect().top - containerTop; + container.scrollBy({ top: elTop - 16, behavior: 'smooth' }); + }; + + return ( + + ); +}; + const ReportView: React.FC<{ report: InsightsReport; onBack: () => void }> = ({ report, onBack }) => { const { t } = useI18n('common'); + const bodyRef = useRef(null); const handleOpenHtml = useCallback(async () => { if (report.html_report_path) { @@ -190,121 +276,149 @@ const ReportView: React.FC<{ report: InsightsReport; onBack: () => void }> = ({
+
+ {report.total_messages} {t('insights.messages')} + {report.total_sessions} {t('insights.sessions')} + {dateStart} ~ {dateEnd} +
-
- {report.total_messages} {t('insights.messages')} - {report.total_sessions} {t('insights.sessions')} ({report.analyzed_sessions} {t('insights.analyzed')}) - {dateStart} ~ {dateEnd} -
- -
- - - - {/* What You Work On */} - {report.project_areas.length > 0 && ( -
-

{t('insights.projectAreas')}

-
- {report.project_areas.map((area) => ( -
-
- {area.name} - ~{area.session_count} {t('insights.sessions')} -
-

-
- ))} +
+
+
+
+

+ {t('insights.reportTitle', { dateStart, dateEnd })} +

+
+
+
-
- )} - - - {/* How You Use BitFun */} - {report.interaction_style.narrative && } - - - {/* Impressive Things You Did */} - {report.big_wins.length > 0 && ( -
-

{t('insights.bigWins')}

-
- {report.big_wins.map((win) => ( -
-
{win.title}
-

- {win.impact &&

} -
- ))} +
+
-
- )} - - - {/* Where Things Go Wrong */} - {report.friction_categories.length > 0 && ( -
-

{t('insights.friction')}

-
- {report.friction_categories.map((f) => ( -
-
{f.category}
-

- {f.examples.length > 0 && ( -
    - {f.examples.map((ex, j) =>
  • )} -
- )} - {f.suggestion &&
} + + {/* What You Work On */} + {report.project_areas.length > 0 && ( +
+

{t('insights.projectAreas')}

+
+ {report.project_areas.map((area) => ( +
+
+ {area.name} + ~{area.session_count} {t('insights.sessions')} +
+

+
+ ))}
- ))} -
-
- )} - + + )} + - + {/* How You Use BitFun */} + {report.interaction_style.narrative &&
} +
+ +
- {report.on_the_horizon.length > 0 && ( -
-

{t('insights.horizon')}

- {report.horizon_intro && ( -

- )} -
- {report.on_the_horizon.map((h) => ( -
-
{h.title}
-

- {h.how_to_try && ( -

- )} -
- ))} -
-
- )} + {/* Impressive Things You Did */} + {report.big_wins.length > 0 && ( +
+

{t('insights.bigWins')}

+
+ {report.big_wins.map((win) => ( +
+
{win.title}
+

+ {win.impact &&

} +
+ ))} +
+
+ )} + + + {/* Where Things Go Wrong */} + {report.friction_categories.length > 0 && ( +
+

{t('insights.friction')}

+
+ {report.friction_categories.map((f) => ( +
+
{f.category}
+

+ {f.examples.length > 0 && ( +
    + {f.examples.map((ex, j) =>
  • )} +
+ )} + {f.suggestion &&
} +
+ ))} +
+
+ )} + - {report.fun_ending && ( -
-
{report.fun_ending.headline}
-

+
+
- )} + + {report.on_the_horizon.length > 0 && ( +
+

{t('insights.horizon')}

+ {report.horizon_intro && ( +

+ )} +
+ {report.on_the_horizon.map((h, i) => ( +
+ {i + 1} +
+
{h.title}
+

+ {h.how_to_try && ( +
+ {t('insights.howToTry')} + +
+ )} +
+
+ ))} +
+
+ )} + + {report.fun_ending && ( +
+
+
+
+
{report.fun_ending.headline}
+

+
+
+ )} +
+
+ + } />
); @@ -380,28 +494,59 @@ const StatsRow: React.FC<{ report: InsightsReport }> = ({ report }) => { const { stats } = report; const hasCodeChanges = (stats.total_lines_added ?? 0) > 0 || (stats.total_lines_removed ?? 0) > 0; + const items: Array<{ key: string; value: string; label: string }> = [ + { key: 'messages', value: report.total_messages.toString(), label: t('insights.messages') }, + { key: 'sessions', value: report.total_sessions.toString(), label: t('insights.sessions') }, + { key: 'hours', value: `${stats.total_hours.toFixed(1)}h`, label: t('insights.hours') }, + { key: 'days', value: report.days_covered.toString(), label: t('insights.days') }, + { key: 'msgsPerDay', value: stats.msgs_per_day.toFixed(1), label: t('insights.msgsPerDay') }, + ]; + if (hasCodeChanges) { + items.push({ + key: 'lines', + value: `+${formatNumber(stats.total_lines_added)}/-${formatNumber(stats.total_lines_removed)}`, + label: t('insights.lines'), + }); + } + if ((stats.total_files_modified ?? 0) > 0) { + items.push({ + key: 'files', + value: formatNumber(stats.total_files_modified), + label: t('insights.files'), + }); + } + if (stats.median_response_time_secs != null) { + items.push({ + key: 'medianRt', + value: formatDurationShort(stats.median_response_time_secs), + label: t('insights.medianResponseTime'), + }); + } + if (stats.avg_response_time_secs != null) { + items.push({ + key: 'avgRt', + value: formatDurationShort(stats.avg_response_time_secs), + label: t('insights.avgResponseTime'), + }); + } + + const mid = Math.ceil(items.length / 2); + const row1 = items.slice(0, mid); + const row2 = items.slice(mid); + return ( -
- {hasCodeChanges && ( - } - value={`+${formatNumber(stats.total_lines_added)}/-${formatNumber(stats.total_lines_removed)}`} - label={t('insights.lines')} - /> - )} - {hasCodeChanges && (stats.total_files_modified ?? 0) > 0 && ( - } value={formatNumber(stats.total_files_modified)} label={t('insights.files')} /> - )} - } value={report.total_sessions.toString()} label={t('insights.sessions')} /> - } value={report.total_messages.toString()} label={t('insights.messages')} /> - } value={stats.total_hours.toFixed(1)} label={t('insights.hours')} /> - } value={report.days_covered.toString()} label={t('insights.days')} /> - } value={stats.msgs_per_day.toFixed(1)} label={t('insights.msgsPerDay')} /> - {stats.median_response_time_secs != null && ( - } value={formatDurationShort(stats.median_response_time_secs)} label={t('insights.medianResponseTime')} /> - )} - {stats.avg_response_time_secs != null && ( - } value={formatDurationShort(stats.avg_response_time_secs)} label={t('insights.avgResponseTime')} /> +
+
+ {row1.map((it) => ( + + ))} +
+ {row2.length > 0 && ( +
+ {row2.map((it) => ( + + ))} +
)}
); @@ -430,13 +575,13 @@ const BasicCharts: React.FC<{ stats: InsightsStats }> = ({ stats }) => { return ( <> - {hasGoals && } - {hasTools && } + {hasGoals && } + {hasTools && } {(hasLangs || hasTypes) && ( - {hasLangs && } - {hasTypes && } + {hasLangs && } + {hasTypes && } )} @@ -473,8 +618,8 @@ const UsageCharts: React.FC<{ stats: InsightsStats }> = ({ stats }) => { )} @@ -484,16 +629,16 @@ const UsageCharts: React.FC<{ stats: InsightsStats }> = ({ stats }) => { )} {hasToolErrors && ( b - a).slice(0, 6)} - color="#dc2626" max={6} + color={CHART_COLORS.red} /> )} @@ -503,8 +648,8 @@ const UsageCharts: React.FC<{ stats: InsightsStats }> = ({ stats }) => { b - a)} - color="#f97316" max={6} + color={CHART_COLORS.purple} /> )} @@ -527,16 +672,16 @@ const OutcomeCharts: React.FC<{ stats: InsightsStats }> = ({ stats }) => { b - a).slice(0, 6)} - color="#16a34a" max={6} + color={CHART_COLORS.green} /> )} {hasOutcomes && ( b - a).slice(0, 6)} - color="#8b5cf6" max={6} + color={CHART_COLORS.purple} /> )} @@ -558,37 +703,50 @@ const FrictionCharts: React.FC<{ stats: InsightsStats }> = ({ stats }) => { b - a).slice(0, 6)} - color="#dc2626" max={6} + color={CHART_COLORS.red} /> )} {hasSatisfaction && ( b - a).slice(0, 6)} - color="#eab308" max={6} + color={CHART_COLORS.orange} /> )} ); }; -const StatItem: React.FC<{ icon: React.ReactNode; value: string; label: string }> = ({ icon, value, label }) => ( +const StatItem: React.FC<{ value: string; label: string }> = ({ value, label }) => (
-
{icon}
-
{value}
-
{label}
+ {value} + {label}
); -const BarChart: React.FC<{ title: string; items: [string, number][]; color: string; max: number }> = ({ title, items, color, max }) => { +// Bar chart palette (default + semantic roles) +const CHART_COLORS = { + blue: '#60a5fa', // default / primary series + green: '#6eb88c', // positive / success + purple: '#8b5cf6', // distribution / category + indigo: '#818cf8', // time-related + orange: '#c9944d', // time-of-day / neutral + red: '#c77070', // issues / errors +} as const; + +type ChartColor = typeof CHART_COLORS[keyof typeof CHART_COLORS]; + +const BarChart: React.FC<{ title: string; items: [string, number][]; max: number; color?: ChartColor }> = ({ title, items, max, color }) => { const nonZero = items.filter(([, v]) => v > 0); const displayed = nonZero.slice(0, max); const maxVal = Math.max(...displayed.map(([, v]) => v), 1); if (displayed.length === 0) return null; + const barColor = color || CHART_COLORS.blue; + return (
{title}
@@ -599,7 +757,7 @@ const BarChart: React.FC<{ title: string; items: [string, number][]; color: stri
{displayLabel}
-
+
{value}
@@ -624,39 +782,44 @@ const SuggestionsSection: React.FC<{ report: InsightsReport }> = ({ report }) =>

{t('insights.suggestions')}

{suggestions.bitfun_md_additions.length > 0 && ( -
+

{t('insights.mdAdditions')}

{suggestions.bitfun_md_additions.map((md, i) => ( -
- {md.section &&
{md.section}
} +
+
+ {md.section && {md.section}} +
-

{md.rationale}

+ {md.rationale &&

{md.rationale}

}
))}
)} {suggestions.features_to_try.length > 0 && ( -
+

{t('insights.featuresToTry')}

- {suggestions.features_to_try.map((f) => ( -
-
{f.feature}
-

-

- {f.example_usage && } + {suggestions.features_to_try.map((f, i) => ( +
+ {i + 1} +
+
{f.feature}
+

+ {f.benefit &&

} + {f.example_usage && } +
))}
)} {suggestions.usage_patterns.length > 0 && ( -
+

{t('insights.usagePatterns')}

{suggestions.usage_patterns.map((p) => ( -
-
{p.pattern}
-

+
+
{p.pattern}
+

{p.suggested_prompt && }
))} diff --git a/src/web-ui/src/app/scenes/settings/SettingsScene.scss b/src/web-ui/src/app/scenes/settings/SettingsScene.scss index 521eb176..f8973780 100644 --- a/src/web-ui/src/app/scenes/settings/SettingsScene.scss +++ b/src/web-ui/src/app/scenes/settings/SettingsScene.scss @@ -2,6 +2,17 @@ * SettingsScene styles. */ +@keyframes settings-content-enter { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + .bitfun-settings-scene { display: flex; flex-direction: column; @@ -12,4 +23,18 @@ &__loading { flex: 1; } + + &__content-wrapper { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + animation: settings-content-enter 220ms cubic-bezier(0.25, 1, 0.5, 1) both; + } +} + +@media (prefers-reduced-motion: reduce) { + .bitfun-settings-scene__content-wrapper { + animation: none; + } } diff --git a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx index 9a6bf3e7..bad7a09c 100644 --- a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx +++ b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx @@ -41,7 +41,11 @@ const SettingsScene: React.FC = () => { return (
}> - {Content && } + {Content && ( +
+ +
+ )}
); diff --git a/src/web-ui/src/component-library/styles/tokens.scss b/src/web-ui/src/component-library/styles/tokens.scss index 34cf5e1a..57b05d8f 100644 --- a/src/web-ui/src/component-library/styles/tokens.scss +++ b/src/web-ui/src/component-library/styles/tokens.scss @@ -298,6 +298,8 @@ $font-weight-medium: 500; $font-weight-semibold: 600; $font-weight-bold: 600; // Optimized: use 600 instead of 700; Bold font removed +$font-size-xxs: 10px; +$font-size-2xs: 11px; $font-size-xs: 12px; $font-size-sm: 14px; $font-size-base: 15px; @@ -920,6 +922,8 @@ $badge-info-text: $color-info; --font-weight-semibold: #{$font-weight-semibold}; --font-weight-bold: #{$font-weight-bold}; + --font-size-xxs: #{$font-size-xxs}; + --font-size-2xs: #{$font-size-2xs}; --font-size-xs: #{$font-size-xs}; --font-size-sm: #{$font-size-sm}; --font-size-base: #{$font-size-base}; diff --git a/src/web-ui/src/infrastructure/config/components/LoggingConfig.scss b/src/web-ui/src/infrastructure/config/components/LoggingConfig.scss index 5799a6ca..371acfcc 100644 --- a/src/web-ui/src/infrastructure/config/components/LoggingConfig.scss +++ b/src/web-ui/src/infrastructure/config/components/LoggingConfig.scss @@ -9,6 +9,50 @@ width: 100%; } + &__path-row { + display: flex; + align-items: stretch; + gap: 8px; + width: 100%; + + .bitfun-logging-config__path-box { + flex: 1; + min-width: 0; + } + } + + &__open-btn { + flex-shrink: 0; + align-self: stretch; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + min-width: 36px; + padding: 0; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; + + &:hover:not(:disabled) { + background: var(--element-bg-base, rgba(255, 255, 255, 0.08)); + border-color: var(--border-subtle); + color: var(--color-text-primary); + } + + &:active:not(:disabled) { + background: var(--element-bg-medium, rgba(255, 255, 255, 0.14)); + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + } + &__path-box { width: 100%; padding: 10px 12px; @@ -19,5 +63,8 @@ font-family: var(--font-family-mono, Consolas, "Courier New", monospace); font-size: 12px; word-break: break-all; + overflow-wrap: anywhere; + line-height: 1.6; + user-select: text; } } diff --git a/src/web-ui/src/infrastructure/config/components/LoggingConfig.tsx b/src/web-ui/src/infrastructure/config/components/LoggingConfig.tsx index 5b17d949..ff7184ae 100644 --- a/src/web-ui/src/infrastructure/config/components/LoggingConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/LoggingConfig.tsx @@ -2,8 +2,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FolderOpen } from 'lucide-react'; import { - Button, Select, + Tooltip, ConfigPageLoading, ConfigPageMessage, ConfigPageRefreshButton, @@ -103,7 +103,6 @@ const LoggingConfig: React.FC = () => { try { setOpeningFolder(true); await workspaceAPI.revealInExplorer(folder); - showMessage('success', t('messages.openedFolder')); } catch (error) { log.error('Failed to open log folder', { folder, error }); showMessage('error', t('messages.openFailed')); @@ -156,26 +155,25 @@ const LoggingConfig: React.FC = () => { - - - {t('actions.openFolder')} - - )} - > + -
- {runtimeInfo?.sessionLogDir || '-'} +
+
+ {runtimeInfo?.sessionLogDir || '-'} +
+ + +
diff --git a/src/web-ui/src/infrastructure/config/components/TerminalConfig.scss b/src/web-ui/src/infrastructure/config/components/TerminalConfig.scss index d1608137..3d6b2793 100644 --- a/src/web-ui/src/infrastructure/config/components/TerminalConfig.scss +++ b/src/web-ui/src/infrastructure/config/components/TerminalConfig.scss @@ -24,106 +24,12 @@ padding: $size-gap-3; } - &__cli-agents-list { - border-top: 1px solid var(--border-subtle); - padding: $size-gap-3; - display: flex; - flex-direction: column; - gap: $size-gap-2; - } - - &__cli-agent-item { - display: flex; - align-items: center; - justify-content: space-between; - gap: $size-gap-3; - padding: $size-gap-3; - border: 1px solid var(--border-base); - border-radius: $size-radius-sm; - background: transparent; - } - - &__cli-agent-info { - min-width: 0; - flex: 1; - } - - &__cli-agent-header { - display: flex; - align-items: center; - gap: $size-gap-2; - flex-wrap: wrap; - } - - &__cli-agent-name { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - } - - &__cli-agent-status { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - border-radius: 9999px; - font-size: $font-size-xs; - font-weight: $font-weight-medium; - - &.checking { - background: var(--element-bg-subtle); - color: var(--color-text-muted); - } - - &.available { - background: var(--color-success-bg); - color: var(--color-success); - } - - &.unavailable { - background: var(--element-bg-subtle); - color: var(--color-text-muted); - } - } - - &__status-spinner { - width: 12px; - height: 12px; - border: 2px solid currentColor; - border-right-color: transparent; - border-radius: 50%; - animation: bitfun-terminal-config-spin 0.9s linear infinite; - } - - &__cli-agent-actions { - display: flex; - align-items: center; - gap: $size-gap-1; - flex-shrink: 0; - } - -} - -@keyframes bitfun-terminal-config-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } } @container config-panel (max-width: 520px) { .bitfun-terminal-config { - &__inline-alert, - &__cli-agents-list { + &__inline-alert { padding: $size-gap-2; } - - &__cli-agent-item { - flex-direction: column; - align-items: flex-start; - } - - &__cli-agent-actions { - width: 100%; - flex-wrap: wrap; - } } } diff --git a/src/web-ui/src/infrastructure/config/components/TerminalConfig.tsx b/src/web-ui/src/infrastructure/config/components/TerminalConfig.tsx index 1ca7c006..0cff0aa0 100644 --- a/src/web-ui/src/infrastructure/config/components/TerminalConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/TerminalConfig.tsx @@ -1,13 +1,11 @@ - + import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { Download, CheckCircle, XCircle, Layers, ExternalLink } from 'lucide-react'; +import { Download } from 'lucide-react'; import { - Button, Alert, Select, - Tooltip, ConfigPageLoading, ConfigPageMessage, ConfigPageRefreshButton, @@ -16,8 +14,6 @@ import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSectio import { configManager } from '../services/ConfigManager'; import { getTerminalService } from '@/tools/terminal'; import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI'; -import { createTerminalTab } from '@/shared/utils/tabUtils'; -import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; import { createLogger } from '@/shared/utils/logger'; import type { TerminalConfig as TerminalConfigType } from '../types'; import type { ShellInfo } from '@/tools/terminal/types/session'; @@ -25,74 +21,6 @@ import './TerminalConfig.scss'; const log = createLogger('TerminalConfig'); - - - -interface CLIAgentInfo { - id: string; - name: string; - command: string; - startUpCommand: string; - installCommandWin: string; - installCommandUnix: string; - websiteUrl: string; - requiresNpm: boolean; -} - - -interface CLIAgentStatus { - exists: boolean; - path: string | null; - checking: boolean; - installing: boolean; -} - - -const CLI_AGENTS: CLIAgentInfo[] = [ - { - id: 'claude', - name: 'Claude Code', - command: 'claude', - startUpCommand: ' claude', - // installCommandWin: ' irm https://claude.ai/install.ps1 | iex', - // installCommandUnix: ' curl -fsSL https://claude.ai/install.sh | bash', - installCommandWin: ' npm install -g @anthropic-ai/claude-code', - installCommandUnix: ' npm install -g @anthropic-ai/claude-code', - websiteUrl: 'https://claude.com/product/claude-code', - requiresNpm: true, - }, - { - id: 'codex', - name: 'CodeX', - command: 'codex', - startUpCommand: ' codex', - installCommandWin: ' npm i -g @openai/codex', - installCommandUnix: ' npm i -g @openai/codex', - websiteUrl: 'https://openai.com/codex/', - requiresNpm: true, - }, - { - id: 'gemini', - name: 'Gemini CLI', - command: 'gemini', - startUpCommand: ' gemini', - installCommandWin: ' npm i -g @google/gemini-cli', - installCommandUnix: ' npm i -g @google/gemini-cli', - websiteUrl: 'https://geminicli.com/', - requiresNpm: true, - }, - { - id: 'opencode', - name: 'OpenCode', - command: 'opencode', - startUpCommand: ' opencode', - installCommandWin: ' npm i -g opencode-ai', - installCommandUnix: ' npm i -g opencode-ai', - websiteUrl: 'https://opencode.ai/', - requiresNpm: true, - }, -]; - const TerminalConfig: React.FC = () => { const { t } = useTranslation('settings/terminal'); const [defaultShell, setDefaultShell] = useState(''); @@ -101,17 +29,7 @@ const TerminalConfig: React.FC = () => { const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error' | 'info'; text: string } | null>(null); const [platform, setPlatform] = useState(''); - - - const [cliAgentStatus, setCLIAgentStatus] = useState>({}); - - - const [npmAvailable, setNpmAvailable] = useState(null); - - - const { workspacePath } = useCurrentWorkspace(); - useEffect(() => { loadData(); }, []); @@ -120,25 +38,18 @@ const TerminalConfig: React.FC = () => { try { setLoading(true); - const [terminalConfig, shells, systemInfo] = await Promise.all([ configManager.getConfig('terminal'), getTerminalService().getAvailableShells(), - systemAPI.getSystemInfo().catch(() => ({ platform: '' })) + systemAPI.getSystemInfo().catch(() => ({ platform: '' })) ]); - setDefaultShell(terminalConfig?.default_shell || ''); - const availableOnly = shells.filter(s => s.available); setAvailableShells(availableOnly); - setPlatform(systemInfo.platform || ''); - - - await checkCLIAgents(); } catch (error) { log.error('Failed to load terminal config data', error); showMessage('error', t('messages.loadFailed')); @@ -146,63 +57,16 @@ const TerminalConfig: React.FC = () => { setLoading(false); } }; - - - const checkCLIAgents = async () => { - - const initialStatus: Record = {}; - CLI_AGENTS.forEach(agent => { - initialStatus[agent.id] = { exists: false, path: null, checking: true, installing: false }; - }); - setCLIAgentStatus(initialStatus); - setNpmAvailable(null); - - try { - - const commands = [...CLI_AGENTS.map(agent => agent.command), 'npm']; - const results = await systemAPI.checkCommandsExist(commands); - - const newStatus: Record = {}; - results.forEach(([command, result]) => { - - if (command === 'npm') { - setNpmAvailable(result.exists); - return; - } - - const agent = CLI_AGENTS.find(a => a.command === command); - if (agent) { - newStatus[agent.id] = { - exists: result.exists, - path: result.path, - checking: false, - installing: false, - }; - } - }); - setCLIAgentStatus(newStatus); - } catch (error) { - log.error('Failed to check CLI agents', { error }); - const errorStatus: Record = {}; - CLI_AGENTS.forEach(agent => { - errorStatus[agent.id] = { exists: false, path: null, checking: false, installing: false }; - }); - setCLIAgentStatus(errorStatus); - setNpmAvailable(false); - } - }; const handleShellChange = useCallback(async (value: string) => { try { setSaving(true); setDefaultShell(value); - - + await configManager.setConfig('terminal.default_shell', value); - - + configManager.clearCache(); - + showMessage('success', t('messages.updated')); } catch (error) { log.error('Failed to save terminal config', { shell: value, error }); @@ -210,7 +74,7 @@ const TerminalConfig: React.FC = () => { } finally { setSaving(false); } - }, []); + }, [t]); const showMessage = (type: 'success' | 'error' | 'info', text: string) => { setMessage({ type, text }); @@ -221,73 +85,11 @@ const TerminalConfig: React.FC = () => { await loadData(); showMessage('info', t('messages.refreshed')); }, [t]); - - - const handleRefreshCLIAgents = useCallback(async () => { - await checkCLIAgents(); - showMessage('info', t('messages.cliRefreshed')); - }, [t]); - - - const handleInstallAgent = useCallback(async (agent: CLIAgentInfo) => { - const terminalService = getTerminalService(); - - - setCLIAgentStatus(prev => ({ - ...prev, - [agent.id]: { ...prev[agent.id], installing: true } - })); - - try { - - const isWindows = platform === 'windows'; - const installCommand = isWindows ? agent.installCommandWin : agent.installCommandUnix; - - - const session = await terminalService.createSession({ - workingDirectory: workspacePath || undefined, - name: t('cliAgents.installSessionName', { name: agent.name }), - }); - - - createTerminalTab(session.id, t('cliAgents.installTabTitle', { name: agent.name })); - - - await terminalService.sendCommand(session.id, installCommand); - - showMessage('info', t('messages.installingAgent', { name: agent.name })); - } catch (error) { - log.error('Failed to install CLI agent', { agentName: agent.name, agentId: agent.id, error }); - showMessage('error', t('messages.installAgentFailed', { name: agent.name })); - } finally { - - setCLIAgentStatus(prev => ({ - ...prev, - [agent.id]: { ...prev[agent.id], installing: false } - })); - } - }, [platform, workspacePath]); - - - const handleOpenInHub = useCallback((agent: CLIAgentInfo) => { - - window.dispatchEvent(new CustomEvent('create-hub-terminal', { - detail: { - name: agent.name, - startupCommand: agent.startUpCommand, - } - })); - - showMessage('success', t('messages.hubCreated', { name: agent.name })); - }, [t]); - const shouldShowPowerShellCoreRecommendation = useCallback(() => { - const isWindows = platform === 'windows'; if (!isWindows) return false; - const hasPowerShellCore = availableShells.some( shell => shell.shellType === 'PowerShellCore' ); @@ -295,13 +97,6 @@ const TerminalConfig: React.FC = () => { return !hasPowerShellCore; }, [availableShells, platform]); - - const shouldShowNpmRecommendation = useCallback(() => { - - return npmAvailable === false; - }, [npmAvailable]); - - const shellOptions = [ { value: '', label: t('terminal.autoDetect') }, ...availableShells.map(shell => ({ @@ -330,12 +125,11 @@ const TerminalConfig: React.FC = () => { title={t('title')} subtitle={t('subtitle')} /> - + - + - {shouldShowPowerShellCoreRecommendation() && (
{
)} - {
)}
- - - )} - > - {shouldShowNpmRecommendation() && ( -
- - {t('recommendations.npm.prefix')} - {t('recommendations.npm.name')} - {t('recommendations.npm.suffix')} - - {t('recommendations.npm.link')} - - - } - /> -
- )} - -
- {CLI_AGENTS.map(agent => { - const status = cliAgentStatus[agent.id] || { exists: false, path: null, checking: true, installing: false }; - return ( -
-
-
- {agent.name} - - {status.checking ? ( -
- ) : status.exists ? ( - - ) : ( - - )} - {status.checking ? t('cliAgents.checking') : status.exists ? t('cliAgents.available') : t('cliAgents.notInstalled')} - -
-
- -
- {agent.websiteUrl && ( - - - - )} - {!status.exists && !status.checking && ( - - - - )} - - - -
-
- ); - })} -
- ); }; export default TerminalConfig; - diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index 3b980f34..3dd05471 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -163,6 +163,7 @@ "confirmEdit": "Confirm", "cancelEdit": "Cancel", "showMore": "{{count}} more sessions", + "showAll": "{{count}} more (show all)", "showLess": "Show less" }, "scheduledJobs": { @@ -818,7 +819,9 @@ }, "insights": { "title": "Insights", + "subtitle": "Analyze your usage patterns and productivity insights", "buttonTooltip": "Insights", + "reportTitle": "Insights Report ({{dateStart}} ~ {{dateEnd}})", "generating": "Generating insights...", "generateNew": "Generate New Report", "generateBtn": "Generate", @@ -827,6 +830,7 @@ "backToList": "Back to list", "openHtml": "Open Full Report", "atAGlance": "At a Glance", + "stats": "Key metrics", "whatsWorking": "What's working", "whatsHindering": "What's hindering you", "quickWins": "Quick wins to try", @@ -851,6 +855,7 @@ "usagePatterns": "Usage Patterns", "tryThisPrompt": "Try this prompt", "horizon": "On the Horizon", + "howToTry": "How to try", "error": "Failed to generate insights report", "impact": "Impact", "rateLimited": "Rate limited, retrying sequentially...", diff --git a/src/web-ui/src/locales/en-US/settings/logging.json b/src/web-ui/src/locales/en-US/settings/logging.json index da517272..59aaad4c 100644 --- a/src/web-ui/src/locales/en-US/settings/logging.json +++ b/src/web-ui/src/locales/en-US/settings/logging.json @@ -18,7 +18,8 @@ }, "actions": { "refreshTooltip": "Refresh runtime status", - "openFolder": "Open Folder" + "openFolder": "Open Folder", + "openFolderTooltip": "Open log folder in file explorer" }, "messages": { "loading": "Loading...", diff --git a/src/web-ui/src/locales/en-US/settings/terminal.json b/src/web-ui/src/locales/en-US/settings/terminal.json index fae8aaa3..8004f8d7 100644 --- a/src/web-ui/src/locales/en-US/settings/terminal.json +++ b/src/web-ui/src/locales/en-US/settings/terminal.json @@ -1,9 +1,8 @@ { "title": "Terminal", - "subtitle": "Terminal configuration and CLI tool management", + "subtitle": "Terminal configuration", "sections": { - "defaultTerminal": "Default Terminal", - "cliAgents": "CLI Agents" + "defaultTerminal": "Default Terminal" }, "terminal": { "refreshTooltip": "Refresh available terminals", @@ -16,34 +15,12 @@ "gitBash": "Git Bash with oh-my-zsh may cause shell integration to fail. If issues occur, switch to PowerShell." } }, - "cliAgents": { - "description": "Manage AI programming assistant CLI tools, quickly launch in Terminal Workshop", - "refreshTooltip": "Refresh CLI Agents status", - "checking": "Checking...", - "available": "Available", - "notInstalled": "Not Installed", - "install": "Install", - "installing": "Installing...", - "installSessionName": "Install {{name}}", - "installTabTitle": "Install {{name}}", - "launch": "Launch", - "visitWebsite": "Visit {{name}} website", - "openInHub": "Open {{name}} in Terminal Workshop", - "needInstall": "Please install this tool first", - "needNpm": "Requires Node.js (npm) installation first" - }, "recommendations": { "pwsh": { "prefix": "We recommend installing", "name": "PowerShell Core", "suffix": "for better cross-platform experience and modern features", "link": "Download" - }, - "npm": { - "prefix": "Some tools require", - "name": "Node.js (npm)", - "suffix": "to install", - "link": "Download" } }, "messages": { @@ -51,10 +28,6 @@ "loadFailed": "Failed to load configuration", "updated": "Default terminal updated", "saveFailed": "Failed to save configuration", - "refreshed": "Available terminals list refreshed", - "cliRefreshed": "CLI Agents status refreshed", - "installingAgent": "Installing {{name}}, check progress in terminal", - "installAgentFailed": "Failed to install {{name}}", - "hubCreated": "Created {{name}} terminal in Terminal Workshop" + "refreshed": "Available terminals list refreshed" } } diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index e38d664c..147f56e8 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -163,6 +163,7 @@ "confirmEdit": "确认", "cancelEdit": "取消", "showMore": "还有 {{count}} 个会话", + "showAll": "还有 {{count}} 个(展开全部)", "showLess": "收起" }, "scheduledJobs": { @@ -818,7 +819,9 @@ }, "insights": { "title": "洞察", + "subtitle": "分析你的使用习惯与效率洞察", "buttonTooltip": "洞察", + "reportTitle": "{{dateStart}} ~ {{dateEnd}} 洞察报告", "generating": "正在生成洞察...", "generateNew": "生成新报告", "generateBtn": "生成", @@ -827,6 +830,7 @@ "backToList": "返回列表", "openHtml": "打开完整报告", "atAGlance": "概览", + "stats": "关键指标", "whatsWorking": "做得好的", "whatsHindering": "遇到的阻碍", "quickWins": "快速提升", @@ -851,6 +855,7 @@ "usagePatterns": "使用模式", "tryThisPrompt": "试试这个提示", "horizon": "未来展望", + "howToTry": "如何尝试", "error": "生成洞察报告失败", "impact": "影响", "rateLimited": "请求频率受限,正在逐个重试...", diff --git a/src/web-ui/src/locales/zh-CN/settings/logging.json b/src/web-ui/src/locales/zh-CN/settings/logging.json index 49589ae6..67dabe2a 100644 --- a/src/web-ui/src/locales/zh-CN/settings/logging.json +++ b/src/web-ui/src/locales/zh-CN/settings/logging.json @@ -18,7 +18,8 @@ }, "actions": { "refreshTooltip": "刷新运行时状态", - "openFolder": "打开文件夹" + "openFolder": "打开文件夹", + "openFolderTooltip": "在文件管理器中打开日志文件夹" }, "messages": { "loading": "加载中...", diff --git a/src/web-ui/src/locales/zh-CN/settings/terminal.json b/src/web-ui/src/locales/zh-CN/settings/terminal.json index d0e2df69..e59f3564 100644 --- a/src/web-ui/src/locales/zh-CN/settings/terminal.json +++ b/src/web-ui/src/locales/zh-CN/settings/terminal.json @@ -1,9 +1,8 @@ { "title": "终端", - "subtitle": "终端配置与 CLI 工具管理", + "subtitle": "终端配置", "sections": { - "defaultTerminal": "默认终端", - "cliAgents": "CLI Agents" + "defaultTerminal": "默认终端" }, "terminal": { "refreshTooltip": "刷新可用终端列表", @@ -16,34 +15,12 @@ "gitBash": "Git Bash 安装 oh-my-zsh 后可能导致 Shell 集成失效,如遇异常建议切换至 PowerShell。" } }, - "cliAgents": { - "description": "管理 AI 编程助手命令行工具,可在终端工坊中快速启动", - "refreshTooltip": "刷新 CLI Agents 状态", - "checking": "检测中...", - "available": "可用", - "notInstalled": "未安装", - "install": "安装", - "installing": "安装中...", - "installSessionName": "安装 {{name}}", - "installTabTitle": "安装 {{name}}", - "launch": "启动", - "visitWebsite": "访问 {{name}} 官网", - "openInHub": "在终端工坊中打开 {{name}}", - "needInstall": "请先安装此工具", - "needNpm": "需要先安装 Node.js (npm)" - }, "recommendations": { "pwsh": { "prefix": "推荐安装使用", "name": "PowerShell Core", "suffix": "以获得更好的跨平台体验和现代化功能", "link": "前往下载" - }, - "npm": { - "prefix": "部分工具需要", - "name": "Node.js (npm)", - "suffix": "才能安装", - "link": "前往下载" } }, "messages": { @@ -51,10 +28,6 @@ "loadFailed": "加载配置失败", "updated": "默认终端已更新", "saveFailed": "保存配置失败", - "refreshed": "已刷新可用终端列表", - "cliRefreshed": "已刷新 CLI Agents 状态", - "installingAgent": "正在安装 {{name}},请在终端中查看进度", - "installAgentFailed": "安装 {{name}} 失败", - "hubCreated": "已在终端工坊中创建 {{name}} 终端" + "refreshed": "已刷新可用终端列表" } }