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> <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> for (const inv of overdue) {</span></div>
<div class="cl"><span class="ln">17</span><span> await this.mailer.remind(inv);</span></div>
</div>
<div class="solid">
<div class="cl"><span class="ln">16</span><span> <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> <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>