DEV Community

Cover image for ASCII progress indicators
Frank Fiegel
Frank Fiegel

Posted on • Originally published at glama.ai

ASCII progress indicators

I 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:

I transformed the concept into a React component.

Below is a collection of progress indicators:

ASCII progress indicators

Each animation is a sequence of characters that rotate at a set interval. For example, the characters ⣾, ⣽, ⣻, ⢿, ⡿, ⣟, ⣯, ⣷ become
Image description.

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)