Spaces:
Running
Running
feat: add animated D3.js architecture diagram to Архітектура tab (#5)
Browse filesInteractive SVG diagram with:
- 9 agent boxes with hover tooltips
- Animated flow arrows (6 flow types with filter chips)
- ConsultationState panel
- Research loop indicator
- Sequential entry animation
Replaces static ASCII art with rich visualization.
Co-authored-by: overthelex <mcvovkes@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- app.py +2 -53
- architecture.html +562 -0
app.py
CHANGED
|
@@ -163,58 +163,7 @@ def stream_chat(message: str, history: list[dict]):
|
|
| 163 |
yield accumulated
|
| 164 |
|
| 165 |
|
| 166 |
-
|
| 167 |
-
## Архітектура LMAF
|
| 168 |
-
|
| 169 |
-
Дев'ять спеціалізованих LLM-агентів працюють у циклі. Кожен агент починає з чистого контексту.
|
| 170 |
-
Весь стан зберігається у структурованому об'єкті `ConsultationState`.
|
| 171 |
-
|
| 172 |
-
```
|
| 173 |
-
Запит клієнта
|
| 174 |
-
│
|
| 175 |
-
▼
|
| 176 |
-
┌─────────────┐
|
| 177 |
-
│ Surveyor │ Огляд правового ландшафту
|
| 178 |
-
└──────┬──────┘
|
| 179 |
-
▼
|
| 180 |
-
┌─────────────┐
|
| 181 |
-
│ Planner │ Стратегія дослідження
|
| 182 |
-
└──────┬──────┘
|
| 183 |
-
▼
|
| 184 |
-
┌─────────────┐ ┌────────────┐
|
| 185 |
-
│Orchestrator │────►│ Researcher │ Судова практика, НПА
|
| 186 |
-
│ (dispatch) │────►│ Analyst │ Обчислення, строки
|
| 187 |
-
└──────┬──────┘ └─────┬──────┘
|
| 188 |
-
│ │
|
| 189 |
-
▼ ▼
|
| 190 |
-
┌─────────────┐ ┌────────────┐
|
| 191 |
-
│ Reviewer │ │ Critic │ Аудит стратегії
|
| 192 |
-
│(верифікація)│ │ (periodic) │
|
| 193 |
-
└──────┬──────┘ └─────┬──────┘
|
| 194 |
-
│ │
|
| 195 |
-
▼ ▼
|
| 196 |
-
┌─────────────┐ ┌──────────────┐
|
| 197 |
-
│ Adjudicator │ │ Planner │
|
| 198 |
-
│ (арбітраж) │ │ (revision) │
|
| 199 |
-
└──────┬──────┘ └──────────────┘
|
| 200 |
-
▼
|
| 201 |
-
┌─────────────┐
|
| 202 |
-
│ Formatter │ Фінальна консультація
|
| 203 |
-
└─────────────┘
|
| 204 |
-
```
|
| 205 |
-
|
| 206 |
-
| Агент | Роль | Аналог в secondlayer-core |
|
| 207 |
-
|-------|------|--------------------------|
|
| 208 |
-
| **Surveyor** | Карта правового ландшафту | IntentClassifier + QueryPlanner |
|
| 209 |
-
| **Planner** | Стратегія дослідження | Генерація ExecutionPlan |
|
| 210 |
-
| **Orchestrator** | Координація та гіпотези | Агентний цикл ChatService |
|
| 211 |
-
| **Researcher** | Пошук практики та НПА | Виклики інструментів |
|
| 212 |
-
| **Analyst** | Обчислення строків, сум | Калькуляційні інструменти |
|
| 213 |
-
| **Reviewer** | Верифікація доказів | CitationValidator + HallucinationGuard |
|
| 214 |
-
| **Critic** | Аудит стратегії | *Нова можливість* |
|
| 215 |
-
| **Adjudicator** | Арбітраж розбіжностей | *Нова можливість* |
|
| 216 |
-
| **Formatter** | Оформлення консультації | Синтез відповіді |
|
| 217 |
-
"""
|
| 218 |
|
| 219 |
|
| 220 |
DATASETS_MD = """
|
|
@@ -265,7 +214,7 @@ def build_app() -> gr.Blocks:
|
|
| 265 |
)
|
| 266 |
|
| 267 |
with gr.Tab("Архітектура"):
|
| 268 |
-
gr.
|
| 269 |
|
| 270 |
with gr.Tab("Датасети"):
|
| 271 |
gr.Markdown(DATASETS_MD)
|
|
|
|
| 163 |
yield accumulated
|
| 164 |
|
| 165 |
|
| 166 |
+
ARCHITECTURE_HTML = (Path(__file__).parent / "architecture.html").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
|
| 169 |
DATASETS_MD = """
|
|
|
|
| 214 |
)
|
| 215 |
|
| 216 |
with gr.Tab("Архітектура"):
|
| 217 |
+
gr.HTML(ARCHITECTURE_HTML)
|
| 218 |
|
| 219 |
with gr.Tab("Датасети"):
|
| 220 |
gr.Markdown(DATASETS_MD)
|
architecture.html
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div id="lmaf-arch"></div>
|
| 2 |
+
|
| 3 |
+
<style>
|
| 4 |
+
#lmaf-arch {
|
| 5 |
+
width: 100%;
|
| 6 |
+
font-family: system-ui, -apple-system, sans-serif;
|
| 7 |
+
color: #e0e0e0;
|
| 8 |
+
background: #1a1a2e;
|
| 9 |
+
border-radius: 12px;
|
| 10 |
+
overflow: hidden;
|
| 11 |
+
position: relative;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
/* Filter bar */
|
| 15 |
+
#lmaf-arch .filter-bar {
|
| 16 |
+
display: flex;
|
| 17 |
+
flex-wrap: wrap;
|
| 18 |
+
align-items: center;
|
| 19 |
+
gap: 8px;
|
| 20 |
+
padding: 14px 20px 10px;
|
| 21 |
+
font-size: 12px;
|
| 22 |
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
| 23 |
+
}
|
| 24 |
+
#lmaf-arch .filter-bar__label {
|
| 25 |
+
font-size: 11px;
|
| 26 |
+
letter-spacing: 0.06em;
|
| 27 |
+
text-transform: uppercase;
|
| 28 |
+
color: #888;
|
| 29 |
+
margin-right: 4px;
|
| 30 |
+
}
|
| 31 |
+
#lmaf-arch .filter-chip {
|
| 32 |
+
display: inline-flex;
|
| 33 |
+
align-items: center;
|
| 34 |
+
gap: 6px;
|
| 35 |
+
padding: 4px 12px;
|
| 36 |
+
border-radius: 999px;
|
| 37 |
+
border: 1px solid rgba(255,255,255,0.15);
|
| 38 |
+
background: transparent;
|
| 39 |
+
color: #999;
|
| 40 |
+
cursor: pointer;
|
| 41 |
+
transition: all .2s ease;
|
| 42 |
+
font-size: 12px;
|
| 43 |
+
font-family: inherit;
|
| 44 |
+
}
|
| 45 |
+
#lmaf-arch .filter-chip:hover {
|
| 46 |
+
color: #fff;
|
| 47 |
+
border-color: rgba(255,255,255,0.4);
|
| 48 |
+
}
|
| 49 |
+
#lmaf-arch .filter-chip.is-active {
|
| 50 |
+
color: #fff;
|
| 51 |
+
border-color: currentColor;
|
| 52 |
+
background: rgba(255,255,255,0.06);
|
| 53 |
+
}
|
| 54 |
+
#lmaf-arch .chip-swatch {
|
| 55 |
+
width: 10px;
|
| 56 |
+
height: 10px;
|
| 57 |
+
border-radius: 2px;
|
| 58 |
+
flex: 0 0 auto;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* SVG */
|
| 62 |
+
#lmaf-arch svg {
|
| 63 |
+
display: block;
|
| 64 |
+
width: 100%;
|
| 65 |
+
height: auto;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/* Tooltip */
|
| 69 |
+
#lmaf-arch .arch-tooltip {
|
| 70 |
+
position: absolute;
|
| 71 |
+
pointer-events: none;
|
| 72 |
+
background: rgba(20,20,40,0.95);
|
| 73 |
+
border: 1px solid rgba(255,255,255,0.15);
|
| 74 |
+
border-radius: 8px;
|
| 75 |
+
padding: 12px 16px;
|
| 76 |
+
font-size: 13px;
|
| 77 |
+
line-height: 1.5;
|
| 78 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
| 79 |
+
opacity: 0;
|
| 80 |
+
transition: opacity .15s ease;
|
| 81 |
+
max-width: 280px;
|
| 82 |
+
z-index: 100;
|
| 83 |
+
backdrop-filter: blur(8px);
|
| 84 |
+
}
|
| 85 |
+
#lmaf-arch .arch-tooltip__title {
|
| 86 |
+
font-weight: 700;
|
| 87 |
+
font-size: 14px;
|
| 88 |
+
margin-bottom: 2px;
|
| 89 |
+
}
|
| 90 |
+
#lmaf-arch .arch-tooltip__sub {
|
| 91 |
+
font-size: 11px;
|
| 92 |
+
color: #888;
|
| 93 |
+
text-transform: uppercase;
|
| 94 |
+
letter-spacing: 0.06em;
|
| 95 |
+
margin-bottom: 6px;
|
| 96 |
+
}
|
| 97 |
+
#lmaf-arch .arch-tooltip__body {
|
| 98 |
+
color: #ccc;
|
| 99 |
+
font-size: 12px;
|
| 100 |
+
}
|
| 101 |
+
</style>
|
| 102 |
+
|
| 103 |
+
<script>
|
| 104 |
+
(() => {
|
| 105 |
+
const ensureD3 = (cb) => {
|
| 106 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 107 |
+
let s = document.getElementById('d3-cdn-lmaf');
|
| 108 |
+
if (!s) {
|
| 109 |
+
s = document.createElement('script');
|
| 110 |
+
s.id = 'd3-cdn-lmaf';
|
| 111 |
+
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 112 |
+
document.head.appendChild(s);
|
| 113 |
+
}
|
| 114 |
+
s.addEventListener('load', () => { if (window.d3) cb(); }, { once: true });
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
ensureD3(() => {
|
| 118 |
+
const root = document.querySelector('#lmaf-arch');
|
| 119 |
+
if (!root || root.dataset.mounted === 'true') return;
|
| 120 |
+
root.dataset.mounted = 'true';
|
| 121 |
+
const d3 = window.d3;
|
| 122 |
+
|
| 123 |
+
const VB_W = 1100, VB_H = 620;
|
| 124 |
+
|
| 125 |
+
// Flow types
|
| 126 |
+
const FLOWS = [
|
| 127 |
+
{ id: 'landscape', label: 'Правовий ландшафт', color: '#5b9bd5' },
|
| 128 |
+
{ id: 'strategy', label: 'Стратегія', color: '#a855f7' },
|
| 129 |
+
{ id: 'task', label: 'Дослідне завдання', color: '#f59e0b' },
|
| 130 |
+
{ id: 'evidence', label: 'Докази', color: '#10b981' },
|
| 131 |
+
{ id: 'critique', label: 'Критика', color: '#ef4444' },
|
| 132 |
+
{ id: 'verdict', label: 'Вердикт', color: '#f97316' },
|
| 133 |
+
];
|
| 134 |
+
const flowById = Object.fromEntries(FLOWS.map(f => [f.id, f]));
|
| 135 |
+
|
| 136 |
+
// ConsultationState box
|
| 137 |
+
const STATE_BOX = {
|
| 138 |
+
x: 148, y: 345, w: 250, h: 430,
|
| 139 |
+
items: [
|
| 140 |
+
{ text: 'Запит клієнта' },
|
| 141 |
+
{ text: 'Юрисдикція' },
|
| 142 |
+
{ text: 'Огляд ландшафту', flow: 'landscape' },
|
| 143 |
+
{ text: 'Стратегія дослідження', flow: 'strategy' },
|
| 144 |
+
{ text: 'Правові позиції (гіпотези)', flow: 'evidence' },
|
| 145 |
+
{ text: 'Зібрані докази', flow: 'evidence' },
|
| 146 |
+
{ text: 'Відкриті питання', flow: 'task' },
|
| 147 |
+
{ text: 'Критичні зауваження', flow: 'critique' },
|
| 148 |
+
{ text: 'Обчислення та строки' },
|
| 149 |
+
{ text: 'Фінальна консультація' },
|
| 150 |
+
]
|
| 151 |
+
};
|
| 152 |
+
|
| 153 |
+
// Agent definitions
|
| 154 |
+
const AGENTS = [
|
| 155 |
+
{
|
| 156 |
+
id: 'question', label: 'Запит клієнта', kind: 'io',
|
| 157 |
+
x: 148, y: 60, w: 190, h: 55,
|
| 158 |
+
tip: ['Запит клієнта', 'Вхідні дані', 'Правове питання від користувача, яке запускає мульти-агентний пайплайн.']
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
id: 'surveyor', label: 'Surveyor', kind: 'agent',
|
| 162 |
+
x: 440, y: 60, w: 190, h: 55,
|
| 163 |
+
tip: ['Surveyor', 'Запускається один раз', 'Будує карту правового ландшафту: визначає галузі права, релев��нтне законодавство, ключові правові інститути.']
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
id: 'planner', label: 'Planner', kind: 'agent',
|
| 167 |
+
x: 740, y: 60, w: 190, h: 55,
|
| 168 |
+
tip: ['Planner', 'До циклу + при критиці', 'Формує стратегію дослідження: які питання досліджувати, яке законодавство аналізувати, які гіпотези перевіряти.']
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
id: 'orchestrator', label: 'Orchestrator', kind: 'orchestrator',
|
| 172 |
+
x: 740, y: 210, w: 260, h: 90,
|
| 173 |
+
tip: ['Orchestrator', 'Серце циклу', 'Читає ConsultationState, формулює гіпотези, диспетчеризує завдання Researcher або Analyst, вирішує коли зупинитись.']
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
id: 'researcher', label: 'Researcher', kind: 'agent',
|
| 177 |
+
x: 620, y: 345, w: 165, h: 55,
|
| 178 |
+
tip: ['Researcher', 'Пошук практики', 'Шукає судову практику, законодавство та доктрину через SecondLayer API (100М+ рішень ЄДРСР).']
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
id: 'analyst', label: 'Analyst', kind: 'agent',
|
| 182 |
+
x: 860, y: 345, w: 155, h: 55,
|
| 183 |
+
tip: ['Analyst', 'Обчислення', 'Розраховує строки, пеню, 3% річних, інфляційні втрати, процесуальні дедлайни.']
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
id: 'reviewer', label: 'Reviewer', kind: 'agent',
|
| 187 |
+
x: 740, y: 435, w: 165, h: 55,
|
| 188 |
+
tip: ['Reviewer', 'Автоматична верифікація', 'Перевіряє кожен доказ: чи існує рішення, чи правильно цитовано, чи відповідає позиції. Вердикт: VERIFIED / REFUTED.']
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
id: 'critic', label: 'Critic', kind: 'agent',
|
| 192 |
+
x: 440, y: 435, w: 175, h: 55,
|
| 193 |
+
tip: ['Critic', 'Періодичний аудит', 'Аудит стратегії та повноти аналізу. Виявляє прогалини, суперечності, пропущені аргументи.']
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
id: 'adjudicator', label: 'Adjudicator', kind: 'agent',
|
| 197 |
+
x: 620, y: 540, w: 195, h: 55,
|
| 198 |
+
tip: ['Adjudicator', 'Незалежний арбітр', 'Арбітрує конфлікти між агентами. Єдиний хто може скасувати встановлену правову позицію.']
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
id: 'formatter', label: 'Formatter', kind: 'agent',
|
| 202 |
+
x: 970, y: 490, w: 175, h: 55,
|
| 203 |
+
tip: ['Formatter', 'Фінальний етап', 'Оформлює структуровану правову консультацію з посиланнями на НПА та судову практику.']
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
id: 'answer', label: 'Консультація', kind: 'io',
|
| 207 |
+
x: 970, y: 575, w: 160, h: 45,
|
| 208 |
+
tip: ['Консультація', 'Результат', 'Структурована правова консультація з обґрунтуванням, посиланнями та рекомендаціями.']
|
| 209 |
+
}
|
| 210 |
+
];
|
| 211 |
+
const agentById = Object.fromEntries(AGENTS.map(a => [a.id, a]));
|
| 212 |
+
|
| 213 |
+
// Color palette for agents
|
| 214 |
+
const PALETTE_ORDER = [
|
| 215 |
+
'orchestrator','surveyor','planner','researcher',
|
| 216 |
+
'analyst','reviewer','critic','adjudicator','formatter'
|
| 217 |
+
];
|
| 218 |
+
const agentPalette = [
|
| 219 |
+
'#6366f1','#5b9bd5','#a855f7','#10b981',
|
| 220 |
+
'#14b8a6','#f59e0b','#ef4444','#f97316','#ec4899'
|
| 221 |
+
];
|
| 222 |
+
const agentColor = {};
|
| 223 |
+
PALETTE_ORDER.forEach((id, i) => { agentColor[id] = agentPalette[i]; });
|
| 224 |
+
|
| 225 |
+
// Edges
|
| 226 |
+
const EDGES = [
|
| 227 |
+
{ from: 'question', to: 'surveyor', flow: 'landscape', label: '' },
|
| 228 |
+
{ from: 'surveyor', to: 'planner', flow: 'strategy', label: '' },
|
| 229 |
+
{ from: 'planner', to: 'orchestrator', flow: 'strategy', label: '' },
|
| 230 |
+
{ from: 'orchestrator', to: 'researcher', flow: 'task', label: '' },
|
| 231 |
+
{ from: 'orchestrator', to: 'analyst', flow: 'task', label: '' },
|
| 232 |
+
{ from: 'researcher', to: 'reviewer', flow: 'evidence', label: '' },
|
| 233 |
+
{ from: 'analyst', to: 'reviewer', flow: 'evidence', label: '' },
|
| 234 |
+
{ from: 'reviewer', to: 'orchestrator', flow: 'evidence', label: '' },
|
| 235 |
+
{ from: 'critic', to: 'planner', flow: 'critique', label: '' },
|
| 236 |
+
{ from: 'critic', to: 'adjudicator', flow: 'critique', label: '' },
|
| 237 |
+
{ from: 'adjudicator', to: 'orchestrator', flow: 'verdict', label: '' },
|
| 238 |
+
{ from: 'orchestrator', to: 'formatter', flow: 'evidence', label: '' },
|
| 239 |
+
{ from: 'formatter', to: 'answer', flow: 'evidence', label: '' },
|
| 240 |
+
];
|
| 241 |
+
|
| 242 |
+
// Build filter bar
|
| 243 |
+
const filterBar = document.createElement('div');
|
| 244 |
+
filterBar.className = 'filter-bar';
|
| 245 |
+
const filterLabel = document.createElement('span');
|
| 246 |
+
filterLabel.className = 'filter-bar__label';
|
| 247 |
+
filterLabel.textContent = 'Потоки:';
|
| 248 |
+
filterBar.appendChild(filterLabel);
|
| 249 |
+
|
| 250 |
+
const activeFlows = new Set(FLOWS.map(f => f.id));
|
| 251 |
+
|
| 252 |
+
FLOWS.forEach(f => {
|
| 253 |
+
const chip = document.createElement('button');
|
| 254 |
+
chip.className = 'filter-chip is-active';
|
| 255 |
+
chip.innerHTML = `<span class="chip-swatch" style="background:${f.color}"></span>${f.label}`;
|
| 256 |
+
chip.dataset.flow = f.id;
|
| 257 |
+
chip.addEventListener('click', () => {
|
| 258 |
+
if (activeFlows.has(f.id)) { activeFlows.delete(f.id); chip.classList.remove('is-active'); }
|
| 259 |
+
else { activeFlows.add(f.id); chip.classList.add('is-active'); }
|
| 260 |
+
updateVisibility();
|
| 261 |
+
});
|
| 262 |
+
filterBar.appendChild(chip);
|
| 263 |
+
});
|
| 264 |
+
root.appendChild(filterBar);
|
| 265 |
+
|
| 266 |
+
// Tooltip
|
| 267 |
+
const tooltip = document.createElement('div');
|
| 268 |
+
tooltip.className = 'arch-tooltip';
|
| 269 |
+
root.appendChild(tooltip);
|
| 270 |
+
|
| 271 |
+
// SVG
|
| 272 |
+
const svg = d3.select(root).append('svg')
|
| 273 |
+
.attr('viewBox', `0 0 ${VB_W} ${VB_H}`)
|
| 274 |
+
.attr('preserveAspectRatio', 'xMidYMid meet');
|
| 275 |
+
|
| 276 |
+
// Defs: arrow markers & glow
|
| 277 |
+
const defs = svg.append('defs');
|
| 278 |
+
|
| 279 |
+
FLOWS.forEach(f => {
|
| 280 |
+
defs.append('marker')
|
| 281 |
+
.attr('id', `arrow-${f.id}`)
|
| 282 |
+
.attr('viewBox', '0 0 10 10')
|
| 283 |
+
.attr('refX', 9).attr('refY', 5)
|
| 284 |
+
.attr('markerWidth', 7).attr('markerHeight', 7)
|
| 285 |
+
.attr('orient', 'auto-start-reverse')
|
| 286 |
+
.append('path')
|
| 287 |
+
.attr('d', 'M 0 1 L 10 5 L 0 9 Z')
|
| 288 |
+
.attr('fill', f.color);
|
| 289 |
+
});
|
| 290 |
+
|
| 291 |
+
// Glow filter
|
| 292 |
+
const glow = defs.append('filter').attr('id', 'glow');
|
| 293 |
+
glow.append('feGaussianBlur').attr('stdDeviation', '3').attr('result', 'blur');
|
| 294 |
+
glow.append('feMerge').selectAll('feMergeNode')
|
| 295 |
+
.data(['blur', 'SourceGraphic']).enter()
|
| 296 |
+
.append('feMergeNode').attr('in', d => d);
|
| 297 |
+
|
| 298 |
+
// Draw state box
|
| 299 |
+
const sb = STATE_BOX;
|
| 300 |
+
const stateG = svg.append('g').attr('class', 'state-group');
|
| 301 |
+
|
| 302 |
+
stateG.append('rect')
|
| 303 |
+
.attr('x', sb.x - sb.w/2).attr('y', sb.y - sb.h/2)
|
| 304 |
+
.attr('width', sb.w).attr('height', sb.h)
|
| 305 |
+
.attr('rx', 14).attr('ry', 14)
|
| 306 |
+
.attr('fill', 'rgba(255,255,255,0.03)')
|
| 307 |
+
.attr('stroke', 'rgba(255,255,255,0.12)')
|
| 308 |
+
.attr('stroke-width', 1.4)
|
| 309 |
+
.attr('stroke-dasharray', '6 4');
|
| 310 |
+
|
| 311 |
+
stateG.append('text')
|
| 312 |
+
.attr('x', sb.x).attr('y', sb.y - sb.h/2 + 24)
|
| 313 |
+
.attr('text-anchor', 'middle')
|
| 314 |
+
.attr('fill', '#999')
|
| 315 |
+
.attr('font-size', '12px')
|
| 316 |
+
.attr('font-weight', '600')
|
| 317 |
+
.attr('letter-spacing', '0.06em')
|
| 318 |
+
.text('CONSULTATION STATE');
|
| 319 |
+
|
| 320 |
+
sb.items.forEach((item, i) => {
|
| 321 |
+
const yPos = sb.y - sb.h/2 + 50 + i * 36;
|
| 322 |
+
stateG.append('rect')
|
| 323 |
+
.attr('x', sb.x - sb.w/2 + 14)
|
| 324 |
+
.attr('y', yPos - 12)
|
| 325 |
+
.attr('width', sb.w - 28)
|
| 326 |
+
.attr('height', 28)
|
| 327 |
+
.attr('rx', 6)
|
| 328 |
+
.attr('fill', item.flow ? `${flowById[item.flow].color}15` : 'rgba(255,255,255,0.04)')
|
| 329 |
+
.attr('stroke', item.flow ? `${flowById[item.flow].color}40` : 'rgba(255,255,255,0.06)')
|
| 330 |
+
.attr('stroke-width', 0.8);
|
| 331 |
+
|
| 332 |
+
stateG.append('text')
|
| 333 |
+
.attr('x', sb.x)
|
| 334 |
+
.attr('y', yPos + 4)
|
| 335 |
+
.attr('text-anchor', 'middle')
|
| 336 |
+
.attr('fill', item.flow ? flowById[item.flow].color : '#aaa')
|
| 337 |
+
.attr('font-size', '11px')
|
| 338 |
+
.text(item.text);
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
// Edge routing helper
|
| 342 |
+
function edgePath(from, to) {
|
| 343 |
+
const a = agentById[from], b = agentById[to];
|
| 344 |
+
const ax = a.x, ay = a.y, bx = b.x, by = b.y;
|
| 345 |
+
const dx = bx - ax, dy = by - ay;
|
| 346 |
+
|
| 347 |
+
// Exit/entry points on box edges
|
| 348 |
+
let sx, sy, ex, ey;
|
| 349 |
+
|
| 350 |
+
if (Math.abs(dx) > Math.abs(dy) * 0.6) {
|
| 351 |
+
// Horizontal-ish
|
| 352 |
+
sx = ax + (dx > 0 ? a.w/2 : -a.w/2);
|
| 353 |
+
sy = ay;
|
| 354 |
+
ex = bx + (dx > 0 ? -b.w/2 : b.w/2);
|
| 355 |
+
ey = by;
|
| 356 |
+
} else {
|
| 357 |
+
// Vertical-ish
|
| 358 |
+
sx = ax;
|
| 359 |
+
sy = ay + (dy > 0 ? a.h/2 : -a.h/2);
|
| 360 |
+
ex = bx;
|
| 361 |
+
ey = by + (dy > 0 ? -b.h/2 : b.h/2);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Curved path
|
| 365 |
+
const mx = (sx + ex) / 2, my = (sy + ey) / 2;
|
| 366 |
+
if (Math.abs(sx - ex) < 5 || Math.abs(sy - ey) < 5) {
|
| 367 |
+
return `M ${sx} ${sy} L ${ex} ${ey}`;
|
| 368 |
+
}
|
| 369 |
+
return `M ${sx} ${sy} C ${sx} ${my}, ${ex} ${my}, ${ex} ${ey}`;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// Draw edges
|
| 373 |
+
const edgeGroup = svg.append('g').attr('class', 'edges');
|
| 374 |
+
|
| 375 |
+
EDGES.forEach(e => {
|
| 376 |
+
const flow = flowById[e.flow];
|
| 377 |
+
const path = edgePath(e.from, e.to);
|
| 378 |
+
|
| 379 |
+
// Glow under-path
|
| 380 |
+
edgeGroup.append('path')
|
| 381 |
+
.attr('d', path)
|
| 382 |
+
.attr('fill', 'none')
|
| 383 |
+
.attr('stroke', flow.color)
|
| 384 |
+
.attr('stroke-width', 4)
|
| 385 |
+
.attr('opacity', 0.15)
|
| 386 |
+
.attr('data-flow', e.flow)
|
| 387 |
+
.attr('class', 'edge-glow');
|
| 388 |
+
|
| 389 |
+
// Main path
|
| 390 |
+
edgeGroup.append('path')
|
| 391 |
+
.attr('d', path)
|
| 392 |
+
.attr('fill', 'none')
|
| 393 |
+
.attr('stroke', flow.color)
|
| 394 |
+
.attr('stroke-width', 1.8)
|
| 395 |
+
.attr('opacity', 0.7)
|
| 396 |
+
.attr('marker-end', `url(#arrow-${e.flow})`)
|
| 397 |
+
.attr('data-flow', e.flow)
|
| 398 |
+
.attr('class', 'edge-main');
|
| 399 |
+
|
| 400 |
+
// Animated dash
|
| 401 |
+
const totalLen = 400;
|
| 402 |
+
edgeGroup.append('path')
|
| 403 |
+
.attr('d', path)
|
| 404 |
+
.attr('fill', 'none')
|
| 405 |
+
.attr('stroke', flow.color)
|
| 406 |
+
.attr('stroke-width', 2.2)
|
| 407 |
+
.attr('opacity', 0.9)
|
| 408 |
+
.attr('stroke-dasharray', '8 20')
|
| 409 |
+
.attr('data-flow', e.flow)
|
| 410 |
+
.attr('class', 'edge-animated')
|
| 411 |
+
.style('animation', `dash-flow ${2 + Math.random()}s linear infinite`);
|
| 412 |
+
});
|
| 413 |
+
|
| 414 |
+
// Draw agents
|
| 415 |
+
const agentGroup = svg.append('g').attr('class', 'agents');
|
| 416 |
+
|
| 417 |
+
AGENTS.forEach(a => {
|
| 418 |
+
const g = agentGroup.append('g')
|
| 419 |
+
.attr('class', `agent agent--${a.kind}`)
|
| 420 |
+
.attr('data-id', a.id)
|
| 421 |
+
.style('cursor', 'pointer');
|
| 422 |
+
|
| 423 |
+
const color = a.kind === 'io' ? '#64748b' :
|
| 424 |
+
a.kind === 'orchestrator' ? agentColor.orchestrator :
|
| 425 |
+
agentColor[a.id] || '#888';
|
| 426 |
+
|
| 427 |
+
// Shadow
|
| 428 |
+
g.append('rect')
|
| 429 |
+
.attr('x', a.x - a.w/2 + 2).attr('y', a.y - a.h/2 + 2)
|
| 430 |
+
.attr('width', a.w).attr('height', a.h)
|
| 431 |
+
.attr('rx', a.kind === 'orchestrator' ? 14 : 10)
|
| 432 |
+
.attr('fill', 'rgba(0,0,0,0.3)');
|
| 433 |
+
|
| 434 |
+
// Box
|
| 435 |
+
g.append('rect')
|
| 436 |
+
.attr('x', a.x - a.w/2).attr('y', a.y - a.h/2)
|
| 437 |
+
.attr('width', a.w).attr('height', a.h)
|
| 438 |
+
.attr('rx', a.kind === 'orchestrator' ? 14 : 10)
|
| 439 |
+
.attr('fill', a.kind === 'io' ? 'rgba(100,116,139,0.15)' : `${color}18`)
|
| 440 |
+
.attr('stroke', a.kind === 'io' ? 'rgba(100,116,139,0.4)' : `${color}60`)
|
| 441 |
+
.attr('stroke-width', a.kind === 'orchestrator' ? 2 : 1.4)
|
| 442 |
+
.attr('class', 'agent-box');
|
| 443 |
+
|
| 444 |
+
// Label
|
| 445 |
+
g.append('text')
|
| 446 |
+
.attr('x', a.x).attr('y', a.y + 1)
|
| 447 |
+
.attr('text-anchor', 'middle')
|
| 448 |
+
.attr('dominant-baseline', 'central')
|
| 449 |
+
.attr('fill', a.kind === 'io' ? '#94a3b8' : '#e0e0e0')
|
| 450 |
+
.attr('font-size', a.kind === 'orchestrator' ? '16px' : '14px')
|
| 451 |
+
.attr('font-weight', a.kind === 'orchestrator' ? '700' : '600')
|
| 452 |
+
.text(a.label);
|
| 453 |
+
|
| 454 |
+
if (a.kind === 'orchestrator') {
|
| 455 |
+
g.append('text')
|
| 456 |
+
.attr('x', a.x).attr('y', a.y + 22)
|
| 457 |
+
.attr('text-anchor', 'middle')
|
| 458 |
+
.attr('fill', '#888')
|
| 459 |
+
.attr('font-size', '11px')
|
| 460 |
+
.text('координація та гіпотези');
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// Hover
|
| 464 |
+
g.on('mouseenter', (event) => {
|
| 465 |
+
if (!a.tip) return;
|
| 466 |
+
g.select('.agent-box')
|
| 467 |
+
.transition().duration(150)
|
| 468 |
+
.attr('stroke-width', 2.5)
|
| 469 |
+
.attr('filter', 'url(#glow)');
|
| 470 |
+
|
| 471 |
+
tooltip.innerHTML = `
|
| 472 |
+
<div class="arch-tooltip__title" style="color:${color}">${a.tip[0]}</div>
|
| 473 |
+
<div class="arch-tooltip__sub">${a.tip[1]}</div>
|
| 474 |
+
<div class="arch-tooltip__body">${a.tip[2]}</div>
|
| 475 |
+
`;
|
| 476 |
+
tooltip.style.opacity = '1';
|
| 477 |
+
|
| 478 |
+
const rect = root.getBoundingClientRect();
|
| 479 |
+
const svgRect = root.querySelector('svg').getBoundingClientRect();
|
| 480 |
+
const scaleX = svgRect.width / VB_W;
|
| 481 |
+
const px = (a.x + a.w/2) * scaleX + svgRect.left - rect.left + 10;
|
| 482 |
+
const py = (a.y - a.h/2) * (svgRect.height / VB_H) + svgRect.top - rect.top;
|
| 483 |
+
tooltip.style.left = Math.min(px, rect.width - 300) + 'px';
|
| 484 |
+
tooltip.style.top = py + 'px';
|
| 485 |
+
})
|
| 486 |
+
.on('mouseleave', () => {
|
| 487 |
+
g.select('.agent-box')
|
| 488 |
+
.transition().duration(200)
|
| 489 |
+
.attr('stroke-width', a.kind === 'orchestrator' ? 2 : 1.4)
|
| 490 |
+
.attr('filter', null);
|
| 491 |
+
tooltip.style.opacity = '0';
|
| 492 |
+
});
|
| 493 |
+
});
|
| 494 |
+
|
| 495 |
+
// Loop indicator
|
| 496 |
+
const loopG = svg.append('g').attr('class', 'loop-indicator');
|
| 497 |
+
loopG.append('rect')
|
| 498 |
+
.attr('x', 370).attr('y', 165)
|
| 499 |
+
.attr('width', 680).attr('height', 355)
|
| 500 |
+
.attr('rx', 20)
|
| 501 |
+
.attr('fill', 'none')
|
| 502 |
+
.attr('stroke', 'rgba(255,255,255,0.06)')
|
| 503 |
+
.attr('stroke-width', 1.5)
|
| 504 |
+
.attr('stroke-dasharray', '8 6');
|
| 505 |
+
|
| 506 |
+
loopG.append('text')
|
| 507 |
+
.attr('x', 710).attr('y', 185)
|
| 508 |
+
.attr('text-anchor', 'middle')
|
| 509 |
+
.attr('fill', 'rgba(255,255,255,0.2)')
|
| 510 |
+
.attr('font-size', '11px')
|
| 511 |
+
.attr('letter-spacing', '0.1em')
|
| 512 |
+
.text('ДОСЛІДНИЦЬКИЙ ЦИКЛ (до 15 ітерацій)');
|
| 513 |
+
|
| 514 |
+
// Critic connection to loop (periodic)
|
| 515 |
+
loopG.append('text')
|
| 516 |
+
.attr('x', 440).attr('y', 490 + 18)
|
| 517 |
+
.attr('text-anchor', 'middle')
|
| 518 |
+
.attr('fill', 'rgba(255,255,255,0.25)')
|
| 519 |
+
.attr('font-size', '10px')
|
| 520 |
+
.text('кожні 3 ітерації');
|
| 521 |
+
|
| 522 |
+
// CSS animation
|
| 523 |
+
const style = document.createElement('style');
|
| 524 |
+
style.textContent = `
|
| 525 |
+
@keyframes dash-flow {
|
| 526 |
+
to { stroke-dashoffset: -56; }
|
| 527 |
+
}
|
| 528 |
+
`;
|
| 529 |
+
document.head.appendChild(style);
|
| 530 |
+
|
| 531 |
+
// Visibility toggle
|
| 532 |
+
function updateVisibility() {
|
| 533 |
+
svg.selectAll('.edge-main, .edge-glow, .edge-animated').each(function() {
|
| 534 |
+
const flow = this.getAttribute('data-flow');
|
| 535 |
+
d3.select(this).transition().duration(300)
|
| 536 |
+
.attr('opacity', activeFlows.has(flow) ?
|
| 537 |
+
(this.classList.contains('edge-glow') ? 0.15 :
|
| 538 |
+
this.classList.contains('edge-animated') ? 0.9 : 0.7) : 0.05);
|
| 539 |
+
});
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
// Entry animation: fade in agents sequentially
|
| 543 |
+
agentGroup.selectAll('.agent')
|
| 544 |
+
.style('opacity', 0)
|
| 545 |
+
.transition()
|
| 546 |
+
.delay((d, i) => i * 80)
|
| 547 |
+
.duration(400)
|
| 548 |
+
.style('opacity', 1);
|
| 549 |
+
|
| 550 |
+
edgeGroup.selectAll('path')
|
| 551 |
+
.style('opacity', 0)
|
| 552 |
+
.transition()
|
| 553 |
+
.delay(600)
|
| 554 |
+
.duration(800)
|
| 555 |
+
.style('opacity', function() {
|
| 556 |
+
if (this.classList.contains('edge-glow')) return 0.15;
|
| 557 |
+
if (this.classList.contains('edge-animated')) return 0.9;
|
| 558 |
+
return 0.7;
|
| 559 |
+
});
|
| 560 |
+
});
|
| 561 |
+
})();
|
| 562 |
+
</script>
|