ASCII progress indicatorsWritten by Frank Fiegel on September 25, 2024.reactasciiopen sourceI have always been fascinated by old terminal interfaces and wanted to bring some of their charm to the Glama chat UI. To indicate the progress of AI responses, I use ASCII progress indicators. The animations are inspired by these two articles: https://odino.org/command-line-spinners-the-amazing-tale-of-modern-typewriters-and-digital-movies/ https://medium.com/@Kaderovski/shell-loading-animations-990255ec415e I transformed the concept into a React component. Below is a collection of progress indicators: ⣾⠋⠋⠄⠋⠁⠈⠁⢹⢄⠁░▛↑𓃉𓃉𓃉⊏☰—×🕛🌍🌑😐💣 Each animation is a sequence of characters that rotate at a set interval. For example, the characters ⣾, ⣽, ⣻, ⢿, ⡿, ⣟, ⣯, ⣷ become ⣾. Here is the component code: import { useEffect, useRef, useState } from 'react'; type ProgressIndicatorStyle = { frames: string[]; interval: number; }; const styles = { arrow: { frames: ['↑', '↗', '→', '↘', '↓', '↙', '←', '↖'], interval: 100, }, ball_wave: { frames: ['𓃉𓃉𓃉', '𓃉𓃉∘', '𓃉∘°', '∘°∘', '°∘𓃉', '∘𓃉𓃉'], interval: 100, }, blocks1: { frames: ['░', '▒', '▓', '█'], interval: 100, }, blocks2: { frames: ['▛','▜','▟','▙'], interval: 100, }, cym: { frames: ['⊏', '⊓', '⊐', '⊔'], interval: 100, }, dots1: { frames: ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'], interval: 50, }, dots2: { frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], interval: 50, }, dots3: { frames: ['⠋', '⠙', '⠚', '⠞', '⠖', '⠦', '⠴', '⠲', '⠳', '⠓'], interval: 50, }, dots4: { frames: [ '⠄', '⠆', '⠇', '⠋', '⠙', '⠸', '⠰', '⠠', '⠰', '⠸', '⠙', '⠋', '⠇', '⠆', ], interval: 50, }, dots5: { frames: [ '⠋', '⠙', '⠚', '⠒', '⠂', '⠂', '⠒', '⠲', '⠴', '⠦', '⠖', '⠒', '⠐', '⠐', '⠒', '⠓', '⠋', ], interval: 50, }, dots6: { frames: [ '⠁', '⠉', '⠙', '⠚', '⠒', '⠂', '⠂', '⠒', '⠲', '⠴', '⠤', '⠄', '⠄', '⠤', '⠴', '⠲', '⠒', '⠂', '⠂', '⠒', '⠚', '⠙', '⠉', '⠁', ], interval: 50, }, dots7: { frames: [ '⠈', '⠉', '⠋', '⠓', '⠒', '⠐', '⠐', '⠒', '⠖', '⠦', '⠤', '⠠', '⠠', '⠤', '⠦', '⠖', '⠒', '⠐', '⠐', '⠒', '⠓', '⠋', '⠉', '⠈', ], interval: 50, }, dots8: { frames: [ '⠁', '⠁', '⠉', '⠙', '⠚', '⠒', '⠂', '⠂', '⠒', '⠲', '⠴', '⠤', '⠄', '⠄', '⠤', '⠠', '⠠', '⠤', '⠦', '⠖', '⠒', '⠐', '⠐', '⠒', '⠓', '⠋', '⠉', '⠈', '⠈', ], interval: 50, }, dots9: { frames: ['⢹', '⢺', '⢼', '⣸', '⣇', '⡧', '⡗', '⡏'], interval: 50, }, dots10: { frames: ['⢄', '⢂', '⢁', '⡁', '⡈', '⡐', '⡠'], interval: 50, }, dots11: { frames: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'], interval: 50, }, emoji_blink: { frames: ['😐', '😐', '😐', '😐', '😐', '😐', '😐', '😐', '😐', '😐', '😑'], interval: 100, }, emoji_bomb: { frames: [ '💣 ', ' 💣 ', ' 💣 ', ' 💣', ' 💣', ' 💣', ' 💣', ' 💣', ' 💥', ' ', ' ', ], interval: 100, }, emoji_earth: { frames: ['🌍', '🌎', '🌏'], interval: 200, }, emoji_hour: { frames: [ '🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', ], interval: 100, }, emoji_moon: { frames: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'], interval: 200, }, line: { frames: ['☰', '☱', '☳', '☷', '☶', '☴'], interval: 100, }, old: { frames: ['—', '\\', '|', '/'], interval: 100, }, x_plus: { frames: ['×', '+'], interval: 100, }, } satisfies Record<string, ProgressIndicatorStyle>; const useInterval = (callback: () => void, delay: null | number) => { const savedCallback = useRef(callback); useEffect(() => { savedCallback.current = callback; }, [callback]); useEffect(() => { if (delay === null) { return undefined; } const id = setInterval(() => savedCallback.current(), delay); return () => { clearInterval(id); }; }, [delay]); }; export const ProgressIndicator = ({ style, }: { style: keyof typeof styles; }) => { const { frames, interval } = styles[style]; if (!style) { throw new Error('Invalid style index'); } const [index, setIndex] = useState<number>(0); useInterval(() => { setIndex((index + 1) % frames.length); }, interval); return ( <div style={{ color: '#00d992', fontFamily: 'monospace', pointerEvents: 'none', textAlign: 'center', userSelect: 'none', whiteSpace: 'pre', width: '24px', }} > {frames[index]} </div> ); };