Puhuvat päät agenteille: ! ja puhekupla vuorovaikutuksen lisäämiseksi

This commit is contained in:
2026-04-03 12:38:45 +03:00
parent 2ea24c6248
commit f83ea9d90a
7 changed files with 353 additions and 278 deletions

Binary file not shown.

View File

@@ -0,0 +1,34 @@
# Kipinä Agentic Playground - Animaatioiden käyttöönotto
Koska Kipinä-verkon agenttien avatarit tällä erää ovat staattisia PNG-kuvatiedostoja, käyttöliittymä hyödyntää CSS-pohjaista pomppimisilmiötä (sekä pulppuavaa 💬 puhekuplaa) "puhumisen" merkkinä. Olemme kuitenkin koodanneet taustalle piilotetun tuen aivioiduille videoloopeille myöhempää käyttöä varten!
Näin saat UI:n tukemaan oikeasti animoituja kasvoja/videoita.
## 1. Luo Animoidut GIF-tiedostot
Valitse mikä tahansa ulkoinen AI-työkalu (kuten HeyGen, Pika v1.0, tai Midjourney+Runway yhdistelmä) ja muunna avatar-kuvat (esim. `kettu_notext.png`) 3-5 sekunnin kestäviksi GIF-loopeiksi. Hahmon leuka tulisi pyöriä tai naama vääntyillä puhuessaan.
## 2. Nimeä Tiedostot Oikein ja Lisää Ne Kansioon
Siirrä uudet GIF-animaatiot samaan kansioon alkuperäisten kuvien kanssa. Muuta niiden nimi siten, että se päättyy tunnisteeseen `_puhuva.gif`.
Esimerkkejä:
- Koodari `kipina_notext.png``kipina_notext_puhuva.gif`
- Manageri `karhunpentu.png``karhunpentu_puhuva.gif`
- Asiakas `kettu_notext.png``kettu_notext_puhuva.gif`
## 3. Aktivoi Koodi
Käännä Kipinä Playground -ohjaimen JavaScript-koodista piilotettu ominaisuus päälle.
Etsi tiedostosta `../index.html` (noin riviltä 1084, `updatePromptEditor`-funktiosta):
```javascript
// Piilotettu ominaisuus: Puhuvien videoiden / gif-animaatioiden kytkentä
window.USE_ANIMATED_GIFS = false;
```
Muuta tuo `false` arvoon `true`:
```javascript
window.USE_ANIMATED_GIFS = true;
```
**Mitä logiikka tekee?**
Aina kun valitset agentin kaaviosta, koodi korvaa aktiivisen kuvakkeen lopussa olevan `.png` -päätteen sanalla `_puhuva.gif` lennosta! Jos poistut agentin valinnasta tai valitset jonkun toisen, koodi vaihtaa kuvan välittömästi takaisin staattiseen `.png`-versioon ja sulkee ilmentymän suun.
Näin saat kaikkien asiantuntijoiden face-track looppeja hallittua yhdellä kädenkäänteellä.

View File

@@ -1,44 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<radialGradient id="bgGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#2a3f5a"/>
<stop offset="100%" stop-color="#0a121d"/>
</radialGradient>
<filter id="neonGold" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#ffca28" flood-opacity="0.8"/>
</filter>
<filter id="neonBlue" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#4fc3f7" flood-opacity="0.9"/>
</filter>
</defs>
<!-- Background Base -->
<rect width="200" height="200" rx="30" fill="url(#bgGlow)"/>
<!-- Swarm Connectivity Lines -->
<g stroke="#4fc3f7" stroke-width="1.5" opacity="0.4" filter="url(#neonBlue)">
<line x1="100" y1="100" x2="30" y2="40" />
<line x1="100" y1="100" x2="170" y2="40" />
<line x1="100" y1="100" x2="40" y2="160" />
<line x1="100" y1="100" x2="160" y2="160" />
<line x1="100" y1="100" x2="190" y2="100" />
<line x1="100" y1="100" x2="10" y2="100" />
</g>
<!-- Gentle Crystal Core -->
<path d="M 100 40 L 130 100 L 100 160 L 70 100 Z" fill="#1b2a4a" stroke="#ffca28" stroke-width="3" filter="url(#neonGold)"/>
<path d="M 100 40 L 130 100 L 100 120 Z" fill="#2a3f5a" opacity="0.8"/>
<path d="M 100 40 L 70 100 L 100 120 Z" fill="#0a121d" opacity="0.6"/>
<!-- Floating Swarm Nodes -->
<circle cx="30" cy="40" r="5" fill="#4fc3f7" filter="url(#neonBlue)"/>
<circle cx="170" cy="40" r="5" fill="#4fc3f7" filter="url(#neonBlue)"/>
<circle cx="40" cy="160" r="5" fill="#4fc3f7" filter="url(#neonBlue)"/>
<circle cx="160" cy="160" r="5" fill="#4fc3f7" filter="url(#neonBlue)"/>
<circle cx="190" cy="100" r="4" fill="#ffca28" filter="url(#neonGold)"/>
<circle cx="10" cy="100" r="4" fill="#ffca28" filter="url(#neonGold)"/>
<circle cx="100" cy="100" r="10" fill="#ffca28" filter="url(#neonGold)"/>
<circle cx="100" cy="100" r="4" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,54 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<radialGradient id="bgGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#0a2e23"/>
<stop offset="100%" stop-color="#090e10"/>
</radialGradient>
<filter id="neonMint" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#0fffa3" flood-opacity="0.8"/>
</filter>
<filter id="neonSun" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#ffb142" flood-opacity="0.9"/>
</filter>
</defs>
<!-- Background Base -->
<rect width="200" height="200" rx="30" fill="url(#bgGlow)"/>
<!-- Cyberpunk soft coding runic rings in background -->
<circle cx="100" cy="100" r="80" fill="none" stroke="#0fffa3" stroke-width="1" opacity="0.1" filter="url(#neonMint)"/>
<circle cx="100" cy="100" r="85" fill="none" stroke="#0fffa3" stroke-width="0.5" stroke-dasharray="4 8" opacity="0.2"/>
<!-- Lizard Gentle Head Shape -->
<path d="M 100 30 C 160 30, 180 80, 150 140 C 120 180, 80 180, 50 140 C 20 80, 40 30, 100 30 Z" fill="#1b2a26" />
<path d="M 100 40 C 150 40, 160 80, 140 130 C 120 160, 80 160, 60 130 C 40 80, 50 40, 100 40 Z" fill="#243831" />
<!-- Code-scale back-ridges (Cyber implants but cute) -->
<path d="M 60 15 L 75 35 M 100 10 L 100 30 M 140 15 L 125 35" stroke="#0fffa3" stroke-width="4" stroke-linecap="round" filter="url(#neonMint)"/>
<!-- Big Lovely Lizard Eyes mapped to sides -->
<ellipse cx="45" cy="85" rx="20" ry="30" fill="#090e10" transform="rotate(-15 45 85)"/>
<ellipse cx="155" cy="85" rx="20" ry="30" fill="#090e10" transform="rotate(15 155 85)"/>
<!-- Neon Glowing Pupils -->
<circle cx="45" cy="85" r="10" fill="#0fffa3" filter="url(#neonMint)"/>
<circle cx="155" cy="85" r="10" fill="#0fffa3" filter="url(#neonMint)"/>
<circle cx="42" cy="78" r="4" fill="#ffffff" opacity="0.9"/>
<circle cx="152" cy="78" r="4" fill="#ffffff" opacity="0.9"/>
<!-- Adorable Snout & Neural Nose-bridge -->
<path d="M 100 45 L 100 100" stroke="#ffb142" stroke-width="2" opacity="0.5" filter="url(#neonSun)"/>
<circle cx="100" cy="100" r="4" fill="#ffb142" filter="url(#neonSun)"/>
<path d="M 80 145 C 90 155, 110 155, 120 145" fill="none" stroke="#0fffa3" stroke-width="3" stroke-linecap="round" filter="url(#neonMint)"/>
<!-- Cute little cheek blushes in neon -->
<ellipse cx="65" cy="115" rx="8" ry="4" fill="#ffb142" opacity="0.6" filter="url(#neonSun)"/>
<ellipse cx="135" cy="115" rx="8" ry="4" fill="#ffb142" opacity="0.6" filter="url(#neonSun)"/>
<!-- Floating Brackets (Coder vibe) -->
<text x="15" y="150" fill="#0fffa3" font-size="24" font-family="monospace" opacity="0.5" filter="url(#neonMint)">{</text>
<text x="170" y="60" fill="#0fffa3" font-size="24" font-family="monospace" opacity="0.5" filter="url(#neonMint)">}</text>
<text x="160" y="160" fill="#ffb142" font-size="18" font-family="monospace" opacity="0.5" filter="url(#neonSun)">&lt;/&gt;</text>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,66 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<radialGradient id="bgGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#3d155f"/>
<stop offset="100%" stop-color="#090a0f"/>
</radialGradient>
<radialGradient id="eyeReflection" cx="40%" cy="40%" r="40%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
<filter id="neonPink" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#ff71ce" flood-opacity="0.8"/>
</filter>
<filter id="neonCyan" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#05d9e8" flood-opacity="0.9"/>
</filter>
</defs>
<!-- Background Base -->
<rect width="200" height="200" rx="30" fill="url(#bgGlow)"/>
<!-- Cyberpunk background grid elements -->
<g stroke="#05d9e8" stroke-width="0.5" opacity="0.3">
<path d="M 20 40 L 180 40 M 20 80 L 180 80 M 20 120 L 180 120 M 20 160 L 180 160"/>
<path d="M 40 20 L 40 180 M 80 20 L 80 180 M 120 20 L 120 180 M 160 20 L 160 180"/>
</g>
<!-- Fluffy Gentle Raccoon Base -->
<path d="M 100 40 C 150 40, 170 80, 160 120 C 145 160, 55 160, 40 120 C 30 80, 50 40, 100 40 Z" fill="#2d3340" />
<!-- Ears -->
<path d="M 45 60 C 25 35, 30 15, 60 30 Z" fill="#1b212b" stroke="#ff71ce" stroke-width="2" filter="url(#neonPink)"/>
<path d="M 155 60 C 175 35, 170 15, 140 30 Z" fill="#1b212b" stroke="#05d9e8" stroke-width="2" filter="url(#neonCyan)"/>
<path d="M 50 55 C 35 40, 40 25, 55 35 Z" fill="#ff71ce" opacity="0.6" filter="url(#neonPink)"/>
<path d="M 150 55 C 165 40, 160 25, 145 35 Z" fill="#05d9e8" opacity="0.6" filter="url(#neonCyan)"/>
<!-- Bandit Mask (Cyber-goggles vibe but soft) -->
<path d="M 30 95 C 60 70, 140 70, 170 95 C 180 105, 140 140, 100 130 C 60 140, 20 105, 30 95 Z" fill="#0b0e14" opacity="0.8"/>
<path d="M 35 95 C 65 75, 135 75, 165 95" fill="none" stroke="#ff71ce" stroke-width="2" filter="url(#neonPink)" opacity="0.3"/>
<!-- Big Gentle Anime-style Eyes -->
<ellipse cx="65" cy="100" rx="16" ry="22" fill="#05d9e8" filter="url(#neonCyan)"/>
<ellipse cx="135" cy="100" rx="16" ry="22" fill="#ff71ce" filter="url(#neonPink)"/>
<ellipse cx="65" cy="100" rx="12" ry="18" fill="#111"/>
<ellipse cx="135" cy="100" rx="12" ry="18" fill="#111"/>
<!-- Glints -->
<circle cx="58" cy="92" r="6" fill="url(#eyeReflection)"/>
<circle cx="128" cy="92" r="6" fill="url(#eyeReflection)"/>
<circle cx="72" cy="108" r="3" fill="#fff" opacity="0.6"/>
<circle cx="142" cy="108" r="3" fill="#fff" opacity="0.6"/>
<!-- Cute Muzzle & Nose -->
<path d="M 80 120 C 100 150, 120 120, 100 140 Z" fill="#e5e5e5"/>
<path d="M 90 140 C 90 135, 110 135, 110 140 C 110 148, 90 148, 90 140 Z" fill="#ff71ce" filter="url(#neonPink)"/>
<path d="M 95 145 C 100 152, 105 145, 105 145" fill="none" stroke="#2d3340" stroke-width="2" stroke-linecap="round"/>
<!-- Cyberpunk Gentle Neck Ring/Scarf -->
<path d="M 50 150 C 80 170, 120 170, 150 150" fill="none" stroke="#05d9e8" stroke-width="6" stroke-linecap="round" filter="url(#neonCyan)"/>
<!-- Little Floating Data Motes -->
<circle cx="30" cy="50" r="3" fill="#ff71ce" filter="url(#neonPink)"/>
<circle cx="170" cy="130" r="2" fill="#05d9e8" filter="url(#neonCyan)"/>
<circle cx="150" cy="40" r="4" fill="#ff71ce" filter="url(#neonPink)"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,49 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<radialGradient id="bgGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#140722"/>
<stop offset="100%" stop-color="#0a0512"/>
</radialGradient>
<filter id="neonViolet" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#b829ea" flood-opacity="0.8"/>
</filter>
<filter id="neonTeal" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#1cf0eb" flood-opacity="0.9"/>
</filter>
</defs>
<!-- Background Base -->
<rect width="200" height="200" rx="30" fill="url(#bgGlow)"/>
<!-- Dreamy Cyber-waves -->
<path d="M 0 160 Q 50 180, 100 150 T 200 160 M 0 180 Q 50 160, 100 190 T 200 170" fill="none" stroke="#b829ea" stroke-width="2" opacity="0.2" filter="url(#neonViolet)"/>
<!-- Sloth Head Fluffy Contour -->
<path d="M 50 60 C 20 90, 40 160, 100 160 C 160 160, 180 90, 150 60 C 130 30, 70 30, 50 60 Z" fill="#231a31" />
<!-- Face Mask (Gentle Heart-like shape) -->
<path d="M 70 80 C 60 120, 90 140, 100 140 C 110 140, 140 120, 130 80 C 120 50, 80 50, 70 80 Z" fill="#3c304d" />
<!-- Cozy VR Sleep-Mask / Cyber-visor -->
<path d="M 50 80 Q 100 100, 150 80 Q 140 110, 100 115 Q 60 110, 50 80 Z" fill="#0d0914" stroke="#b829ea" stroke-width="2" filter="url(#neonViolet)"/>
<!-- Sweet Dreamy Digital Lines inside Visor -->
<path d="M 70 88 C 80 95, 90 95, 95 88 M 130 88 C 120 95, 110 95, 105 88" fill="none" stroke="#1cf0eb" stroke-width="3" stroke-linecap="round" filter="url(#neonTeal)"/>
<!-- Cute Sleepy Snout -->
<circle cx="100" cy="130" r="6" fill="#140722"/>
<path d="M 90 140 C 95 145, 105 145, 110 140" fill="none" stroke="#1cf0eb" stroke-width="2.5" stroke-linecap="round" filter="url(#neonTeal)"/>
<!-- Sleepy Zzzs and Star nodes -->
<text x="140" y="40" fill="#b829ea" font-size="20" font-family="sans-serif" font-weight="bold" filter="url(#neonViolet)">Z</text>
<text x="160" y="25" fill="#1cf0eb" font-size="14" font-family="sans-serif" font-weight="bold" filter="url(#neonTeal)">z</text>
<text x="175" y="15" fill="#b829ea" font-size="10" font-family="sans-serif" font-weight="bold" filter="url(#neonViolet)">z</text>
<circle cx="40" cy="50" r="2" fill="#1cf0eb" filter="url(#neonTeal)"/>
<circle cx="50" cy="30" r="1.5" fill="#b829ea" filter="url(#neonViolet)"/>
<!-- Cybernetic headset nodes -->
<circle cx="45" cy="85" r="8" fill="#140722" stroke="#1cf0eb" stroke-width="2" filter="url(#neonTeal)"/>
<circle cx="155" cy="85" r="8" fill="#140722" stroke="#1cf0eb" stroke-width="2" filter="url(#neonTeal)"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -393,6 +393,7 @@
align-items: center; align-items: center;
margin-bottom: 40px; margin-bottom: 40px;
perspective: 1000px; perspective: 1000px;
padding: 25px 50px;
} }
.org-level { .org-level {
display: flex; display: flex;
@@ -439,6 +440,18 @@
border-color: rgba(240, 246, 252, 0.3); border-color: rgba(240, 246, 252, 0.3);
box-shadow: 0 12px 20px rgba(0,0,0,0.4); box-shadow: 0 12px 20px rgba(0,0,0,0.4);
} }
@keyframes idle-breathe {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-2px) scale(1.01); }
}
@keyframes talking-head {
0% { transform: scale(1.05) scaleY(1) translateY(0); }
25% { transform: scale(1.05) scaleY(0.96) scaleX(1.02) translateY(2px); }
50% { transform: scale(1.05) scaleY(1.02) scaleX(0.98) translateY(-2px); }
75% { transform: scale(1.05) scaleY(0.97) scaleX(1.01) translateY(1px); }
100% { transform: scale(1.05) scaleY(1) translateY(0); }
}
.avatar-card img { .avatar-card img {
width: 80px; width: 80px;
height: 80px; height: 80px;
@@ -448,7 +461,10 @@
transition: all 0.4s ease; transition: all 0.4s ease;
object-fit: cover; object-fit: cover;
background: #010409; background: #010409;
animation: idle-breathe 4s infinite ease-in-out;
transform-origin: bottom center;
} }
.avatar-card.active, .avatar-card.selected { .avatar-card.active, .avatar-card.selected {
opacity: 1; opacity: 1;
transform: translateY(-8px) scale(1.05); transform: translateY(-8px) scale(1.05);
@@ -457,10 +473,134 @@
box-shadow: 0 16px 24px rgba(0,0,0,0.5), 0 0 20px rgba(88, 166, 255, 0.3); box-shadow: 0 16px 24px rgba(0,0,0,0.5), 0 0 20px rgba(88, 166, 255, 0.3);
z-index: 2; z-index: 2;
} }
.avatar-card.active img, .avatar-card.selected img {
.avatar-card.selected img {
border-color: var(--accent-color); border-color: var(--accent-color);
box-shadow: 0 0 25px rgba(88, 166, 255, 0.5); box-shadow: 0 0 25px rgba(88, 166, 255, 0.5);
transform: scale(1.05); transform: scale(1.05);
animation: none;
}
.avatar-card.active img {
border-color: var(--accent-color);
box-shadow: 0 0 25px rgba(88, 166, 255, 0.8);
animation: talking-head 0.4s infinite ease-in-out;
transform-origin: bottom center;
}
@keyframes talking-head-gallery {
0% { transform: scaleY(1) translateY(0); }
25% { transform: scaleY(0.94) scaleX(1.04) translateY(3px); }
50% { transform: scaleY(1.04) scaleX(0.96) translateY(-3px); }
75% { transform: scaleY(0.96) scaleX(1.02) translateY(1px); }
100% { transform: scaleY(1) translateY(0); }
}
.gallery-head {
width: 55px;
height: 55px;
border-radius: 12px;
border: 2px solid rgba(240, 246, 252, 0.1);
object-fit: cover;
background: #010409;
transition: all 0.3s ease;
opacity: 0.4;
filter: grayscale(80%);
}
.gallery-head.active {
opacity: 1;
filter: grayscale(0%);
border-color: var(--accent-color);
box-shadow: 0 0 15px rgba(88, 166, 255, 0.5);
animation: talking-head-gallery 0.4s infinite ease-in-out;
transform-origin: bottom center;
}
@keyframes confused-shake {
0% { transform: translateX(0); }
25% { transform: translateX(-2px) rotate(-3deg); }
50% { transform: translateX(0); }
75% { transform: translateX(2px) rotate(3deg); }
100% { transform: translateX(0); }
}
.gallery-head-wrap[data-tooltip]::before {
content: attr(data-tooltip);
position: absolute;
bottom: 110%;
left: 50%;
transform: translateX(-50%);
background: rgba(13, 17, 23, 0.95);
color: #f0f6fc;
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
white-space: pre-wrap;
width: 140px;
text-align: left;
border: 1px solid var(--border-color);
z-index: 100;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.gallery-head-wrap:hover[data-tooltip]:not([data-tooltip=""])::before { opacity: 1; }
/* Yhteiset kuplasäännöt */
.gallery-head-wrap.state-question::after,
.gallery-head-wrap.state-alert::after,
.gallery-head-wrap.active:not(.state-question):not(.state-alert)::after {
position: absolute;
top: -10px;
right: -10px;
font-size: 14px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
z-index: 10;
}
/* State: Kysymys (Sininen ?) */
.gallery-head-wrap.state-question::after {
content: '?';
color: #ffffff;
font-weight: 900;
font-family: system-ui, -apple-system, sans-serif;
animation: speech-pulse 1s infinite alternate;
background: #1f6feb; border: 1px solid #58a6ff;
}
.gallery-head.state-question {
border-color: #58a6ff; box-shadow: 0 0 15px rgba(88, 166, 255, 0.4);
animation: confused-shake 2s infinite ease-in-out; filter: grayscale(10%); opacity: 0.9;
}
/* State: Alert (Punainen !) */
.gallery-head-wrap.state-alert::after {
content: '!';
color: #ffffff;
font-weight: 900;
font-family: system-ui, -apple-system, sans-serif;
animation: speech-pulse 0.5s infinite alternate;
background: #da3633; border: 1px solid #ff7b72;
}
.gallery-head.state-alert {
border-color: #ff4444; box-shadow: 0 0 15px rgba(255, 68, 68, 0.5);
animation: confused-shake 0.5s infinite; filter: grayscale(30%); opacity: 0.9;
}
.gallery-head-wrap { position: relative; display: inline-block; cursor: help; }
@keyframes speech-pulse {
0% { transform: scale(0.8) translateY(0); opacity: 0.6; }
50% { transform: scale(1.1) translateY(-2px); opacity: 1; }
100% { transform: scale(0.8) translateY(0); opacity: 0.6; }
}
.gallery-head-wrap.active:not(.state-question):not(.state-alert)::after {
content: '💬';
animation: speech-pulse 0.8s infinite ease-in-out;
background: #0d1117;
border: 1px solid var(--accent-color);
} }
.avatar-name { font-weight: 700; font-size: 13px; color: #f0f6fc; letter-spacing: 0.5px; margin-bottom: 2px; } .avatar-name { font-weight: 700; font-size: 13px; color: #f0f6fc; letter-spacing: 0.5px; margin-bottom: 2px; }
.avatar-role { font-size: 10px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; line-height: 1.2; word-wrap: break-word; } .avatar-role { font-size: 10px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; line-height: 1.2; word-wrap: break-word; }
@@ -523,7 +663,7 @@
#metrics-grid { grid-template-columns: 1fr 1fr !important; } #metrics-grid { grid-template-columns: 1fr 1fr !important; }
/* Org chart mobile tweaks */ /* Org chart mobile tweaks */
.org-chart { padding-left: 0; padding-right: 0; } .org-chart { padding: 20px 10px; }
.org-branch { display: none; } .org-branch { display: none; }
.org-connector { margin-bottom: 10px; height: 20px; } .org-connector { margin-bottom: 10px; height: 20px; }
.org-level { flex-wrap: wrap; justify-content: center; gap: 15px !important; } .org-level { flex-wrap: wrap; justify-content: center; gap: 15px !important; }
@@ -859,75 +999,96 @@
<span id="agent-status" style="font-size:12px;color:var(--success-color)">Monitoring Active</span> <span id="agent-status" style="font-size:12px;color:var(--success-color)">Monitoring Active</span>
</div> </div>
<div class="org-chart"> <div class="workspace-split" style="display:flex; gap:20px; align-items:flex-start; flex-wrap: wrap;">
<!-- Taso 1 --> <!-- LEFT COLUMN: Org chart & Prompt Editor -->
<div class="org-level"> <div style="flex:1; min-width:300px; overflow-x:auto;">
<div class="avatar-card" id="avatar-client" data-agent="client" onclick="selectAgent('client')"> <div class="org-chart">
<img src="/avatars/kettu_notext.png" alt="Asiakas (Kettu)"> <!-- Taso 1 -->
<div class="avatar-name">Asiakas</div> <div class="org-level">
<div class="avatar-role">Tuoteomistaja</div> <div class="avatar-card" id="avatar-client" data-agent="client" onclick="selectAgent('client')">
<img src="/avatars/kettu_notext.png" alt="Asiakas (Kettu)">
<div class="avatar-name">Asiakas</div>
<div class="avatar-role">Tuoteomistaja</div>
</div>
</div>
<div class="org-connector"></div>
<!-- Taso 2 -->
<div class="org-level" style="position: relative;">
<!-- Tarkkailija laitetaan erilleen kauemmas sivuun jotta se näyttää itsenäiseltä valvojalta -->
<div class="avatar-card" id="avatar-observer" data-agent="observer" onclick="selectAgent('observer')" style="position: absolute; right: calc(50% + 350px); top: 0;">
<img src="/avatars/aikuinen_susi.png" alt="Tarkkailija (Aikuinen Susi)">
<div class="avatar-name">Tarkkailija</div>
<div class="avatar-role">Laadunvalvonta</div>
</div>
<div class="avatar-card" id="avatar-kpn" data-agent="manager" onclick="selectAgent('manager')">
<img src="/avatars/karhunpentu.png" alt="Manageri (Karhunpentu)">
<div class="avatar-name">Manageri</div>
<div class="avatar-role">KPN CLI</div>
</div>
</div>
<div class="org-connector"></div>
<div class="org-branch"></div>
<!-- Taso 3 -->
<div class="org-level" style="gap: 20px;">
<div class="avatar-card" id="avatar-coder" data-agent="coder" onclick="selectAgent('coder')">
<img src="/avatars/kipina_notext.png" alt="Koodari (Salamanteri)">
<div class="avatar-name">Koodari</div>
<div class="avatar-role">SOFTAKEHITYS</div>
</div>
<div class="avatar-card" id="avatar-data" data-agent="data" onclick="selectAgent('data')">
<img src="/avatars/pesukarhu_notext.png" alt="Data-Agentti (Pesukarhu)">
<div class="avatar-name">Data</div>
<div class="avatar-role">Tietokannat</div>
</div>
<div class="avatar-card" id="avatar-qa" data-agent="qa" onclick="selectAgent('qa')">
<img src="/avatars/susi_notext.png" alt="QA (Pikkususi)">
<div class="avatar-name">QA</div>
<div class="avatar-role">Testaus</div>
</div>
<div class="avatar-card" id="avatar-tester" data-agent="tester" onclick="selectAgent('tester')">
<img src="/avatars/laiskiainen_notext.png" alt="DevOps (Laiskiainen)">
<div class="avatar-name">DevOps</div>
<div class="avatar-role">Käyttöönotto</div>
</div>
</div>
</div>
<!-- Prompt Editor -->
<div class="agent-prompt-editor" id="agent-prompt-editor" style="margin-top:20px;">
<div class="agent-prompt-label">
<strong id="agent-prompt-name"></strong>
<span id="agent-prompt-saved" style="color:var(--success-color);opacity:0;transition:opacity 0.3s">Tallennettu</span>
</div>
<textarea id="agent-prompt-text" placeholder="Kirjoita system prompt..."></textarea>
<div id="shared-prompt-section" style="display:none;margin-top:8px;font-size:12px;color:#8b949e">
Yhteinen konteksti liitetään jokaisen valitun agentin oman promptin alkuun.
</div>
</div> </div>
</div> </div>
<div class="org-connector"></div> <!-- RIGHT COLUMN: Puhuvat Päät Gallery -->
<div style="flex-basis:150px; flex-shrink:0;">
<!-- Taso 2 --> <div style="background:rgba(1, 4, 9, 0.6); border:1px solid var(--border-color); border-radius:6px; padding:12px; height: 100%;">
<div class="org-level" style="position: relative;"> <div id="all-heads-gallery" style="display:flex; flex-wrap:wrap; gap:10px; justify-content:center;">
<!-- Tarkkailija laitetaan erilleen kauemmas sivuun jotta se näyttää itsenäiseltä valvojalta --> <div class="gallery-head-wrap" id="wrap-client"><img src="/avatars/kettu_notext.png" id="gallery-client" class="gallery-head" alt="Asiakas"></div>
<div class="avatar-card" id="avatar-observer" data-agent="observer" onclick="selectAgent('observer')" style="position: absolute; right: calc(50% + 350px); top: 0;"> <div class="gallery-head-wrap" id="wrap-observer"><img src="/avatars/aikuinen_susi.png" id="gallery-observer" class="gallery-head" alt="Tarkkailija"></div>
<img src="/avatars/aikuinen_susi.png" alt="Tarkkailija (Aikuinen Susi)"> <div class="gallery-head-wrap" id="wrap-manager"><img src="/avatars/karhunpentu.png" id="gallery-manager" class="gallery-head" alt="Manageri"></div>
<div class="avatar-name">Tarkkailija</div> <div class="gallery-head-wrap" id="wrap-coder"><img src="/avatars/kipina_notext.png" id="gallery-coder" class="gallery-head" alt="Koodari"></div>
<div class="avatar-role">Laadunvalvonta</div> <div class="gallery-head-wrap" id="wrap-data"><img src="/avatars/pesukarhu_notext.png" id="gallery-data" class="gallery-head" alt="Data"></div>
</div> <div class="gallery-head-wrap" id="wrap-qa"><img src="/avatars/susi_notext.png" id="gallery-qa" class="gallery-head" alt="QA"></div>
<div class="gallery-head-wrap" id="wrap-tester"><img src="/avatars/laiskiainen_notext.png" id="gallery-tester" class="gallery-head" alt="DevOps"></div>
<div class="avatar-card" id="avatar-kpn" data-agent="manager" onclick="selectAgent('manager')"> </div>
<img src="/avatars/karhunpentu.png" alt="Manageri (Karhunpentu)">
<div class="avatar-name">Manageri</div>
<div class="avatar-role">KPN CLI</div>
</div>
</div>
<div class="org-connector"></div>
<div class="org-branch"></div>
<!-- Taso 3 -->
<div class="org-level" style="gap: 20px;">
<div class="avatar-card" id="avatar-coder" data-agent="coder" onclick="selectAgent('coder')">
<img src="/avatars/kipina_notext.png" alt="Koodari (Salamanteri)">
<div class="avatar-name">Koodari</div>
<div class="avatar-role">Ohjelmistokehitys</div>
</div>
<div class="avatar-card" id="avatar-data" data-agent="data" onclick="selectAgent('data')">
<img src="/avatars/pesukarhu_notext.png" alt="Data-Agentti (Pesukarhu)">
<div class="avatar-name">Data</div>
<div class="avatar-role">Tietokannat</div>
</div>
<div class="avatar-card" id="avatar-qa" data-agent="qa" onclick="selectAgent('qa')">
<img src="/avatars/susi_notext.png" alt="QA (Pikkususi)">
<div class="avatar-name">QA</div>
<div class="avatar-role">Testaus</div>
</div>
<div class="avatar-card" id="avatar-tester" data-agent="tester" onclick="selectAgent('tester')">
<img src="/avatars/laiskiainen_notext.png" alt="DevOps (Laiskiainen)">
<div class="avatar-name">DevOps</div>
<div class="avatar-role">Käyttöönotto</div>
</div> </div>
</div> </div>
</div> </div>
<div class="agent-prompt-editor" id="agent-prompt-editor"> <div class="terminal-panel" id="agent-terminal" style="margin-top: 20px;">
<div class="agent-prompt-label">
<strong id="agent-prompt-name"></strong>
<span id="agent-prompt-saved" style="color:var(--success-color);opacity:0;transition:opacity 0.3s">Tallennettu</span>
</div>
<textarea id="agent-prompt-text" placeholder="Kirjoita system prompt..."></textarea>
<div id="shared-prompt-section" style="display:none;margin-top:8px;font-size:12px;color:#8b949e">
Yhteinen konteksti liitetään jokaisen valitun agentin oman promptin alkuun.
</div>
</div>
<div class="terminal-panel" id="agent-terminal">
<div class="terminal-line"><span class="terminal-prompt">$</span> kpn hub connect wss://localhost</div> <div class="terminal-line"><span class="terminal-prompt">$</span> kpn hub connect wss://localhost</div>
<div class="terminal-line" style="color:#a5d6ff"> ✓ Yhdistetty Kipinä Hubiin</div> <div class="terminal-line" style="color:#a5d6ff"> ✓ Yhdistetty Kipinä Hubiin</div>
</div> </div>
@@ -981,6 +1142,32 @@
} }
} }
// Piilotettu ominaisuus: Puhuvien videoiden / gif-animaatioiden kytkentä
// Aseta true kun olet luonut _puhuva.gif tiedostot /avatars -kansioon
window.USE_ANIMATED_GIFS = false;
// Update gallery heads
document.querySelectorAll('.gallery-head').forEach(el => {
el.classList.remove('active');
if (window.USE_ANIMATED_GIFS) {
el.src = el.src.replace('_puhuva.gif', '.png');
}
});
document.querySelectorAll('.gallery-head-wrap').forEach(el => el.classList.remove('active'));
selectedAgents.forEach(agent => {
const gel = document.getElementById('gallery-' + agent);
const wrap = document.getElementById('wrap-' + agent);
if (gel) {
gel.classList.add('active');
if (window.USE_ANIMATED_GIFS) {
gel.src = gel.src.replace('.png', '_puhuva.gif');
}
}
if (wrap) wrap.classList.add('active');
});
checkAgentConfusion();
if (selectedAgents.size === 0) { if (selectedAgents.size === 0) {
editor.classList.remove('visible'); editor.classList.remove('visible');
return; return;
@@ -988,6 +1175,9 @@
editor.classList.add('visible'); editor.classList.add('visible');
if (selectedAgents.size === 1) { if (selectedAgents.size === 1) {
const agent = [...selectedAgents][0]; const agent = [...selectedAgents][0];
const cfg = agentPrompts[agent]; const cfg = agentPrompts[agent];
@@ -1062,10 +1252,74 @@
localStorage.setItem('kpn-shared-prompt', e.target.value); localStorage.setItem('kpn-shared-prompt', e.target.value);
} }
checkAgentConfusion();
saved.style.opacity = '1'; saved.style.opacity = '1';
clearTimeout(saved._t); clearTimeout(saved._t);
saved._t = setTimeout(() => saved.style.opacity = '0', 1500); saved._t = setTimeout(() => saved.style.opacity = '0', 1500);
}); });
function checkAgentConfusion() {
Object.keys(agentPrompts).forEach(agent => {
const prompt = agentPrompts[agent].prompt || "";
const wrap = document.getElementById('wrap-' + agent);
const gel = document.getElementById('gallery-' + agent);
if (!wrap || !gel) return;
// Nollataan tilat
wrap.classList.remove('state-question', 'state-alert');
gel.classList.remove('state-question', 'state-alert');
wrap.removeAttribute('data-tooltip');
const pLow = prompt.toLowerCase();
const agentTitle = (agentPrompts[agent].name.split(' — ')[0] || "AGENTTI").toUpperCase();
// Hälytys / Virhe
if (pLow.includes('todo') || pLow.includes('viallinen') || pLow.includes('virhe')) {
wrap.classList.add('state-alert');
gel.classList.add('state-alert');
wrap.setAttribute('data-tooltip', `${agentTitle}: "Kriittinen virhe ohjeistuksessa!"\n(Koodissa tai promptissa esiintyy teksti TODO, viallinen tai virhe. Korjaa ohje välittömästi.)`);
}
// Kysyttävää / Hämmennys
else if (prompt.trim().length <= 15 || prompt.includes('?')) {
wrap.classList.add('state-question');
gel.classList.add('state-question');
const questions = {
client: 'Millaisia uusia ominaisuuksia tuotteessa pitäisi olla?',
manager: 'Kuka asiantuntijoista ottaa vastuulleen tämän taskin?',
coder: 'Käytetäänkö tässä komponentissa uusinta React-ohjetta?',
data: 'Millainen tietorakenne käyttäjästä tallennetaan kantaan?',
qa: 'Onko tälle koodille olemassa jo kattavat yksikkötestit?',
tester: 'Mihin haluaisit julkaista tämän laiteympäristön version?',
observer: 'Mitä laatumetriikkoja minun tulisi ensisijaisesti painottaa?'
};
const exampleQ = questions[agent] || 'Mitä minun pitäisi tehdä seuraavaksi?';
const reason = prompt.trim().length <= 15 ? 'Määrittely on tällä hetkellä liian lyhyt.' : 'Ohje on jätetty avoimeksi (? -merkki).';
wrap.setAttribute('data-tooltip', `${agentTitle}: "${exampleQ}"\n\n(Agentti odottaa päätöstäsi: ${reason})`);
}
// Normaali keskustelu aktiivisena
else if (selectedAgents.has(agent)) {
// Haetaan satunnainen "toinen agentti", johon viitata
// Tehdään tästä deterministinen agentin nimen perusteella, ettei vilku
const targets = {
client: 'Managerin',
manager: 'Asiakkaan',
coder: 'DevOpsin',
data: 'Koodarin',
qa: 'Data-Agentin',
tester: 'QA:n',
observer: 'Managerin'
};
const targetName = targets[agent] || 'Koodarin';
wrap.setAttribute('data-tooltip', `💬 ${agentTitle}: "Hei, minäkin haluan osallistua!\nVoisin tehdä ${targetName} asiaan tällaisen toiminnallisuuden!"`);
}
});
}
// Tarkistetaan heti alussa
setTimeout(checkAgentConfusion, 100);
window.switchMainTab = function(tab) { window.switchMainTab = function(tab) {
document.querySelectorAll('.main-panel').forEach(p => p.classList.remove('active')); document.querySelectorAll('.main-panel').forEach(p => p.classList.remove('active'));