Slavko Babić
Svi eksperimenti

30. maj 2026.

AI na delu, u četiri scene

Četiri kratke scene koje prikazuju AI kako radi stvarne zadatke — RAG podršku, predloge u editoru, izvršavanje agenta i vektorsku pretragu — sve u čistoj CSS animaciji.

Animacije su ilustrativne — urađene za potrebe prezentacije.

Kad god neko pokušava da prikaže AI, skoro uvek dobiješ svetleći mozak ili apstraktnu mrežu neurona. Mene su zanimale običnije stvari: male scene u kojima alati rade svoj svakodnevni, neupadljivi posao. Zato sam napravio četiri animacije koje se vrte u petlji — bez JavaScripta, bez <canvas>-a, samo CSS keyframe-ovi pažljivo usklađeni u zajedničkoj petlji od devet sekundi.

Podrška iz baze znanjaOdgovor dolazi iz stvarnih dokumenata kompanije — sa izvorom, a ne iz mašte.

AI u editoruAsistent predlaže, inženjer odlučuje. Deo mog svakodnevnog alata.

Agent preuzima rutinuVišekoračni proces koji tiho radi svoj posao — čovek prati izuzetke.

Vektorska pretragaPitanje se ne poklapa sa ključnim rečima, već sa značenjem — i pronalazi najbliže znanje.

Svaku od ovih situacija sam u nekom obliku već isporučio u produkciji:

  • RAG podrška — odgovor na pitanje dolazi iz interne dokumentacije, uz jasno navedene izvore. Citati nisu ukras. Oni su razlog zbog kog ovakav sistem uopšte ima smisla.
  • Editor — sivi predlog teksta pojavi se ispred kursora i prihvati pritiskom na Tab. AI predlaže, inženjer odlučuje.
  • Agent — višekoračni proces odradi posao u pozadini, zabeleži svaki korak i ostavi čist rezultat, dok čovek reaguje samo kada postoji izuzetak.
  • Vektorska pretraga — upit pronađe najsličnije dokumente po značenju, a ne po podudaranju ključnih reči.

Najzanimljivije ograničenje bilo je to što svaka animacija mora da izgleda dovršeno čak i kada miruje. Zamrzni petlju — ili uključi prefers-reduced-motion — i svaka kartica izgleda kao završena scena, a ne kao pokvarena animacija: kvačice su na mestu, spineri su nestali, odgovor je prikazan. Dobra animacija treba da ima smisla i kada stane. To je sitnica, ali upravo tu se vidi razlika između animacije koja poštuje korisnika i one koja samo pokušava da privuče pažnju.

Sve četiri scene staju u jedan HTML fajl — kopiraj ga ili preuzmi:

Izvorni kod — ai-in-practice.html(563 linija)Preuzmi .html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AI in practice — four loops, pure CSS animation</title>
<!--
  Four looping vignettes of AI doing real work — RAG support, an editor
  suggestion, an agent run, a vector search. No JavaScript, no canvas: just
  CSS keyframes timed against each other on a shared nine-second loop.
  From slavkobabic.com/experiments — copy freely.

  Every animation's resting state is its finished state: freeze the loop (or
  turn on prefers-reduced-motion) and each card reads as a completed scene.
-->
<style>
  :root {
    color-scheme: dark;
    --bg: #0f0e17;
    --bg-raised: #1a1826;
    --fg: #d8d6e2;
    --fg-strong: #f3f2f8;
    --fg-muted: #a09cb0;
    --border: #312e40;
    --accent: #a78bfa;
    --accent-contrast: #0f0e17;
    --font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
    --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  }

  @media (prefers-color-scheme: light) {
    :root {
      color-scheme: light;
      --bg: #fbfaff;
      --bg-raised: #f1effa;
      --fg: #1d1b26;
      --fg-strong: #0e0d15;
      --fg-muted: #5a5568;
      --border: #dedbe8;
      --accent: #6d28d9;
      --accent-contrast: #fbfaff;
    }
  }

  body {
    margin: 0;
    padding: 2.5rem 1rem;
    background: var(--bg);
    color: var(--fg);
    font-family: system-ui, sans-serif;
    line-height: 1.5;
  }

  .demos {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.5rem;
    max-width: 920px;
    margin-inline: auto;
  }

  @media (max-width: 720px) {
    .demos { grid-template-columns: 1fr; }
  }

  .demo {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--bg);
  }

  .titlebar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.55rem 0.9rem;
    border-bottom: 1px solid var(--border);
    background: var(--bg-raised);
    font-family: var(--font-mono);
    font-size: 0.7rem;
    color: var(--fg-muted);
  }

  .lamp {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
    animation: lamp 2.4s var(--ease-out) infinite;
  }

  @keyframes lamp {
    0%, 100% { opacity: 0.35; }
    50% { opacity: 1; }
  }

  .stage {
    flex: 1;
    min-height: 172px;
    padding: 0.95rem 1rem;
    font-size: 0.8rem;
    line-height: 1.5;
  }

  .caption {
    margin: 0;
    padding: 0.7rem 1rem 0.85rem;
    border-top: 1px dashed var(--border);
    font-size: 0.875rem;
    color: var(--fg-muted);
  }

  .caption strong {
    display: block;
    margin-block-end: 0.1rem;
    color: var(--fg-strong);
    font-weight: 600;
  }

  /* ---- shared vignette building blocks ---- */
  .bub {
    display: inline-block;
    max-width: 90%;
    padding: 0.4rem 0.7rem;
    border-radius: 0.8rem;
  }

  .bub.user {
    background: var(--accent);
    color: var(--accent-contrast);
    border-bottom-right-radius: 0.25rem;
  }

  .bub.bot {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-bottom-left-radius: 0.25rem;
  }

  .row-r { margin-block-end: 0.55rem; text-align: right; }
  .row-l { margin-block-end: 0.55rem; }

  .dots {
    display: inline-flex;
    gap: 4px;
    padding-block: 0.15rem;
  }

  .dots i {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--fg-muted);
    animation: blink 1s infinite ease-in-out;
  }

  .dots i:nth-child(2) { animation-delay: 0.18s; }
  .dots i:nth-child(3) { animation-delay: 0.36s; }

  @keyframes blink {
    0%, 70%, 100% { opacity: 0.3; transform: translateY(0); }
    35% { opacity: 1; transform: translateY(-2px); }
  }

  .chip {
    display: inline-block;
    margin-inline-end: 0.35rem;
    padding: 0.08rem 0.45rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--bg-raised);
    font-family: var(--font-mono);
    font-size: 0.66rem;
    color: var(--fg-muted);
  }

  /* ---- vignette 1: RAG chat (9s loop) ---- */
  .v1-q { animation: v1q 9s infinite; }
  .v1-dots { animation: v1dots 9s infinite; }
  .v1-a { animation: v1a 9s infinite; }
  .v1-src { animation: v1src 9s infinite; }

  @keyframes v1q {
    0% { opacity: 0; }
    5% { opacity: 1; }
    93% { opacity: 1; }
    100% { opacity: 0; }
  }

  @keyframes v1dots {
    0%, 9% { opacity: 0; }
    12%, 26% { opacity: 1; }
    29%, 100% { opacity: 0; }
  }

  @keyframes v1a {
    0%, 29% { opacity: 0; transform: translateY(4px); }
    34% { opacity: 1; transform: none; }
    93% { opacity: 1; }
    100% { opacity: 0; }
  }

  @keyframes v1src {
    0%, 40% { opacity: 0; }
    46% { opacity: 1; }
    93% { opacity: 1; }
    100% { opacity: 0; }
  }

  @media (prefers-reduced-motion: reduce) {
    .v1-dots { display: none; }
  }

  /* ---- vignette 2: coding assistant (9s loop) ---- */
  .code {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    line-height: 1.7;
  }

  .cl { display: flex; white-space: pre; }

  .ln {
    width: 2ch;
    margin-inline-end: 1ch;
    color: var(--fg-muted);
    opacity: 0.55;
    text-align: right;
    user-select: none;
    flex-shrink: 0;
  }

  .kw { color: var(--accent); }

  .swap { display: grid; }
  .swap > * { grid-area: 1 / 1; }

  .ghost {
    color: var(--fg-muted);
    font-style: italic;
    animation: v2ghost 9s infinite;
    opacity: 0;
  }

  .solid { animation: v2solid 9s infinite; }

  .accepted {
    display: inline-block;
    margin-block-start: 0.45rem;
    font-family: var(--font-mono);
    font-size: 0.64rem;
    color: var(--accent);
    animation: v2tab 9s infinite;
  }

  .caret {
    display: inline-block;
    width: 1px;
    height: 0.95em;
    background: var(--accent);
    vertical-align: -0.15em;
    animation: caret 1s steps(1) infinite;
  }

  @keyframes caret {
    0%, 55% { opacity: 1; }
    56%, 100% { opacity: 0; }
  }

  @keyframes v2ghost {
    0%, 18% { opacity: 0; }
    24%, 52% { opacity: 0.75; }
    56%, 100% { opacity: 0; }
  }

  @keyframes v2solid {
    0%, 55% { opacity: 0; }
    60%, 94% { opacity: 1; }
    100% { opacity: 0; }
  }

  @keyframes v2tab {
    0%, 58% { opacity: 0; }
    63%, 88% { opacity: 1; }
    94%, 100% { opacity: 0; }
  }

  /* ---- vignette 3: agent run (9s loop) ---- */
  .steps {
    display: grid;
    gap: 0.55rem;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .steps li {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--fg-muted);
  }

  .ico {
    position: relative;
    width: 14px;
    height: 14px;
    flex-shrink: 0;
  }

  .ring, .spin, .done {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .ring {
    border: 1.5px solid var(--border);
    border-radius: 50%;
  }

  .spin {
    border: 1.5px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    opacity: 0;
  }

  .done {
    color: var(--accent);
    font-size: 11px;
    line-height: 1;
    opacity: 0;
  }

  @keyframes spinrot {
    to { transform: rotate(360deg); }
  }

  .s1 .spin { animation: spinrot 0.8s linear infinite, v3s1spin 9s linear infinite; }
  .s2 .spin { animation: spinrot 0.8s linear infinite, v3s2spin 9s linear infinite; }
  .s3 .spin { animation: spinrot 0.8s linear infinite, v3s3spin 9s linear infinite; }
  .s4 .spin { animation: spinrot 0.8s linear infinite, v3s4spin 9s linear infinite; }
  .s1 .done { animation: v3s1done 9s infinite; }
  .s2 .done { animation: v3s2done 9s infinite; }
  .s3 .done { animation: v3s3done 9s infinite; }
  .s4 .done { animation: v3s4done 9s infinite; }

  @keyframes v3s1spin { 0%, 4% { opacity: 0; } 5%, 21% { opacity: 1; } 22%, 100% { opacity: 0; } }
  @keyframes v3s1done { 0%, 21% { opacity: 0; } 22%, 95% { opacity: 1; } 100% { opacity: 0; } }
  @keyframes v3s2spin { 0%, 22% { opacity: 0; } 23%, 39% { opacity: 1; } 40%, 100% { opacity: 0; } }
  @keyframes v3s2done { 0%, 39% { opacity: 0; } 40%, 95% { opacity: 1; } 100% { opacity: 0; } }
  @keyframes v3s3spin { 0%, 40% { opacity: 0; } 41%, 57% { opacity: 1; } 58%, 100% { opacity: 0; } }
  @keyframes v3s3done { 0%, 57% { opacity: 0; } 58%, 95% { opacity: 1; } 100% { opacity: 0; } }
  @keyframes v3s4spin { 0%, 58% { opacity: 0; } 59%, 75% { opacity: 1; } 76%, 100% { opacity: 0; } }
  @keyframes v3s4done { 0%, 75% { opacity: 0; } 76%, 95% { opacity: 1; } 100% { opacity: 0; } }

  .agent-log {
    margin-block-start: 0.7rem;
    font-family: var(--font-mono);
    font-size: 0.64rem;
    color: var(--accent);
    animation: v3log 9s infinite;
  }

  @keyframes v3log {
    0%, 76% { opacity: 0; }
    80%, 94% { opacity: 1; }
    100% { opacity: 0; }
  }

  /* ---- vignette 4: vector search (9s loop) ---- */
  .vec { width: 100%; height: auto; }

  .pt {
    fill: var(--fg-muted);
    opacity: 0.45;
  }

  .hit {
    fill: var(--accent);
    transform-box: fill-box;
    transform-origin: center;
    animation: v4hit 9s infinite;
  }

  .qry {
    fill: var(--accent);
    transform-box: fill-box;
    transform-origin: center;
    animation: v4qry 9s infinite;
  }

  .ripple {
    fill: none;
    stroke: var(--accent);
    stroke-width: 1.5;
    transform-box: fill-box;
    transform-origin: center;
    opacity: 0;
    animation: v4rip 9s infinite;
  }

  .link {
    stroke: var(--accent);
    stroke-width: 1.2;
    fill: none;
    stroke-dasharray: 1;
    animation: v4link 9s infinite;
  }

  .vec-label {
    font-family: var(--font-mono);
    font-size: 0.64rem;
    color: var(--fg-muted);
    animation: v4lbl 9s infinite;
  }

  @keyframes v4qry {
    0%, 6% { opacity: 0; transform: scale(0.4); }
    12%, 94% { opacity: 1; transform: scale(1); }
    100% { opacity: 0; }
  }

  @keyframes v4rip {
    0%, 13% { opacity: 0; transform: scale(0.3); }
    18% { opacity: 0.9; transform: scale(1); }
    34% { opacity: 0; transform: scale(3.2); }
    100% { opacity: 0; }
  }

  @keyframes v4hit {
    0%, 34% { opacity: 0.45; fill: var(--fg-muted); transform: scale(1); }
    42%, 94% { opacity: 1; fill: var(--accent); transform: scale(1.45); }
    100% { opacity: 0.45; }
  }

  @keyframes v4link {
    0%, 42% { stroke-dashoffset: 1; opacity: 0; }
    46% { opacity: 1; }
    60%, 92% { stroke-dashoffset: 0; opacity: 1; }
    100% { stroke-dashoffset: 0; opacity: 0; }
  }

  @keyframes v4lbl {
    0%, 60% { opacity: 0; }
    66%, 93% { opacity: 1; }
    100% { opacity: 0; }
  }

  /* Reduced motion: every card reads as a completed static scene. */
  @media (prefers-reduced-motion: reduce) {
    * { animation: none !important; }

    .done, .agent-log, .solid, .accepted { opacity: 1; }
    .spin, .ghost, .ripple { opacity: 0; }
  }
</style>
</head>
<body>

<div class="demos">
  <!-- ------------- 1. RAG support chat ------------- -->
  <article class="demo" aria-label="Support from a knowledge base">
    <div class="titlebar" aria-hidden="true"><span class="lamp"></span>support.internal · RAG</div>
    <div class="stage" aria-hidden="true">
      <div class="row-r v1-q"><span class="bub user">How long is the warranty on the X200?</span></div>
      <div class="row-l v1-dots">
        <span class="bub bot"><span class="dots"><i></i><i></i><i></i></span></span>
      </div>
      <div class="row-l v1-a"><span class="bub bot">The X200 warranty runs 36 months from the date of purchase and covers parts and labour.</span></div>
      <div class="row-l v1-src">
        <span class="chip">[warranty.pdf · p. 4]</span>
        <span class="chip">[pricelist-2026.xlsx]</span>
      </div>
    </div>
    <p class="caption">
      <strong>Support from a knowledge base</strong>
      The answer comes from the company's real documents — with a source, not from imagination.
    </p>
  </article>

  <!-- ------------- 2. Coding assistant ------------- -->
  <article class="demo" aria-label="AI in the editor">
    <div class="titlebar" aria-hidden="true"><span class="lamp"></span>invoice_service.ts · editor</div>
    <div class="stage" aria-hidden="true">
      <div class="code">
        <div class="cl"><span class="ln">14</span><span><span class="kw">async</span> sendReminders() {</span></div>
        <div class="cl"><span class="ln">15</span><span>&nbsp;&nbsp;<span class="kw">const</span> overdue = <span class="kw">await</span> this.repo.findOverdue();<span class="caret"></span></span></div>
        <div class="swap">
          <div class="ghost">
            <div class="cl"><span class="ln">16</span><span>&nbsp;&nbsp;for (const inv of overdue) {</span></div>
            <div class="cl"><span class="ln">17</span><span>&nbsp;&nbsp;&nbsp;&nbsp;await this.mailer.remind(inv);</span></div>
          </div>
          <div class="solid">
            <div class="cl"><span class="ln">16</span><span>&nbsp;&nbsp;<span class="kw">for</span> (<span class="kw">const</span> inv <span class="kw">of</span> overdue) {</span></div>
            <div class="cl"><span class="ln">17</span><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="kw">await</span> this.mailer.remind(inv);</span></div>
          </div>
        </div>
      </div>
      <span class="accepted">Tab · suggestion accepted ✓</span>
    </div>
    <p class="caption">
      <strong>AI in the editor</strong>
      The assistant suggests, the engineer decides. Part of my everyday toolkit.
    </p>
  </article>

  <!-- ------------- 3. Agent run ------------- -->
  <article class="demo" aria-label="An agent takes over the routine">
    <div class="titlebar" aria-hidden="true"><span class="lamp"></span>agent/order-processing · run #4821</div>
    <div class="stage" aria-hidden="true">
      <ul class="steps" role="presentation">
        <li class="s1"><span class="ico"><span class="ring"></span><span class="spin"></span><span class="done">✓</span></span>Fetch new orders from the inbox</li>
        <li class="s2"><span class="ico"><span class="ring"></span><span class="spin"></span><span class="done">✓</span></span>Extract items and amounts (LLM)</li>
        <li class="s3"><span class="ico"><span class="ring"></span><span class="spin"></span><span class="done">✓</span></span>Reconcile against the ERP</li>
        <li class="s4"><span class="ico"><span class="ring"></span><span class="spin"></span><span class="done">✓</span></span>Post and archive the document</li>
      </ul>
      <div class="agent-log">✓ 4/4 steps · 12 orders processed · 0 errors</div>
    </div>
    <p class="caption">
      <strong>An agent takes over the routine</strong>
      A multi-step process quietly doing its job — a human watches the exceptions.
    </p>
  </article>

  <!-- ------------- 4. Vector search ------------- -->
  <article class="demo" aria-label="Vector search">
    <div class="titlebar" aria-hidden="true"><span class="lamp"></span>embeddings · top-k=3</div>
    <div class="stage" aria-hidden="true">
      <svg class="vec" viewBox="0 0 280 120">
        <circle class="pt" cx="36" cy="30" r="4"></circle>
        <circle class="pt" cx="70" cy="92" r="4"></circle>
        <circle class="pt" cx="118" cy="18" r="4"></circle>
        <circle class="pt" cx="236" cy="100" r="4"></circle>
        <circle class="pt" cx="256" cy="28" r="4"></circle>
        <circle class="hit" cx="172" cy="38" r="4"></circle>
        <circle class="hit" cx="196" cy="72" r="4"></circle>
        <circle class="hit" cx="148" cy="84" r="4"></circle>
        <path class="link" d="M150 60 L172 38" pathLength="1"></path>
        <path class="link" d="M150 60 L196 72" pathLength="1"></path>
        <path class="link" d="M150 60 L148 84" pathLength="1"></path>
        <circle class="ripple" cx="150" cy="60" r="8"></circle>
        <circle class="qry" cx="150" cy="60" r="5"></circle>
      </svg>
      <div class="vec-label">query → 3 nearest documents · cos ≥ 0.87</div>
    </div>
    <p class="caption">
      <strong>Vector search</strong>
      The question doesn't match keywords, it matches meaning — and finds the nearest knowledge.
    </p>
  </article>
</div>

</body>
</html>

Tehnologije

  • CSS animation
  • SVG

Teme

  • RAG
  • LLM
  • Embeddings
  • AI agenti