refactor(frontend): extract pilot sidebar into workspace component
This commit is contained in:
@@ -4,6 +4,7 @@ import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue"
|
||||
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
||||
import CrmDocumentsPanel from "~~/app/components/workspace/documents/CrmDocumentsPanel.vue";
|
||||
import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.vue";
|
||||
import CrmPilotSidebar from "~~/app/components/workspace/pilot/CrmPilotSidebar.vue";
|
||||
import CrmChangeReviewOverlay from "~~/app/components/workspace/review/CrmChangeReviewOverlay.vue";
|
||||
import meQuery from "~~/graphql/operations/me.graphql?raw";
|
||||
import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
|
||||
@@ -424,6 +425,9 @@ const pilotTranscribing = ref(false);
|
||||
const pilotMicSupported = ref(false);
|
||||
const pilotMicError = ref<string | null>(null);
|
||||
const pilotWaveContainer = ref<HTMLDivElement | null>(null);
|
||||
function setPilotWaveContainerRef(element: HTMLDivElement | null) {
|
||||
pilotWaveContainer.value = element;
|
||||
}
|
||||
const livePilotUserText = ref("");
|
||||
const livePilotAssistantText = ref("");
|
||||
const contextPickerEnabled = ref(false);
|
||||
@@ -4750,255 +4754,51 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
|
||||
<template v-else>
|
||||
<div class="grid h-full min-h-0 grid-cols-1 gap-0 lg:grid-cols-[320px_minmax(0,1fr)]">
|
||||
<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
|
||||
v-model="pilotInput"
|
||||
class="pilot-input-textarea"
|
||||
:placeholder="pilotRecording ? 'Recording... speak, then press mic to fill or send to submit' : 'Type a message for Pilot...'"
|
||||
@keydown.enter="handlePilotComposerEnter"
|
||||
<CrmPilotSidebar
|
||||
:pilot-header-text="pilotHeaderText"
|
||||
:chat-switching="chatSwitching"
|
||||
:chat-threads-loading="chatThreadsLoading"
|
||||
:chat-conversations="chatConversations"
|
||||
:auth-me="authMe"
|
||||
:chat-creating="chatCreating"
|
||||
:rendered-pilot-messages="renderedPilotMessages"
|
||||
:pilot-live-logs="pilotLiveLogs"
|
||||
:pilot-live-logs-expanded="pilotLiveLogsExpanded"
|
||||
:pilot-live-log-hidden-count="pilotLiveLogHiddenCount"
|
||||
:pilot-visible-log-count="pilotVisibleLogCount"
|
||||
:pilot-visible-live-logs="pilotVisibleLiveLogs"
|
||||
:chat-thread-picker-open="chatThreadPickerOpen"
|
||||
:selected-chat-id="selectedChatId"
|
||||
:chat-archiving-id="chatArchivingId"
|
||||
:pilot-input-ref="pilotInput"
|
||||
:pilot-recording="pilotRecording"
|
||||
:context-scope-chips="contextScopeChips"
|
||||
:context-picker-enabled="contextPickerEnabled"
|
||||
:pilot-transcribing="pilotTranscribing"
|
||||
:pilot-sending="pilotSending"
|
||||
:pilot-mic-supported="pilotMicSupported"
|
||||
:pilot-mic-error="pilotMicError"
|
||||
:toggle-chat-thread-picker="toggleChatThreadPicker"
|
||||
:create-new-chat-conversation="createNewChatConversation"
|
||||
:pilot-role-badge="pilotRoleBadge"
|
||||
:pilot-role-name="pilotRoleName"
|
||||
:format-pilot-stamp="formatPilotStamp"
|
||||
:summarize-change-actions="summarizeChangeActions"
|
||||
:summarize-change-entities="summarizeChangeEntities"
|
||||
:open-change-review="openChangeReview"
|
||||
:toggle-pilot-live-logs-expanded="togglePilotLiveLogsExpanded"
|
||||
:close-chat-thread-picker="closeChatThreadPicker"
|
||||
:switch-chat-conversation="switchChatConversation"
|
||||
:format-chat-thread-meta="formatChatThreadMeta"
|
||||
:archive-chat-conversation="archiveChatConversation"
|
||||
:handle-pilot-composer-enter="handlePilotComposerEnter"
|
||||
:set-pilot-wave-container-ref="setPilotWaveContainerRef"
|
||||
:toggle-context-picker="toggleContextPicker"
|
||||
:remove-context-scope="removeContextScope"
|
||||
:toggle-pilot-recording="togglePilotRecording"
|
||||
:handle-pilot-send-action="handlePilotSendAction"
|
||||
/>
|
||||
|
||||
<div v-if="pilotRecording" class="pilot-meter">
|
||||
<div ref="pilotWaveContainer" 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 && !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>
|
||||
|
||||
<main class="relative min-h-0 bg-base-100">
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<CrmWorkspaceTopbar
|
||||
@@ -6607,265 +6407,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.comm-input-wrap {
|
||||
display: grid;
|
||||
@@ -7009,56 +6550,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.feed-chart-wrap {
|
||||
display: flex;
|
||||
|
||||
637
frontend/app/components/workspace/pilot/CrmPilotSidebar.vue
Normal file
637
frontend/app/components/workspace/pilot/CrmPilotSidebar.vue
Normal file
@@ -0,0 +1,637 @@
|
||||
<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;
|
||||
pilotInputRef: { value: 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;
|
||||
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
|
||||
v-model="pilotInputRef.value"
|
||||
class="pilot-input-textarea"
|
||||
:placeholder="pilotRecording ? 'Recording... speak, then press mic to fill or send to submit' : 'Type a message for Pilot...'"
|
||||
@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 && !pilotInputRef.value.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>
|
||||
Reference in New Issue
Block a user