640 lines
21 KiB
Vue
640 lines
21 KiB
Vue
<script setup lang="ts">
|
||
type ChatConversation = {
|
||
id: string;
|
||
title: string;
|
||
lastMessageText?: string | null;
|
||
};
|
||
|
||
type PilotMessage = {
|
||
id: string;
|
||
role: "user" | "assistant" | "system";
|
||
text: string;
|
||
messageKind?: string | null;
|
||
changeSummary?: string | null;
|
||
changeItems?: Array<{ id: string; entity: string; action: string; title: string }> | null;
|
||
changeSetId?: string | null;
|
||
changeStatus?: string | null;
|
||
createdAt?: string;
|
||
};
|
||
|
||
type ContextScopeChip = {
|
||
scope: string;
|
||
label: string;
|
||
};
|
||
|
||
defineProps<{
|
||
pilotHeaderText: string;
|
||
chatSwitching: boolean;
|
||
chatThreadsLoading: boolean;
|
||
chatConversations: ChatConversation[];
|
||
authMe: { conversation?: { title?: string | null } | null } | null;
|
||
chatCreating: boolean;
|
||
renderedPilotMessages: PilotMessage[];
|
||
pilotLiveLogs: Array<{ id: string; text: string }>;
|
||
pilotLiveLogsExpanded: boolean;
|
||
pilotLiveLogHiddenCount: number;
|
||
pilotVisibleLogCount: number;
|
||
pilotVisibleLiveLogs: Array<{ id: string; text: string }>;
|
||
chatThreadPickerOpen: boolean;
|
||
selectedChatId: string;
|
||
chatArchivingId: string;
|
||
pilotInput: string;
|
||
pilotRecording: boolean;
|
||
contextScopeChips: ContextScopeChip[];
|
||
contextPickerEnabled: boolean;
|
||
pilotTranscribing: boolean;
|
||
pilotSending: boolean;
|
||
pilotMicSupported: boolean;
|
||
pilotMicError: string | null;
|
||
toggleChatThreadPicker: () => void;
|
||
createNewChatConversation: () => void;
|
||
pilotRoleBadge: (role: PilotMessage["role"]) => string;
|
||
pilotRoleName: (role: PilotMessage["role"]) => string;
|
||
formatPilotStamp: (iso?: string) => string;
|
||
summarizeChangeActions: (items: PilotMessage["changeItems"] | null | undefined) => {
|
||
created: number;
|
||
updated: number;
|
||
deleted: number;
|
||
};
|
||
summarizeChangeEntities: (
|
||
items: PilotMessage["changeItems"] | null | undefined,
|
||
) => Array<{ entity: string; count: number }>;
|
||
openChangeReview: (changeSetId: string, step?: number, push?: boolean) => void;
|
||
togglePilotLiveLogsExpanded: () => void;
|
||
closeChatThreadPicker: () => void;
|
||
switchChatConversation: (id: string) => void;
|
||
formatChatThreadMeta: (conversation: ChatConversation) => string;
|
||
archiveChatConversation: (id: string) => void;
|
||
handlePilotComposerEnter: (event: KeyboardEvent) => void;
|
||
onPilotInput: (value: string) => void;
|
||
setPilotWaveContainerRef: (element: HTMLDivElement | null) => void;
|
||
toggleContextPicker: () => void;
|
||
removeContextScope: (scope: string) => void;
|
||
togglePilotRecording: () => void;
|
||
handlePilotSendAction: () => void;
|
||
}>();
|
||
</script>
|
||
|
||
<template>
|
||
<aside class="pilot-shell min-h-0 border-r border-base-300">
|
||
<div class="flex h-full min-h-0 flex-col p-0">
|
||
<div class="pilot-header">
|
||
<div>
|
||
<h2 class="text-sm font-semibold text-white/75">{{ pilotHeaderText }}</h2>
|
||
</div>
|
||
</div>
|
||
<div class="pilot-threads">
|
||
<div class="flex w-full items-center justify-between gap-2">
|
||
<button
|
||
class="btn btn-ghost btn-xs h-7 min-h-7 max-w-[228px] justify-start px-1 text-xs font-medium text-white/90 hover:bg-white/10"
|
||
:disabled="chatSwitching || chatThreadsLoading || chatConversations.length === 0"
|
||
:title="authMe?.conversation?.title || 'Thread'"
|
||
@click="toggleChatThreadPicker"
|
||
>
|
||
<span class="truncate">{{ authMe?.conversation?.title || "Thread" }}</span>
|
||
<svg viewBox="0 0 20 20" class="ml-1 h-3.5 w-3.5 fill-current opacity-80">
|
||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
class="btn btn-ghost btn-xs h-7 min-h-7 px-1 text-white/85 hover:bg-white/10"
|
||
:disabled="chatCreating"
|
||
title="New chat"
|
||
@click="createNewChatConversation"
|
||
>
|
||
{{ chatCreating ? "…" : "+" }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pilot-stream-wrap min-h-0 flex-1">
|
||
<div class="pilot-timeline min-h-0 h-full overflow-y-auto">
|
||
<div
|
||
v-for="message in renderedPilotMessages"
|
||
:key="message.id"
|
||
class="pilot-row"
|
||
>
|
||
<div class="pilot-avatar" :class="message.role === 'user' ? 'pilot-avatar-user' : ''">
|
||
{{ pilotRoleBadge(message.role) }}
|
||
</div>
|
||
|
||
<div class="pilot-body">
|
||
<div class="pilot-meta">
|
||
<span class="pilot-author">{{ pilotRoleName(message.role) }}</span>
|
||
<span class="pilot-time">{{ formatPilotStamp(message.createdAt) }}</span>
|
||
</div>
|
||
|
||
<div v-if="message.messageKind === 'change_set_summary'" class="rounded-xl border border-amber-300/35 bg-amber-500/10 p-3">
|
||
<p class="text-xs font-semibold text-amber-100">
|
||
{{ message.changeSummary || "Technical change summary" }}
|
||
</p>
|
||
<div class="mt-2 overflow-x-auto">
|
||
<table class="w-full min-w-[340px] text-left text-[11px] text-white/85">
|
||
<thead>
|
||
<tr class="text-white/60">
|
||
<th class="py-1 pr-2 font-medium">Metric</th>
|
||
<th class="py-1 pr-2 font-medium">Value</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td class="py-1 pr-2">Total changes</td>
|
||
<td class="py-1 pr-2">{{ message.changeItems?.length || 0 }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="py-1 pr-2">Created</td>
|
||
<td class="py-1 pr-2">{{ summarizeChangeActions(message.changeItems).created }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="py-1 pr-2">Updated</td>
|
||
<td class="py-1 pr-2">{{ summarizeChangeActions(message.changeItems).updated }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="py-1 pr-2">Archived</td>
|
||
<td class="py-1 pr-2">{{ summarizeChangeActions(message.changeItems).deleted }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div v-if="summarizeChangeEntities(message.changeItems).length" class="mt-2 flex flex-wrap gap-1.5">
|
||
<span
|
||
v-for="row in summarizeChangeEntities(message.changeItems)"
|
||
:key="`entity-summary-${message.id}-${row.entity}`"
|
||
class="rounded border border-white/20 px-2 py-0.5 text-[10px] text-white/75"
|
||
>
|
||
{{ row.entity }}: {{ row.count }}
|
||
</span>
|
||
</div>
|
||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||
<button
|
||
v-if="message.changeSetId"
|
||
class="btn btn-xs btn-outline"
|
||
@click="openChangeReview(message.changeSetId, 0, true)"
|
||
>
|
||
Review Changes
|
||
</button>
|
||
<span class="text-[10px] uppercase tracking-wide text-amber-100/80">
|
||
status: {{ message.changeStatus || "pending" }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="pilot-message-text">
|
||
{{ message.text }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="pilotLiveLogs.length" class="pilot-stream-status">
|
||
<div class="pilot-stream-head">
|
||
<p v-if="!pilotLiveLogsExpanded && pilotLiveLogHiddenCount > 0" class="pilot-stream-caption">
|
||
Showing last {{ pilotVisibleLogCount }} steps
|
||
</p>
|
||
<button
|
||
v-if="pilotLiveLogHiddenCount > 0 || pilotLiveLogsExpanded"
|
||
type="button"
|
||
class="pilot-stream-toggle"
|
||
@click="togglePilotLiveLogsExpanded"
|
||
>
|
||
{{ pilotLiveLogsExpanded ? "Show less" : `Show all (+${pilotLiveLogHiddenCount})` }}
|
||
</button>
|
||
</div>
|
||
<p
|
||
v-for="log in pilotVisibleLiveLogs"
|
||
:key="`pilot-log-${log.id}`"
|
||
class="pilot-stream-line"
|
||
:class="log.id === pilotLiveLogs[pilotLiveLogs.length - 1]?.id ? 'pilot-stream-line-current' : ''"
|
||
>
|
||
{{ log.text }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="chatThreadPickerOpen" class="pilot-thread-overlay">
|
||
<div class="mb-2 flex items-center justify-between">
|
||
<p class="text-[11px] font-semibold uppercase tracking-wide text-white/60">Threads</p>
|
||
<button class="btn btn-ghost btn-xs btn-square text-white/70 hover:bg-white/10" title="Close" @click="closeChatThreadPicker">
|
||
<svg viewBox="0 0 20 20" class="h-3.5 w-3.5 fill-current">
|
||
<path d="M11.06 10 15.53 5.53a.75.75 0 1 0-1.06-1.06L10 8.94 5.53 4.47a.75.75 0 0 0-1.06 1.06L8.94 10l-4.47 4.47a.75.75 0 1 0 1.06 1.06L10 11.06l4.47 4.47a.75.75 0 0 0 1.06-1.06z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="max-h-full space-y-1 overflow-y-auto pr-1">
|
||
<div
|
||
v-for="thread in chatConversations"
|
||
:key="`thread-row-${thread.id}`"
|
||
class="flex items-center gap-1 rounded-md"
|
||
>
|
||
<button
|
||
class="min-w-0 flex-1 rounded-md px-2 py-1.5 text-left transition hover:bg-white/10"
|
||
:class="selectedChatId === thread.id ? 'bg-white/12' : ''"
|
||
:disabled="chatSwitching || chatArchivingId === thread.id"
|
||
@click="switchChatConversation(thread.id)"
|
||
>
|
||
<p class="truncate text-xs font-medium text-white">{{ thread.title }}</p>
|
||
<p class="truncate text-[11px] text-white/55">
|
||
{{ thread.lastMessageText || "No messages yet" }} · {{ formatChatThreadMeta(thread) }}
|
||
</p>
|
||
</button>
|
||
<button
|
||
class="btn btn-ghost btn-xs btn-square text-white/55 hover:bg-white/10 hover:text-red-300"
|
||
:disabled="chatSwitching || chatArchivingId === thread.id || chatConversations.length <= 1"
|
||
title="Archive thread"
|
||
@click="archiveChatConversation(thread.id)"
|
||
>
|
||
<span v-if="chatArchivingId === thread.id" class="loading loading-spinner loading-xs" />
|
||
<svg v-else viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
|
||
<path d="M20.54 5.23 19 3H5L3.46 5.23A2 2 0 0 0 3 6.36V8a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.36a2 2 0 0 0-.46-1.13M5.16 5h13.68l.5.73A.5.5 0 0 1 19.5 6H4.5a.5.5 0 0 1-.34-.27zM6 12h12v6a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pilot-input-wrap">
|
||
<div class="pilot-input-shell">
|
||
<textarea
|
||
:value="pilotInput"
|
||
class="pilot-input-textarea"
|
||
:placeholder="pilotRecording ? 'Recording... speak, then press mic to fill or send to submit' : 'Type a message for Pilot...'"
|
||
@input="onPilotInput(($event.target as HTMLTextAreaElement | null)?.value ?? '')"
|
||
@keydown.enter="handlePilotComposerEnter"
|
||
/>
|
||
|
||
<div v-if="pilotRecording" class="pilot-meter">
|
||
<div :ref="setPilotWaveContainerRef" class="pilot-wave-canvas" />
|
||
</div>
|
||
|
||
<div v-if="!pilotRecording" class="pilot-input-context">
|
||
<button
|
||
v-if="contextScopeChips.length === 0"
|
||
class="context-pipette-trigger"
|
||
:class="contextPickerEnabled ? 'context-pipette-active' : ''"
|
||
:disabled="pilotTranscribing || pilotSending"
|
||
aria-label="Контекстная пипетка"
|
||
:title="contextPickerEnabled ? 'Выключить пипетку' : 'Включить пипетку контекста'"
|
||
@click="toggleContextPicker"
|
||
>
|
||
<svg viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
|
||
<path d="M19.29 4.71a1 1 0 0 0-1.42 0l-2.58 2.58-1.17-1.17a1 1 0 0 0-1.41 0l-1.42 1.42a1 1 0 0 0 0 1.41l.59.59-5.3 5.3a2 2 0 0 0-.53.92l-.86 3.43a1 1 0 0 0 1.21 1.21l3.43-.86a2 2 0 0 0 .92-.53l5.3-5.3.59.59a1 1 0 0 0 1.41 0l1.42-1.42a1 1 0 0 0 0-1.41l-1.17-1.17 2.58-2.58a1 1 0 0 0 0-1.42z" />
|
||
</svg>
|
||
</button>
|
||
|
||
<div v-else class="pilot-context-chips">
|
||
<button
|
||
v-for="chip in contextScopeChips"
|
||
:key="`context-chip-${chip.scope}`"
|
||
type="button"
|
||
class="context-pipette-chip"
|
||
@click="removeContextScope(chip.scope)"
|
||
>
|
||
{{ chip.label }}
|
||
<span class="opacity-70">×</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pilot-input-actions">
|
||
<button
|
||
class="btn btn-xs btn-circle border border-white/20 bg-transparent text-white/90 hover:bg-white/10"
|
||
:class="pilotRecording ? 'pilot-mic-active' : ''"
|
||
:disabled="!pilotMicSupported || pilotTranscribing || pilotSending"
|
||
:title="pilotRecording ? 'Stop and insert transcript' : 'Voice input'"
|
||
@click="togglePilotRecording"
|
||
>
|
||
<svg v-if="!pilotTranscribing" viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
|
||
<path d="M12 15a3 3 0 0 0 3-3V7a3 3 0 1 0-6 0v5a3 3 0 0 0 3 3m5-3a1 1 0 1 1 2 0 7 7 0 0 1-6 6.92V21h3a1 1 0 1 1 0 2H8a1 1 0 1 1 0-2h3v-2.08A7 7 0 0 1 5 12a1 1 0 1 1 2 0 5 5 0 0 0 10 0" />
|
||
</svg>
|
||
<span v-else class="loading loading-spinner loading-xs" />
|
||
</button>
|
||
|
||
<button
|
||
class="btn btn-sm btn-circle border-0 bg-[#5865f2] text-white hover:bg-[#4752c4]"
|
||
:disabled="pilotTranscribing || pilotSending || (!pilotRecording && !String(pilotInput ?? '').trim())"
|
||
:title="pilotRecording ? 'Transcribe and send' : 'Send message'"
|
||
@click="handlePilotSendAction"
|
||
>
|
||
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current" :class="pilotSending ? 'opacity-50' : ''">
|
||
<path d="M4.5 19.5 21 12 4.5 4.5l.02 5.84L15 12l-10.48 1.66z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p v-if="pilotMicError" class="pilot-mic-error">{{ pilotMicError }}</p>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.pilot-shell {
|
||
background:
|
||
radial-gradient(circle at 10% -10%, rgba(124, 144, 255, 0.25), transparent 40%),
|
||
radial-gradient(circle at 85% 110%, rgba(88, 101, 242, 0.2), transparent 45%),
|
||
#151821;
|
||
color: #f5f7ff;
|
||
}
|
||
|
||
.pilot-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||
background: rgba(8, 10, 16, 0.2);
|
||
}
|
||
|
||
.pilot-threads {
|
||
padding: 10px 10px 8px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.pilot-timeline {
|
||
padding: 10px 8px;
|
||
}
|
||
|
||
.pilot-stream-wrap {
|
||
position: relative;
|
||
}
|
||
|
||
.pilot-thread-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 20;
|
||
padding: 10px 8px;
|
||
background:
|
||
linear-gradient(180deg, rgba(15, 18, 28, 0.96), rgba(15, 18, 28, 0.92)),
|
||
rgba(15, 18, 28, 0.9);
|
||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.pilot-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
padding: 8px 6px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.pilot-row:hover {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
}
|
||
|
||
.pilot-avatar {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 30px;
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 999px;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.05em;
|
||
color: #e6ebff;
|
||
background: linear-gradient(135deg, #5865f2, #7c90ff);
|
||
}
|
||
|
||
.pilot-avatar-user {
|
||
background: linear-gradient(135deg, #2a9d8f, #38b2a7);
|
||
}
|
||
|
||
.pilot-body {
|
||
min-width: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.pilot-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pilot-author {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: #f8f9ff;
|
||
}
|
||
|
||
.pilot-time {
|
||
font-size: 11px;
|
||
color: rgba(255, 255, 255, 0.45);
|
||
}
|
||
|
||
.pilot-message-text {
|
||
margin-top: 2px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
white-space: pre-wrap;
|
||
color: rgba(255, 255, 255, 0.92);
|
||
}
|
||
|
||
.pilot-input-wrap {
|
||
display: grid;
|
||
gap: 6px;
|
||
padding: 10px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||
background: transparent;
|
||
}
|
||
|
||
.pilot-input-shell {
|
||
position: relative;
|
||
}
|
||
|
||
.pilot-input-textarea {
|
||
width: 100%;
|
||
min-height: 96px;
|
||
resize: none;
|
||
border-radius: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
color: #f5f7ff;
|
||
padding: 10px 88px 36px 12px;
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.pilot-input-textarea::placeholder {
|
||
color: rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
.pilot-input-textarea:focus {
|
||
outline: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.pilot-input-actions {
|
||
position: absolute;
|
||
right: 10px;
|
||
bottom: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.pilot-input-context {
|
||
position: absolute;
|
||
left: 10px;
|
||
bottom: 8px;
|
||
right: 96px;
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 24px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.context-pipette-trigger {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 26px;
|
||
height: 26px;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||
background: rgba(255, 255, 255, 0.06);
|
||
color: rgba(245, 247, 255, 0.94);
|
||
padding: 0;
|
||
transition: transform 220ms ease, border-color 220ms ease, background-color 220ms ease;
|
||
}
|
||
|
||
.context-pipette-trigger:hover {
|
||
transform: scale(1.03);
|
||
border-color: color-mix(in oklab, var(--color-primary) 62%, transparent);
|
||
background: color-mix(in oklab, var(--color-primary) 18%, transparent);
|
||
}
|
||
|
||
.context-pipette-active {
|
||
border-color: color-mix(in oklab, var(--color-primary) 72%, transparent);
|
||
background: color-mix(in oklab, var(--color-primary) 24%, transparent);
|
||
box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-primary) 38%, transparent) inset;
|
||
}
|
||
|
||
.pilot-context-chips {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
max-width: 100%;
|
||
overflow-x: auto;
|
||
padding-bottom: 1px;
|
||
}
|
||
|
||
.pilot-context-chips::-webkit-scrollbar {
|
||
height: 3px;
|
||
}
|
||
|
||
.pilot-context-chips::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.24);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.context-pipette-chip {
|
||
flex: 0 0 auto;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: rgba(245, 247, 255, 0.95);
|
||
padding: 4px 9px;
|
||
font-size: 10px;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
transition: transform 220ms ease, border-color 220ms ease, background-color 220ms ease;
|
||
}
|
||
|
||
.context-pipette-chip:hover {
|
||
transform: scale(1.03);
|
||
border-color: color-mix(in oklab, var(--color-primary) 62%, transparent);
|
||
background: color-mix(in oklab, var(--color-primary) 20%, transparent);
|
||
}
|
||
|
||
.pilot-meter {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 88px;
|
||
bottom: 9px;
|
||
height: 22px;
|
||
}
|
||
|
||
.pilot-wave-canvas {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.pilot-wave-canvas :deep(wave) {
|
||
display: block;
|
||
height: 100% !important;
|
||
}
|
||
|
||
.pilot-wave-canvas :deep(canvas) {
|
||
height: 100% !important;
|
||
}
|
||
|
||
.pilot-mic-active {
|
||
border-color: rgba(255, 95, 95, 0.8) !important;
|
||
background: rgba(255, 95, 95, 0.16) !important;
|
||
color: #ffd9d9 !important;
|
||
}
|
||
|
||
.pilot-mic-error {
|
||
margin: 0;
|
||
font-size: 11px;
|
||
color: rgba(255, 160, 160, 0.92);
|
||
}
|
||
|
||
.pilot-stream-status {
|
||
margin-top: 8px;
|
||
padding: 2px 4px 8px;
|
||
display: grid;
|
||
gap: 3px;
|
||
}
|
||
|
||
.pilot-stream-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.pilot-stream-caption {
|
||
margin: 0;
|
||
text-align: center;
|
||
font-size: 10px;
|
||
line-height: 1.3;
|
||
color: rgba(174, 185, 223, 0.72);
|
||
}
|
||
|
||
.pilot-stream-toggle {
|
||
border: 1px solid rgba(164, 179, 230, 0.35);
|
||
background: rgba(25, 33, 56, 0.45);
|
||
color: rgba(229, 235, 255, 0.92);
|
||
border-radius: 999px;
|
||
font-size: 10px;
|
||
line-height: 1.2;
|
||
padding: 3px 8px;
|
||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||
}
|
||
|
||
.pilot-stream-toggle:hover {
|
||
border-color: rgba(180, 194, 240, 0.6);
|
||
background: rgba(35, 45, 72, 0.7);
|
||
}
|
||
|
||
.pilot-stream-line {
|
||
margin: 0;
|
||
text-align: center;
|
||
font-size: 11px;
|
||
line-height: 1.35;
|
||
color: rgba(189, 199, 233, 0.72);
|
||
}
|
||
|
||
.pilot-stream-line-current {
|
||
color: rgba(234, 239, 255, 0.95);
|
||
}
|
||
</style>
|