selftune 0.2.21 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +1 -0
- package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +59 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +12 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/adapters/cline/hook.ts +167 -0
- package/cli/selftune/adapters/cline/install.ts +197 -0
- package/cli/selftune/adapters/codex/hook.ts +296 -0
- package/cli/selftune/adapters/codex/install.ts +289 -0
- package/cli/selftune/adapters/opencode/hook.ts +222 -0
- package/cli/selftune/adapters/opencode/install.ts +543 -0
- package/cli/selftune/adapters/pi/hook.ts +273 -0
- package/cli/selftune/adapters/pi/install.ts +207 -0
- package/cli/selftune/constants.ts +10 -1
- package/cli/selftune/dashboard-contract.ts +14 -0
- package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
- package/cli/selftune/evolution/engines/replay-engine.ts +158 -0
- package/cli/selftune/evolution/evidence.ts +2 -6
- package/cli/selftune/evolution/evolve-body.ts +73 -20
- package/cli/selftune/evolution/validate-body.ts +78 -42
- package/cli/selftune/evolution/validate-routing.ts +45 -104
- package/cli/selftune/hooks/auto-activate.ts +43 -37
- package/cli/selftune/hooks/skill-eval.ts +2 -1
- package/cli/selftune/hooks-shared/git-metadata.ts +149 -0
- package/cli/selftune/hooks-shared/hook-output.ts +105 -0
- package/cli/selftune/hooks-shared/normalize.ts +196 -0
- package/cli/selftune/hooks-shared/session-state.ts +76 -0
- package/cli/selftune/hooks-shared/skill-paths.ts +50 -0
- package/cli/selftune/hooks-shared/stdin-dispatch.ts +59 -0
- package/cli/selftune/hooks-shared/types.ts +91 -0
- package/cli/selftune/index.ts +76 -6
- package/cli/selftune/ingestors/pi-ingest.ts +726 -0
- package/cli/selftune/init.ts +11 -1
- package/cli/selftune/localdb/direct-write.ts +85 -0
- package/cli/selftune/localdb/materialize.ts +6 -7
- package/cli/selftune/localdb/queries.ts +126 -0
- package/cli/selftune/localdb/schema.ts +38 -0
- package/cli/selftune/observability.ts +8 -1
- package/cli/selftune/orchestrate.ts +43 -0
- package/cli/selftune/registry/client.ts +74 -0
- package/cli/selftune/registry/history.ts +54 -0
- package/cli/selftune/registry/index.ts +90 -0
- package/cli/selftune/registry/install.ts +141 -0
- package/cli/selftune/registry/list.ts +44 -0
- package/cli/selftune/registry/push.ts +171 -0
- package/cli/selftune/registry/rollback.ts +49 -0
- package/cli/selftune/registry/status.ts +62 -0
- package/cli/selftune/registry/sync.ts +125 -0
- package/cli/selftune/repair/skill-usage.ts +4 -1
- package/cli/selftune/status.ts +31 -0
- package/cli/selftune/sync.ts +127 -23
- package/cli/selftune/types.ts +2 -1
- package/cli/selftune/utils/jsonl.ts +1 -30
- package/cli/selftune/utils/llm-call.ts +99 -34
- package/cli/selftune/utils/skill-discovery.ts +22 -0
- package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/package.json +1 -1
- package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
- package/node_modules/@selftune/telemetry-contract/src/schemas.ts +22 -4
- package/node_modules/@selftune/telemetry-contract/src/types.ts +1 -12
- package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/package.json +1 -1
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/packages/telemetry-contract/package.json +1 -1
- package/packages/telemetry-contract/src/index.ts +1 -0
- package/packages/telemetry-contract/src/schemas.ts +22 -4
- package/packages/telemetry-contract/src/types.ts +1 -12
- package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/packages/ui/AGENTS.md +16 -0
- package/packages/ui/README.md +1 -1
- package/packages/ui/package.json +1 -1
- package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
- package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
- package/packages/ui/src/components/EvidenceViewer.tsx +153 -443
- package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
- package/packages/ui/src/components/InfoTip.tsx +1 -2
- package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
- package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
- package/packages/ui/src/components/OverviewPanels.tsx +652 -0
- package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
- package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
- package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
- package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
- package/packages/ui/src/components/index.ts +56 -1
- package/packages/ui/src/components/section-cards.tsx +18 -35
- package/packages/ui/src/components/skill-health-grid.tsx +47 -37
- package/packages/ui/src/lib/constants.tsx +0 -1
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/packages/ui/src/primitives/checkbox.tsx +1 -1
- package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
- package/packages/ui/src/primitives/select.tsx +2 -2
- package/packages/ui/src/types.ts +172 -4
- package/skill/SKILL.md +26 -2
- package/skill/Workflows/Ingest.md +60 -2
- package/skill/Workflows/Initialize.md +54 -9
- package/skill/Workflows/PlatformHooks.md +109 -0
- package/skill/Workflows/Registry.md +99 -0
- package/skill/Workflows/Sync.md +3 -1
- package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
- package/cli/selftune/utils/html.ts +0 -27
- package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
/* ── Types ──────────────────────────────────────────────── */
|
|
6
|
+
|
|
7
|
+
export interface PassRateTrendPoint {
|
|
8
|
+
date: string;
|
|
9
|
+
pass_rate: number;
|
|
10
|
+
total_checks: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SkillRanking {
|
|
14
|
+
skill_name: string;
|
|
15
|
+
pass_rate: number;
|
|
16
|
+
total_checks: number;
|
|
17
|
+
triggered_count: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DailyActivity {
|
|
21
|
+
date: string;
|
|
22
|
+
checks: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EvolutionImpact {
|
|
26
|
+
skill_name: string;
|
|
27
|
+
proposal_id: string;
|
|
28
|
+
deployed_at: string;
|
|
29
|
+
pass_rate_before: number;
|
|
30
|
+
pass_rate_after: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AnalyticsSummary {
|
|
34
|
+
total_evolutions: number;
|
|
35
|
+
avg_improvement: number;
|
|
36
|
+
total_checks_30d: number;
|
|
37
|
+
active_skills: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AnalyticsResponse {
|
|
41
|
+
pass_rate_trend: PassRateTrendPoint[];
|
|
42
|
+
skill_rankings: SkillRanking[];
|
|
43
|
+
daily_activity: DailyActivity[];
|
|
44
|
+
evolution_impact: EvolutionImpact[];
|
|
45
|
+
summary: AnalyticsSummary;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ── Helpers ────────────────────────────────────────────── */
|
|
49
|
+
|
|
50
|
+
function formatDayBucketLabel(day: string): string {
|
|
51
|
+
const parts = day.split("-");
|
|
52
|
+
const month = parts[1];
|
|
53
|
+
const date = parts[2];
|
|
54
|
+
if (!month || !date) return day;
|
|
55
|
+
return `${Number(month)}/${Number(date)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ── SVG Line Chart ─────────────────────────────────────── */
|
|
59
|
+
|
|
60
|
+
export function PassRateTrendChart({
|
|
61
|
+
data,
|
|
62
|
+
mode,
|
|
63
|
+
}: {
|
|
64
|
+
data: PassRateTrendPoint[];
|
|
65
|
+
mode: "pass_rate" | "volume";
|
|
66
|
+
}) {
|
|
67
|
+
const width = 720;
|
|
68
|
+
const height = 260;
|
|
69
|
+
const padX = 48;
|
|
70
|
+
const padY = 32;
|
|
71
|
+
const padBottom = 28;
|
|
72
|
+
|
|
73
|
+
const values = data.map((d) => (mode === "pass_rate" ? d.pass_rate * 100 : d.total_checks));
|
|
74
|
+
const maxVal = Math.max(...values, mode === "pass_rate" ? 100 : 1);
|
|
75
|
+
const minVal = 0;
|
|
76
|
+
|
|
77
|
+
const chartW = width - padX * 2;
|
|
78
|
+
const chartH = height - padY - padBottom;
|
|
79
|
+
|
|
80
|
+
const points = values.map((v, i) => {
|
|
81
|
+
const x = padX + (i / Math.max(1, values.length - 1)) * chartW;
|
|
82
|
+
const y = padY + chartH - ((v - minVal) / Math.max(1, maxVal - minVal)) * chartH;
|
|
83
|
+
return { x, y };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const pathD = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" ");
|
|
87
|
+
const areaD = `${pathD} L${points[points.length - 1]?.x ?? padX},${padY + chartH} L${padX},${padY + chartH} Z`;
|
|
88
|
+
|
|
89
|
+
const yTicks =
|
|
90
|
+
mode === "pass_rate"
|
|
91
|
+
? [0, 25, 50, 75, 100]
|
|
92
|
+
: Array.from({ length: 5 }, (_, i) => Math.round((maxVal / 4) * i));
|
|
93
|
+
|
|
94
|
+
const xLabels: Array<{ label: string; x: number }> = [];
|
|
95
|
+
const step = Math.max(1, Math.floor(data.length / 6));
|
|
96
|
+
for (let i = 0; i < data.length; i += step) {
|
|
97
|
+
const d = data[i];
|
|
98
|
+
const pt = points[i];
|
|
99
|
+
if (d && pt) {
|
|
100
|
+
xLabels.push({ label: formatDayBucketLabel(d.date), x: pt.x });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (data.length === 0) {
|
|
105
|
+
return (
|
|
106
|
+
<div className="flex items-center justify-center h-[260px] text-muted-foreground text-sm">
|
|
107
|
+
No trend data available yet
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<svg
|
|
114
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
115
|
+
className="w-full h-auto"
|
|
116
|
+
preserveAspectRatio="xMidYMid meet"
|
|
117
|
+
>
|
|
118
|
+
<defs>
|
|
119
|
+
<linearGradient id="analytics-chart-fill" x1="0" y1="0" x2="0" y2="1">
|
|
120
|
+
<stop offset="0%" stopColor="var(--primary)" stopOpacity="0.3" />
|
|
121
|
+
<stop offset="100%" stopColor="var(--primary)" stopOpacity="0.02" />
|
|
122
|
+
</linearGradient>
|
|
123
|
+
</defs>
|
|
124
|
+
|
|
125
|
+
{yTicks.map((tick) => {
|
|
126
|
+
const y = padY + chartH - ((tick - minVal) / Math.max(1, maxVal - minVal)) * chartH;
|
|
127
|
+
return (
|
|
128
|
+
<g key={tick}>
|
|
129
|
+
<line
|
|
130
|
+
x1={padX}
|
|
131
|
+
y1={y}
|
|
132
|
+
x2={width - padX}
|
|
133
|
+
y2={y}
|
|
134
|
+
stroke="var(--border)"
|
|
135
|
+
strokeWidth="0.5"
|
|
136
|
+
strokeDasharray="4 4"
|
|
137
|
+
/>
|
|
138
|
+
<text
|
|
139
|
+
x={padX - 8}
|
|
140
|
+
y={y + 3}
|
|
141
|
+
textAnchor="end"
|
|
142
|
+
fill="var(--muted-foreground)"
|
|
143
|
+
fontSize="9"
|
|
144
|
+
fontFamily="var(--font-headline)"
|
|
145
|
+
>
|
|
146
|
+
{mode === "pass_rate" ? `${tick}%` : tick}
|
|
147
|
+
</text>
|
|
148
|
+
</g>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
|
|
152
|
+
{xLabels.map((label) => (
|
|
153
|
+
<text
|
|
154
|
+
key={label.label}
|
|
155
|
+
x={label.x}
|
|
156
|
+
y={height - 4}
|
|
157
|
+
textAnchor="middle"
|
|
158
|
+
fill="var(--muted-foreground)"
|
|
159
|
+
fontSize="9"
|
|
160
|
+
fontFamily="var(--font-headline)"
|
|
161
|
+
>
|
|
162
|
+
{label.label}
|
|
163
|
+
</text>
|
|
164
|
+
))}
|
|
165
|
+
|
|
166
|
+
{points.length > 1 && <path d={areaD} fill="url(#analytics-chart-fill)" />}
|
|
167
|
+
|
|
168
|
+
{points.length > 1 && (
|
|
169
|
+
<path
|
|
170
|
+
d={pathD}
|
|
171
|
+
fill="none"
|
|
172
|
+
stroke="var(--primary)"
|
|
173
|
+
strokeWidth="2"
|
|
174
|
+
strokeLinecap="round"
|
|
175
|
+
strokeLinejoin="round"
|
|
176
|
+
style={{ filter: "drop-shadow(0 0 4px rgba(79,242,255,0.5))" }}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{points.map((p, i) => (
|
|
181
|
+
<circle
|
|
182
|
+
key={i}
|
|
183
|
+
cx={p.x}
|
|
184
|
+
cy={p.y}
|
|
185
|
+
r="3"
|
|
186
|
+
fill="var(--primary)"
|
|
187
|
+
stroke="var(--muted)"
|
|
188
|
+
strokeWidth="1.5"
|
|
189
|
+
/>
|
|
190
|
+
))}
|
|
191
|
+
</svg>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ── Skill Rankings List ────────────────────────────────── */
|
|
196
|
+
|
|
197
|
+
export function SkillRankingsList({ skills }: { skills: SkillRanking[] }) {
|
|
198
|
+
if (skills.length === 0) {
|
|
199
|
+
return (
|
|
200
|
+
<div className="flex-1 flex items-center justify-center">
|
|
201
|
+
<p className="text-sm text-muted-foreground">No skills graded yet</p>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className="flex-1 flex flex-col gap-4">
|
|
208
|
+
{skills.map((skill) => (
|
|
209
|
+
<div key={skill.skill_name}>
|
|
210
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
211
|
+
<span className="font-headline text-[11px] uppercase tracking-wider text-foreground truncate max-w-[65%]">
|
|
212
|
+
{skill.skill_name}
|
|
213
|
+
</span>
|
|
214
|
+
<span className="font-headline text-xs font-semibold text-primary">
|
|
215
|
+
{Math.round(skill.pass_rate * 100)}%
|
|
216
|
+
</span>
|
|
217
|
+
</div>
|
|
218
|
+
<div className="h-[1.5px] rounded-full bg-border/30 overflow-hidden">
|
|
219
|
+
<div
|
|
220
|
+
className="h-full rounded-full bg-primary transition-all duration-500"
|
|
221
|
+
style={{
|
|
222
|
+
width: `${Math.round(skill.pass_rate * 100)}%`,
|
|
223
|
+
boxShadow: "0 0 6px rgba(79,242,255,0.4)",
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
<p className="text-[10px] text-muted-foreground mt-1">
|
|
228
|
+
{skill.total_checks} checks · {skill.triggered_count} triggered
|
|
229
|
+
</p>
|
|
230
|
+
</div>
|
|
231
|
+
))}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ── Activity Heatmap ───────────────────────────────────── */
|
|
237
|
+
|
|
238
|
+
export function ActivityHeatmap({ data }: { data: DailyActivity[] }) {
|
|
239
|
+
const cells = data.slice(-84);
|
|
240
|
+
const maxChecks = Math.max(...cells.map((d) => d.checks), 1);
|
|
241
|
+
|
|
242
|
+
if (cells.length === 0) {
|
|
243
|
+
return (
|
|
244
|
+
<div className="flex items-center justify-center h-32 text-muted-foreground text-sm">
|
|
245
|
+
No grading activity recorded yet
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div className="flex flex-col h-full">
|
|
252
|
+
<div className="flex flex-wrap gap-1.5 flex-1 content-start">
|
|
253
|
+
{cells.map((day) => {
|
|
254
|
+
const intensity = day.checks / maxChecks;
|
|
255
|
+
const opacity = Math.max(0.08, intensity);
|
|
256
|
+
return (
|
|
257
|
+
<div
|
|
258
|
+
key={day.date}
|
|
259
|
+
className="size-5 rounded-sm transition-colors"
|
|
260
|
+
style={{
|
|
261
|
+
backgroundColor: `color-mix(in srgb, var(--primary) ${Math.round(opacity * 100)}%, transparent)`,
|
|
262
|
+
}}
|
|
263
|
+
title={`${day.date}: ${day.checks} checks`}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
266
|
+
})}
|
|
267
|
+
</div>
|
|
268
|
+
<div className="flex items-center justify-end gap-2 mt-auto pt-3">
|
|
269
|
+
<span className="text-[10px] font-headline uppercase tracking-widest text-muted-foreground">
|
|
270
|
+
Quiet
|
|
271
|
+
</span>
|
|
272
|
+
{[8, 25, 50, 75, 100].map((pct) => (
|
|
273
|
+
<div
|
|
274
|
+
key={pct}
|
|
275
|
+
className="size-3 rounded-sm"
|
|
276
|
+
style={{
|
|
277
|
+
backgroundColor: `color-mix(in srgb, var(--primary) ${pct}%, transparent)`,
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
))}
|
|
281
|
+
<span className="text-[10px] font-headline uppercase tracking-widest text-muted-foreground">
|
|
282
|
+
Active
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ── Evolution ROI List ─────────────────────────────────── */
|
|
290
|
+
|
|
291
|
+
export function EvolutionROIList({ impacts }: { impacts: EvolutionImpact[] }) {
|
|
292
|
+
if (impacts.length === 0) {
|
|
293
|
+
return (
|
|
294
|
+
<div className="flex items-center justify-center h-32">
|
|
295
|
+
<p className="text-sm text-muted-foreground">No evolution deployments yet</p>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div className="flex flex-col gap-3 max-h-[260px] overflow-y-auto">
|
|
302
|
+
{impacts.map((evo) => {
|
|
303
|
+
const delta = (evo.pass_rate_after - evo.pass_rate_before) * 100;
|
|
304
|
+
const improved = delta > 0;
|
|
305
|
+
return (
|
|
306
|
+
<div
|
|
307
|
+
key={evo.proposal_id}
|
|
308
|
+
className="flex items-center justify-between bg-muted/50 rounded-lg px-4 py-3"
|
|
309
|
+
>
|
|
310
|
+
<div className="min-w-0">
|
|
311
|
+
<p className="font-headline text-[11px] uppercase tracking-wider text-foreground truncate">
|
|
312
|
+
{evo.skill_name}
|
|
313
|
+
</p>
|
|
314
|
+
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
315
|
+
{Math.round(evo.pass_rate_before * 100)}% →{" "}
|
|
316
|
+
{Math.round(evo.pass_rate_after * 100)}%
|
|
317
|
+
</p>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
320
|
+
<svg
|
|
321
|
+
className={`size-3.5 ${improved ? "text-primary" : "text-destructive rotate-90"}`}
|
|
322
|
+
viewBox="0 0 24 24"
|
|
323
|
+
fill="none"
|
|
324
|
+
stroke="currentColor"
|
|
325
|
+
strokeWidth="2"
|
|
326
|
+
strokeLinecap="round"
|
|
327
|
+
strokeLinejoin="round"
|
|
328
|
+
>
|
|
329
|
+
<line x1="7" y1="17" x2="17" y2="7" />
|
|
330
|
+
<polyline points="7 7 17 7 17 17" />
|
|
331
|
+
</svg>
|
|
332
|
+
<span
|
|
333
|
+
className={`font-headline text-sm font-semibold ${improved ? "text-primary" : "text-destructive"}`}
|
|
334
|
+
>
|
|
335
|
+
{improved ? "+" : ""}
|
|
336
|
+
{Math.round(delta)}%
|
|
337
|
+
</span>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
})}
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|