| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useEffect, useState } from "react"; |
|
|
| const BRAND_ORANGE = "#D95C3D"; |
|
|
| const DEFAULT_LABELS = [ |
| "Reading repository", |
| "Building plan", |
| "Checking context", |
| "Preparing response", |
| ]; |
|
|
| const ROTATION_MS = 1800; |
|
|
| |
| |
| const KEYFRAMES = ` |
| @keyframes gp-thinking-mount { |
| from { opacity: 0; transform: translateY(2px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| @keyframes gp-thinking-label { |
| from { opacity: 0; transform: translateY(1px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| @keyframes gp-thinking-dot-pulse { |
| 0%, 100% { opacity: 0.50; transform: scale(1); } |
| 50% { opacity: 1.00; transform: scale(1.18); } |
| } |
| @keyframes gp-thinking-trail { |
| 0%, 100% { opacity: 0.25; } |
| 50% { opacity: 0.90; } |
| } |
| `; |
|
|
| export default function ThinkingIndicator({ label, labels = DEFAULT_LABELS }) { |
| const [step, setStep] = useState(0); |
|
|
| |
| |
| |
| const useRotation = !label && Array.isArray(labels) && labels.length > 1; |
|
|
| useEffect(() => { |
| if (!useRotation) return undefined; |
| const id = setInterval( |
| () => setStep((prev) => (prev + 1) % labels.length), |
| ROTATION_MS, |
| ); |
| return () => clearInterval(id); |
| }, [labels, useRotation]); |
|
|
| const currentLabel = label || labels[step] || labels[0] || "Thinking"; |
|
|
| |
| |
| |
| const styles = { |
| bubble: { |
| display: "inline-flex", |
| alignItems: "center", |
| gap: "8px", |
| height: "32px", |
| padding: "0 12px", |
| borderRadius: "10px", |
| border: "1px solid rgba(255, 255, 255, 0.08)", |
| background: "rgba(255, 255, 255, 0.035)", |
| color: "rgba(255, 255, 255, 0.72)", |
| fontSize: "13px", |
| fontWeight: 500, |
| letterSpacing: "normal", |
| fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', |
| animation: "gp-thinking-mount 220ms ease-out both", |
| }, |
| brandDot: { |
| flex: "0 0 auto", |
| width: "6px", |
| height: "6px", |
| borderRadius: "999px", |
| background: BRAND_ORANGE, |
| animation: "gp-thinking-dot-pulse 1.35s ease-in-out infinite", |
| }, |
| label: { |
| minWidth: "120px", |
| animation: "gp-thinking-label 180ms ease-out both", |
| }, |
| trailRow: { |
| display: "inline-flex", |
| alignItems: "center", |
| gap: "3px", |
| paddingLeft: "2px", |
| }, |
| trailDot: { |
| width: "4px", |
| height: "4px", |
| borderRadius: "999px", |
| background: "currentColor", |
| animation: "gp-thinking-trail 1.2s ease-in-out infinite", |
| }, |
| }; |
|
|
| return ( |
| <div |
| className="gitpilot-thinking-indicator" |
| role="status" |
| aria-live="polite" |
| aria-label={`${currentLabel} in progress`} |
| style={styles.bubble} |
| > |
| <style>{KEYFRAMES}</style> |
| <span style={styles.brandDot} aria-hidden="true" /> |
| {/* keyed on the label so the fade-in plays each rotation */} |
| <span key={currentLabel} style={styles.label}> |
| {currentLabel} |
| </span> |
| <span style={styles.trailRow} aria-hidden="true"> |
| <span style={{ ...styles.trailDot, animationDelay: "0s" }} /> |
| <span style={{ ...styles.trailDot, animationDelay: "0.18s" }} /> |
| <span style={{ ...styles.trailDot, animationDelay: "0.36s" }} /> |
| </span> |
| </div> |
| ); |
| } |
|
|