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

View File

@@ -393,6 +393,7 @@
align-items: center;
margin-bottom: 40px;
perspective: 1000px;
padding: 25px 50px;
}
.org-level {
display: flex;
@@ -439,6 +440,18 @@
border-color: rgba(240, 246, 252, 0.3);
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 {
width: 80px;
height: 80px;
@@ -448,7 +461,10 @@
transition: all 0.4s ease;
object-fit: cover;
background: #010409;
animation: idle-breathe 4s infinite ease-in-out;
transform-origin: bottom center;
}
.avatar-card.active, .avatar-card.selected {
opacity: 1;
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);
z-index: 2;
}
.avatar-card.active img, .avatar-card.selected img {
.avatar-card.selected img {
border-color: var(--accent-color);
box-shadow: 0 0 25px rgba(88, 166, 255, 0.5);
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-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; }
/* Org chart mobile tweaks */
.org-chart { padding-left: 0; padding-right: 0; }
.org-chart { padding: 20px 10px; }
.org-branch { display: none; }
.org-connector { margin-bottom: 10px; height: 20px; }
.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>
</div>
<div class="org-chart">
<!-- Taso 1 -->
<div class="org-level">
<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 class="workspace-split" style="display:flex; gap:20px; align-items:flex-start; flex-wrap: wrap;">
<!-- LEFT COLUMN: Org chart & Prompt Editor -->
<div style="flex:1; min-width:300px; overflow-x:auto;">
<div class="org-chart">
<!-- Taso 1 -->
<div class="org-level">
<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 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">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>
<!-- RIGHT COLUMN: Puhuvat Päät Gallery -->
<div style="flex-basis:150px; flex-shrink:0;">
<div style="background:rgba(1, 4, 9, 0.6); border:1px solid var(--border-color); border-radius:6px; padding:12px; height: 100%;">
<div id="all-heads-gallery" style="display:flex; flex-wrap:wrap; gap:10px; justify-content:center;">
<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="gallery-head-wrap" id="wrap-observer"><img src="/avatars/aikuinen_susi.png" id="gallery-observer" class="gallery-head" alt="Tarkkailija"></div>
<div class="gallery-head-wrap" id="wrap-manager"><img src="/avatars/karhunpentu.png" id="gallery-manager" class="gallery-head" alt="Manageri"></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="gallery-head-wrap" id="wrap-data"><img src="/avatars/pesukarhu_notext.png" id="gallery-data" class="gallery-head" alt="Data"></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>
</div>
</div>
</div>
<div class="agent-prompt-editor" id="agent-prompt-editor">
<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-panel" id="agent-terminal" style="margin-top: 20px;">
<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>
@@ -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) {
editor.classList.remove('visible');
return;
@@ -988,6 +1175,9 @@
editor.classList.add('visible');
if (selectedAgents.size === 1) {
const agent = [...selectedAgents][0];
const cfg = agentPrompts[agent];
@@ -1062,10 +1252,74 @@
localStorage.setItem('kpn-shared-prompt', e.target.value);
}
checkAgentConfusion();
saved.style.opacity = '1';
clearTimeout(saved._t);
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) {
document.querySelectorAll('.main-panel').forEach(p => p.classList.remove('active'));