Files
rubber-duck-mcp/apps/web/src/routes/+page.svelte
T
2026-09-05 10:25:18 +05:00

245 lines
5.4 KiB
Svelte

<script lang="ts">
import DuckAvatar from '$lib/components/duck/DuckAvatar.svelte';
import ChatPanel from '$lib/components/duck/ChatPanel.svelte';
import WardrobePanel from '$lib/components/duck/WardrobePanel.svelte';
import SettingsPanel from '$lib/components/duck/SettingsPanel.svelte';
import { outfit, saveOutfit } from '$lib/duck/state.svelte';
const TABS = [
{ id: 'chat', label: 'Чат' },
{ id: 'wardrobe', label: 'Гардероб' },
{ id: 'settings', label: 'Настройки' }
] as const;
type Tab = (typeof TABS)[number]['id'];
let tab = $state<Tab>('chat');
// сохраняем образ при любом изменении
$effect(() => {
saveOutfit();
});
const outfitLine = $derived(
`{"cap":"${outfit.cap}","cap_logo":${JSON.stringify(outfit.capLogo)},` +
`"tee":"${outfit.tee}","tee_logo":${JSON.stringify(outfit.teeLogo)}}`
);
</script>
<svelte:head>
<title>Rubber Duck — отладка с уткой</title>
<meta name="description" content="Резиновая утка для отладки: переодевай, вешай логотипы, рассказывай о проблеме." />
</svelte:head>
<section class="hero">
<div>
<h1>Rubber Duck<span class="cursor mono">_</span></h1>
<p class="sub">
Проговори проблему вслух — утка выслушает. Пока говоришь, решение часто находится само.
</p>
</div>
<div class="chips mono">
<span class="chip">debugging-as-a-service</span>
<span class="chip">local-first</span>
<span class="chip">quack · симуляция</span>
</div>
</section>
<div class="grid">
<section class="stage" aria-label="Сцена с уткой">
<div class="stage-top mono">
<span>duck_id: RU-1337</span>
<span>status: listening</span>
<span>uptime: ∞</span>
</div>
<div class="duck-wrap">
<DuckAvatar />
</div>
<div class="term mono">
<div class="term-line"><span class="prompt">$</span> duck outfit --json</div>
<div class="term-out">{outfitLine}</div>
</div>
</section>
<aside class="side" aria-label="Чат и настройки">
<div class="tabs mono" role="tablist">
{#each TABS as t (t.id)}
<button
role="tab"
class:active={tab === t.id}
aria-selected={tab === t.id}
onclick={() => (tab = t.id)}
>
{t.label}
</button>
{/each}
</div>
<div class="tab-body chat-body" class:hidden={tab !== 'chat'}>
<ChatPanel />
</div>
<div class="tab-body" class:hidden={tab !== 'wardrobe'}>
<WardrobePanel />
</div>
<div class="tab-body" class:hidden={tab !== 'settings'}>
<SettingsPanel />
</div>
</aside>
</div>
<style>
.hero {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
margin-bottom: 22px;
flex-wrap: wrap;
}
h1 {
margin: 0;
font-size: 34px;
line-height: 1.15;
letter-spacing: -0.02em;
}
.cursor {
color: #f5883c;
animation: caret 1.1s steps(1) infinite;
font-weight: 400;
}
@keyframes caret {
50% {
opacity: 0;
}
}
.sub {
margin: 8px 0 0;
color: var(--muted);
max-width: 520px;
font-size: 15px;
}
.chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
font-size: 11.5px;
color: var(--muted);
border: 1px solid var(--border);
background: var(--panel);
border-radius: 999px;
padding: 4px 10px;
}
.grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 400px;
gap: 20px;
align-items: stretch;
}
.stage {
border: 1px solid var(--border);
border-radius: 16px;
background:
radial-gradient(420px 320px at 50% 46%, rgba(255, 217, 59, 0.09), transparent 70%),
linear-gradient(rgba(148, 163, 184, 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.06) 1px, transparent 1px),
var(--panel);
background-size: auto, 28px 28px, 28px 28px, auto;
display: flex;
flex-direction: column;
overflow: hidden;
}
.stage-top {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 10px 16px;
font-size: 11.5px;
color: var(--muted);
border-bottom: 1px solid var(--border);
}
.duck-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 22px 20px 10px;
min-height: 420px;
}
.term {
border-top: 1px solid var(--border);
background: rgba(2, 6, 23, 0.45);
padding: 10px 16px 12px;
font-size: 12px;
}
.term-line {
color: var(--muted);
}
.prompt {
color: var(--ok);
}
.term-out {
color: #fbbf24;
margin-top: 2px;
word-break: break-all;
}
.side {
border: 1px solid var(--border);
border-radius: 16px;
background: var(--panel);
display: flex;
flex-direction: column;
height: 640px;
overflow: hidden;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
}
.tabs button {
flex: 1;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--muted);
font: inherit;
font-size: 13px;
padding: 12px 8px;
cursor: pointer;
}
.tabs button:hover {
color: var(--text);
}
.tabs button.active {
color: var(--text);
border-bottom-color: #3b82f6;
}
.tab-body {
flex: 1;
overflow-y: auto;
padding: 14px;
min-height: 0;
}
.chat-body {
display: flex;
}
.tab-body.hidden {
display: none;
}
@media (max-width: 1020px) {
.grid {
grid-template-columns: 1fr;
}
.side {
height: auto;
}
.chat-body {
height: 520px;
}
.duck-wrap {
min-height: 320px;
}
}
</style>