add loading animation on streaming (#190)

This commit is contained in:
Ismail Pelaseyed authored and GitHub committed 2026-03-22 22:25:29 +01:00
1 parent 5245dcef1a
commit e98e36ddc6
1 file changed
+80 -3
+80 -3
View File
@@ -51,6 +51,12 @@ import { dark, type Theme } from "./theme";
const STAR_PALETTE = ["#777777", "#666666", "#4a4a4a", "#333333", "#222222"];
const LOADING_SPINNER_FRAMES = ["⬒", "⬔", "⬓", "⬕"];
const PROMPT_LOADING_FRAMES = [
{ active: 0, forward: true },
{ active: 1, forward: true },
{ active: 2, forward: true },
{ active: 1, forward: false },
] as const;
type Star = { col: number; ch: string };
type Row = { stars: Star[]; grok?: number };
@@ -2252,9 +2258,7 @@ function PromptBox({
alignItems="flex-start"
flexShrink={0}
>
<text fg={modeInfo.color}>
<b>{modeInfo.label}</b>
</text>
<PromptModeLabel t={t} modeInfo={modeInfo} isProcessing={isProcessing} />
<box flexGrow={1}>
<textarea
ref={inputRef}
@@ -2316,6 +2320,79 @@ function PromptBox({
);
}
function PromptModeLabel({
t,
modeInfo,
isProcessing,
}: {
t: Theme;
modeInfo: (typeof MODES)[number];
isProcessing: boolean;
}) {
if (!isProcessing) {
return (
<text fg={modeInfo.color}>
<b>{modeInfo.label}</b>
</text>
);
}
return <PromptLoadingBoxes t={t} color={modeInfo.color} />;
}
function PromptLoadingBoxes({ t, color }: { t: Theme; color: string }) {
const [frame, setFrame] = useState(0);
useEffect(() => {
const id = setInterval(() => setFrame((n) => (n + 1) % PROMPT_LOADING_FRAMES.length), 120);
return () => clearInterval(id);
}, []);
const step = PROMPT_LOADING_FRAMES[frame] ?? PROMPT_LOADING_FRAMES[0];
return (
<text>
{[0, 1, 2].map((idx) => (
<span key={idx} style={{ fg: promptLoadingCellColor(color, idx, step.active, step.forward) }}>
{promptLoadingCellGlyph(idx, step.active, step.forward)}
</span>
))}
</text>
);
}
function promptLoadingCellGlyph(index: number, active: number, forward: boolean): string {
const distance = forward ? active - index : index - active;
return distance >= 0 && distance < 2 ? "■" : "⬝";
}
function promptLoadingCellColor(color: string, index: number, active: number, forward: boolean): string {
const distance = forward ? active - index : index - active;
if (distance === 0) return color;
if (distance === 1) return withAlpha(color, 0.72);
return withAlpha(color, 0.22);
}
function withAlpha(color: string, alpha: number): string {
const normalized = color.trim();
const hex = normalized.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
if (!hex) return color;
const body = hex[1];
const expanded =
body.length === 3
? body
.split("")
.map((ch) => ch + ch)
.join("")
: body;
const alphaHex = Math.round(Math.max(0, Math.min(1, alpha)) * 255)
.toString(16)
.padStart(2, "0");
return `#${expanded}${alphaHex}`;
}
function CopyFlashBanner({ t, width }: { t: Theme; width: number }) {
return (
<box