[eric] splash for start up ux improvements

This commit is contained in:
ciregenz
2026-04-25 03:46:59 -07:00
parent 94ea07dd0c
commit c67d27976b
3 changed files with 355 additions and 36 deletions
+26 -5
View File
@@ -166,6 +166,30 @@ function emitSplashStatus(payload) {
}
}
// OS-tailored status copy. The "first launch is slow" experience has very
// different causes per platform (Defender on Windows, Gatekeeper +
// XProtect notarization scan on macOS), and naming the actual culprit
// helps users feel like the wait is intentional rather than the app being
// broken. Used by the long-wait branches in waitForBackend below.
function osStillStartingText() {
if (process.platform === 'win32') {
return 'Still starting — Windows Defender is scanning files (first launch only)…';
}
if (process.platform === 'darwin') {
return 'Still starting — macOS is verifying the bundle (first launch only)…';
}
return 'Still starting (first launch is slower than subsequent launches)…';
}
function osTakingTooLongText() {
if (process.platform === 'win32') {
return 'Backend is taking longer than usual. Defender scans of 14k files can take a few minutes on slow drives.';
}
if (process.platform === 'darwin') {
return 'Backend is taking longer than usual. macOS first-launch checks can be slow on cold cache.';
}
return 'Backend is taking longer than usual. You can wait, view logs, or restart.';
}
/**
* macOS GUI apps launched from Finder/Dock inherit a minimal PATH from launchd
* (/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin) — none of the user's shell
@@ -294,15 +318,12 @@ function waitForBackend(port, opts = {}) {
const elapsed = Date.now() - start;
if (elapsed > 60_000 && !stillStartingNotified) {
stillStartingNotified = true;
emitSplashStatus({
text: 'Still starting (first launch can take 2-3 minutes on Windows while Defender scans)…',
level: 'warning',
});
emitSplashStatus({ text: osStillStartingText(), level: 'warning' });
}
if (elapsed > 180_000 && !actionsShown) {
actionsShown = true;
emitSplashStatus({
text: 'Backend is taking unusually long. You can wait, view logs, or restart.',
text: osTakingTooLongText(),
level: 'warning',
showActions: true,
logs: recentBackendStderr.slice(-20).join(''),
+102
View File
@@ -0,0 +1,102 @@
// Standalone splash preview — opens the splash window in isolation and
// cycles through every status state so you can eyeball the UI without
// running the full backend + frontend + electron stack.
//
// Run from the electron/ dir:
// npx electron splash/preview.js
//
// Cycle (~26s total, then idles on the error state with action buttons):
// 0s "Starting OpenSwarm…"
// 3s "Connecting to dev backend…"
// 6s "Loading components…"
// 9s "Almost ready…"
// 12s "Still starting (warning, no actions)"
// 18s "Backend taking too long" (with View Logs / Restart / Quit buttons)
// 24s error state: "OpenSwarm couldn't start" + last lines of stderr
const { app, BrowserWindow, ipcMain, shell } = require('electron');
const fs = require('fs');
const path = require('path');
let splashWindow = null;
function loadSplashDataUrl() {
const html = fs.readFileSync(path.join(__dirname, 'splash.html'), 'utf8');
const iconPath = path.join(__dirname, '..', 'build', 'icon.png');
const iconBytes = fs.readFileSync(iconPath);
const iconDataUrl = 'data:image/png;base64,' + iconBytes.toString('base64');
const finalHtml = html.replace('__OPENSWARM_LOGO__', iconDataUrl);
return 'data:text/html;charset=utf-8;base64,' + Buffer.from(finalHtml).toString('base64');
}
function emit(payload) {
if (splashWindow && !splashWindow.isDestroyed()) {
splashWindow.webContents.send('splash:status', payload);
const txt = typeof payload === 'string' ? payload : payload.text;
console.log(`[preview] -> ${txt}`);
}
}
ipcMain.on('splash:action', (_e, action) => {
console.log(`[preview] splash button clicked: ${action}`);
if (action === 'quit') app.quit();
else if (action === 'restart') { app.relaunch(); app.exit(0); }
else if (action === 'open-logs') console.log('[preview] (would open log dir)');
});
app.whenReady().then(() => {
splashWindow = new BrowserWindow({
width: 460,
height: 340,
frame: false,
resizable: false,
skipTaskbar: false, // keep visible in taskbar for the preview
show: true,
center: true,
backgroundColor: '#0a0a10',
title: 'OpenSwarm splash preview',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
sandbox: false,
backgroundThrottling: false,
},
});
splashWindow.setMenuBarVisibility(false);
splashWindow.loadURL(loadSplashDataUrl());
// Cycle through every state with delays so you can see each one.
splashWindow.webContents.once('did-finish-load', () => {
setTimeout(() => emit('Starting OpenSwarm…'), 0);
setTimeout(() => emit('Connecting to dev backend…'), 3000);
setTimeout(() => emit('Loading components…'), 6000);
setTimeout(() => emit('Almost ready…'), 9000);
// Mirror the OS-tailored copy from main.js so the preview shows what
// real users would see on this platform.
const stillStarting = process.platform === 'win32'
? 'Still starting — Windows Defender is scanning files (first launch only)…'
: process.platform === 'darwin'
? 'Still starting — macOS is verifying the bundle (first launch only)…'
: 'Still starting (first launch is slower than subsequent launches)…';
const takingTooLong = process.platform === 'win32'
? 'Backend is taking longer than usual. Defender scans of 14k files can take a few minutes on slow drives.'
: process.platform === 'darwin'
? 'Backend is taking longer than usual. macOS first-launch checks can be slow on cold cache.'
: 'Backend is taking longer than usual. You can wait, view logs, or restart.';
setTimeout(() => emit({ text: stillStarting, level: 'warning' }), 12000);
setTimeout(() => emit({
text: takingTooLong,
level: 'warning',
showActions: true,
logs: '[backend] uvicorn warming up...\n[backend] importing claude_agent_sdk...\n[backend] (still working)',
}), 18000);
setTimeout(() => emit({
text: "OpenSwarm couldn't start: Backend process exited with code 1 during startup",
level: 'error',
showActions: true,
logs: '[backend] Traceback (most recent call last):\n[backend] File "backend/main.py", line 48\n[backend] ImportError: simulated failure for splash preview',
}), 24000);
});
splashWindow.on('closed', () => { app.quit(); });
});
+227 -31
View File
@@ -36,13 +36,23 @@
padding: 0 24px;
max-width: 100%;
}
/* Logo is rendered inside the WebGL canvas now (sampled as a texture
so the dither pattern can flow through it). The <img> stays in the
DOM purely as the WebGL-failure fallback; CSS hides it once the
shader has uploaded the texture, and reveals it if WebGL init fails. */
.logo {
width: 84px; height: 84px;
image-rendering: auto;
image-rendering: pixelated;
image-rendering: crisp-edges;
filter: drop-shadow(0 0 18px rgba(160, 100, 240, 0.55));
opacity: 0;
animation: fadeIn 700ms ease forwards 60ms;
}
/* `visibility: hidden` (not display:none) so the layout slot is preserved
— the shader queries .logo.getBoundingClientRect() to know where to
render the dithered octopus, and we want that slot to keep flexbox
centering the brand text + status text below it. */
.logo.shader-active { visibility: hidden; }
.name {
font-size: 14px; font-weight: 600; letter-spacing: 0.32em;
text-transform: uppercase;
@@ -152,25 +162,157 @@
'void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }'
].join('\n');
// Animated layered fluid (sin/cos blend) → bayer-dithered into 2 colors.
// Port of the reactbits Dither shader (https://reactbits.dev/backgrounds/dither)
// — same Perlin noise + fbm + 8×8 Bayer + N-color quantization. Difference
// from upstream: we render the wave AND the octopus icon overlay in a
// single fragment program (one draw call), and the per-pixel "downsample
// to fat pixels then dither" trick is folded into the same pass instead of
// being a postprocessing EffectComposer chain. Costs the same per pixel
// as upstream, no postprocess RTT.
//
// Octopus overlay: pixel-art icon sampled at NEAREST (so the dither
// doesn't smear the source pixels into a blur first). Its body color
// gets quantized through the same dither so it visually belongs in
// the same stippled world as the background.
const fsSrc = [
'precision highp float;',
'uniform vec2 u_res;',
'uniform float u_time;',
'uniform sampler2D u_bayer;',
'uniform float u_dpr;',
'uniform sampler2D u_icon;',
'uniform vec2 u_icon_center_px;',
'uniform float u_icon_size_px;',
'uniform vec3 u_wave_color;',
'uniform float u_wave_speed;',
'uniform float u_wave_freq;',
'uniform float u_wave_amp;',
'uniform float u_color_num;',
'uniform float u_pixel_size;',
'',
// ----- Classic Perlin noise (Stefan Gustavson) -----
'vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}',
'vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}',
'vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}',
'vec2 fade(vec2 t){return t*t*t*(t*(t*6.0-15.0)+10.0);}',
'float cnoise(vec2 P){',
' vec4 Pi=floor(P.xyxy)+vec4(0.0,0.0,1.0,1.0);',
' vec4 Pf=fract(P.xyxy)-vec4(0.0,0.0,1.0,1.0);',
' Pi=mod289(Pi);',
' vec4 ix=Pi.xzxz; vec4 iy=Pi.yyww;',
' vec4 fx=Pf.xzxz; vec4 fy=Pf.yyww;',
' vec4 i=permute(permute(ix)+iy);',
' vec4 gx=fract(i*(1.0/41.0))*2.0-1.0;',
' vec4 gy=abs(gx)-0.5;',
' vec4 tx=floor(gx+0.5); gx=gx-tx;',
' vec2 g00=vec2(gx.x,gy.x); vec2 g10=vec2(gx.y,gy.y);',
' vec2 g01=vec2(gx.z,gy.z); vec2 g11=vec2(gx.w,gy.w);',
' vec4 norm=taylorInvSqrt(vec4(dot(g00,g00),dot(g01,g01),dot(g10,g10),dot(g11,g11)));',
' g00*=norm.x; g01*=norm.y; g10*=norm.z; g11*=norm.w;',
' float n00=dot(g00,vec2(fx.x,fy.x));',
' float n10=dot(g10,vec2(fx.y,fy.y));',
' float n01=dot(g01,vec2(fx.z,fy.z));',
' float n11=dot(g11,vec2(fx.w,fy.w));',
' vec2 fade_xy=fade(Pf.xy);',
' vec2 n_x=mix(vec2(n00,n01),vec2(n10,n11),fade_xy.x);',
' return 2.3*mix(n_x.x,n_x.y,fade_xy.y);',
'}',
'',
// ----- 4-octave fractal-Brownian-motion of |cnoise| -----
'float fbm(vec2 p){',
' float v=0.0; float amp=1.0; float freq=u_wave_freq;',
' for(int i=0;i<4;i++){',
' v+=amp*abs(cnoise(p));',
' p*=freq;',
' amp*=u_wave_amp;',
' }',
' return v;',
'}',
'float pattern(vec2 p){',
' vec2 p2=p-u_time*u_wave_speed;',
' return fbm(p+fbm(p2));',
'}',
'',
// ----- 8×8 Bayer threshold (computed inline — no texture lookup) -----
'float bayer8(vec2 c){',
' int x=int(mod(c.x,8.0));',
' int y=int(mod(c.y,8.0));',
// Inline lookup: avoids the WebGL1 dynamic-array-indexing footgun that
// killed earlier tries. 64 if-statements compile to a fast jump.
' if(y==0){if(x==0)return 0.0/64.0;if(x==1)return 48.0/64.0;if(x==2)return 12.0/64.0;if(x==3)return 60.0/64.0;if(x==4)return 3.0/64.0;if(x==5)return 51.0/64.0;if(x==6)return 15.0/64.0;return 63.0/64.0;}',
' if(y==1){if(x==0)return 32.0/64.0;if(x==1)return 16.0/64.0;if(x==2)return 44.0/64.0;if(x==3)return 28.0/64.0;if(x==4)return 35.0/64.0;if(x==5)return 19.0/64.0;if(x==6)return 47.0/64.0;return 31.0/64.0;}',
' if(y==2){if(x==0)return 8.0/64.0;if(x==1)return 56.0/64.0;if(x==2)return 4.0/64.0;if(x==3)return 52.0/64.0;if(x==4)return 11.0/64.0;if(x==5)return 59.0/64.0;if(x==6)return 7.0/64.0;return 55.0/64.0;}',
' if(y==3){if(x==0)return 40.0/64.0;if(x==1)return 24.0/64.0;if(x==2)return 36.0/64.0;if(x==3)return 20.0/64.0;if(x==4)return 43.0/64.0;if(x==5)return 27.0/64.0;if(x==6)return 39.0/64.0;return 23.0/64.0;}',
' if(y==4){if(x==0)return 2.0/64.0;if(x==1)return 50.0/64.0;if(x==2)return 14.0/64.0;if(x==3)return 62.0/64.0;if(x==4)return 1.0/64.0;if(x==5)return 49.0/64.0;if(x==6)return 13.0/64.0;return 61.0/64.0;}',
' if(y==5){if(x==0)return 34.0/64.0;if(x==1)return 18.0/64.0;if(x==2)return 46.0/64.0;if(x==3)return 30.0/64.0;if(x==4)return 33.0/64.0;if(x==5)return 17.0/64.0;if(x==6)return 45.0/64.0;return 29.0/64.0;}',
' if(y==6){if(x==0)return 10.0/64.0;if(x==1)return 58.0/64.0;if(x==2)return 6.0/64.0;if(x==3)return 54.0/64.0;if(x==4)return 9.0/64.0;if(x==5)return 57.0/64.0;if(x==6)return 5.0/64.0;return 53.0/64.0;}',
' if(x==0)return 42.0/64.0;if(x==1)return 26.0/64.0;if(x==2)return 38.0/64.0;if(x==3)return 22.0/64.0;if(x==4)return 41.0/64.0;if(x==5)return 25.0/64.0;if(x==6)return 37.0/64.0;return 21.0/64.0;',
'}',
'',
// ----- N-color ordered dither, exactly matching reactbits -----
'vec3 dither(vec2 pixCoord, vec3 color){',
' float threshold = bayer8(pixCoord) - 0.25;',
' float stepSize = 1.0 / (u_color_num - 1.0);',
' color += threshold * stepSize;',
' color = clamp(color - 0.2, 0.0, 1.0);',
' return floor(color*(u_color_num - 1.0) + 0.5) / (u_color_num - 1.0);',
'}',
'',
'void main() {',
' vec2 uv = gl_FragCoord.xy / u_res;',
' float t = u_time;',
' float a = sin(uv.x * 5.2 + t * 0.42) * cos(uv.y * 3.8 + t * 0.29);',
' float b = sin((uv.x + uv.y) * 4.6 + t * 0.58) * 0.55;',
' float c = cos(uv.x * 7.1 - uv.y * 5.3 + t * 0.21) * 0.35;',
' float fluid = (a + b + c) * 0.32 + 0.5;',
' vec2 bUv = fract(gl_FragCoord.xy / 4.0);',
' float thresh = texture2D(u_bayer, bUv).r;',
' float lit = step(thresh, fluid);',
' vec3 dark = vec3(0.038, 0.038, 0.062);', // ~#0a0a10
' vec3 lite = vec3(0.522, 0.286, 0.878);', // ~#854ae0 (electric purple)
' gl_FragColor = vec4(mix(dark, lite, lit), 1.0);',
' vec2 fc = gl_FragCoord.xy;',
// Snap to fat pixel grid (matches reactbits pixelSize behavior)
' vec2 snapped = floor(fc / u_pixel_size) * u_pixel_size;',
// ----- Background: cnoise/fbm pattern in aspect-corrected coords -----
' vec2 uv = snapped / u_res - 0.5;',
' uv.x *= u_res.x / u_res.y;',
' float f = pattern(uv);',
' vec3 col = mix(vec3(0.0), u_wave_color, f);',
'',
// ----- Foreground: octopus icon, integrated into the dithered world -----
// Goal: the octopus shouldn't look "stuck on top of" the background. It
// should feel like part of the same dithered terrain — same color
// grammar, same quantization. We achieve that by:
// (a) Letting some of the bg pattern bleed INTO the octopus body
// (multiply icon brightness by a subtle pattern factor).
// (b) Surrounding the icon with a soft glow that's also dithered
// (the glow shares the same palette as the icon body).
// (c) Boosting the icon's contrast slightly so it remains the focal
// point even with the integration.
// Eyes (very dark pixels) stay solid via a luminance early-out below.
' vec2 logical = fc / u_dpr;',
' logical.y = (u_res.y / u_dpr) - logical.y;',
' vec2 iconRel = (logical - u_icon_center_px) / u_icon_size_px + 0.5;',
// Distance-from-icon-center, 0 at center, 1 at edge of icon bbox, >1 outside
' vec2 iconLocal = (logical - u_icon_center_px) / (u_icon_size_px * 0.5);',
' float iconDist = length(iconLocal);',
// Soft glow ring around the icon — bleeds the icon color into bg waves
' float glow = exp(-iconDist * iconDist * 1.8);',
' vec3 glowColor = vec3(1.00, 0.45, 0.32);', // coral matches octopus body
' col = mix(col, glowColor, glow * 0.18);',
' if (iconRel.x >= 0.0 && iconRel.x <= 1.0 && iconRel.y >= 0.0 && iconRel.y <= 1.0) {',
' vec4 iconTex = texture2D(u_icon, iconRel);',
' if (iconTex.a > 0.05) {',
' float lum = dot(iconTex.rgb, vec3(0.30, 0.59, 0.11));',
' vec3 iconCol = iconTex.rgb;',
// Boost coral saturation slightly so the dither doesn\'t mute it.
' iconCol = mix(vec3(lum), iconCol, 1.15);',
// Bleed bg pattern into icon body (only on bright pixels, not eyes):
// multiply the body color by a subtle pattern factor so it shimmers
// with the same noise that drives the bg. Eyes (lum<0.2) skip this.
' if (lum > 0.20) {',
' float bleed = 0.85 + 0.30 * f;',
' iconCol *= bleed;',
' }',
' col = mix(col, iconCol, iconTex.a);',
' }',
' }',
'',
// ----- Final ordered-dither over the composited frame -----
// Apply dither to the WHOLE image (bg + icon together) so the icon and
// background share the same color quantization. Matches reactbits.
' vec2 ditherCoord = floor(fc / u_pixel_size);',
' col = dither(ditherCoord, col);',
'',
' gl_FragColor = vec4(col, 1.0);',
'}'
].join('\n');
@@ -202,31 +344,85 @@
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
// 4×4 Bayer ordered-dither matrix as a luminance texture.
const bayer = new Uint8Array([
0, 8, 2, 10,
12, 4, 14, 6,
3, 11, 1, 9,
15, 7, 13, 5
].map(function (v) { return Math.floor(v * 255 / 16); }));
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, 4, 4, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, bayer);
// ------ Octopus icon texture (only texture we use; bayer lives inline in shader) ------
const iconTex = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, iconTex);
// 1-px transparent stub so the shader doesn't sample undefined memory
// before the PNG finishes decoding.
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 0, 0]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const uRes = gl.getUniformLocation(prog, 'u_res');
const uTime = gl.getUniformLocation(prog, 'u_time');
const uTex = gl.getUniformLocation(prog, 'u_bayer');
gl.uniform1i(uTex, 0);
var iconLoaded = false;
var logoEl = document.querySelector('.logo');
function uploadIcon() {
if (iconLoaded || !logoEl || !logoEl.naturalWidth) return;
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, iconTex);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, logoEl);
iconLoaded = true;
// Hide the DOM <img> now that the texture is on the GPU.
logoEl.classList.add('shader-active');
}
if (logoEl) {
if (logoEl.complete && logoEl.naturalWidth > 0) uploadIcon();
else logoEl.addEventListener('load', uploadIcon, { once: true });
}
const uRes = gl.getUniformLocation(prog, 'u_res');
const uTime = gl.getUniformLocation(prog, 'u_time');
const uDpr = gl.getUniformLocation(prog, 'u_dpr');
const uIcon = gl.getUniformLocation(prog, 'u_icon');
const uIconCenter = gl.getUniformLocation(prog, 'u_icon_center_px');
const uIconSize = gl.getUniformLocation(prog, 'u_icon_size_px');
const uWaveColor = gl.getUniformLocation(prog, 'u_wave_color');
const uWaveSpeed = gl.getUniformLocation(prog, 'u_wave_speed');
const uWaveFreq = gl.getUniformLocation(prog, 'u_wave_freq');
const uWaveAmp = gl.getUniformLocation(prog, 'u_wave_amp');
const uColorNum = gl.getUniformLocation(prog, 'u_color_num');
const uPixelSize = gl.getUniformLocation(prog, 'u_pixel_size');
gl.uniform1i(uIcon, 0);
// Warm magenta-leaning purple — the orange tilt resonates with the
// octopus's coral body, so the icon and bg feel like the same world.
// Pure purple (0.52, 0.29, 0.88) was too cool and the octopus looked
// pasted on; pure orange would clash with brand. This sits between.
gl.uniform3f(uWaveColor, 0.78, 0.32, 0.62);
gl.uniform1f(uWaveSpeed, 0.05);
gl.uniform1f(uWaveFreq, 3.0);
gl.uniform1f(uWaveAmp, 0.3);
gl.uniform1f(uColorNum, 4.0);
gl.uniform1f(uPixelSize, 2.0);
renderFrame = function (tSec) {
resizeCanvas();
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
// Octopus geometry: read the live .logo element's bounding rect so
// the shader draws the dithered icon EXACTLY where the (now-hidden)
// <img> would have appeared. This stays correct if CSS sizing
// changes or window is resized — no constants to drift.
var rect = logoEl ? logoEl.getBoundingClientRect() : null;
var cx, cy, sz;
if (rect && rect.width > 0) {
cx = rect.left + rect.width * 0.5;
cy = rect.top + rect.height * 0.5;
sz = Math.max(rect.width, rect.height);
} else {
// Fallback: center horizontally, top third vertically.
var cssW = canvas.width / dpr, cssH = canvas.height / dpr;
cx = cssW * 0.5; cy = cssH * 0.35; sz = 96;
}
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uTime, tSec);
gl.uniform1f(uDpr, dpr);
gl.uniform2f(uIconCenter, cx, cy);
gl.uniform1f(uIconSize, sz);
gl.drawArrays(gl.TRIANGLES, 0, 6);
};
}