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 CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
||||||
import CrmDocumentsPanel from "~~/app/components/workspace/documents/CrmDocumentsPanel.vue";
|
import CrmDocumentsPanel from "~~/app/components/workspace/documents/CrmDocumentsPanel.vue";
|
||||||
import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.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 CrmChangeReviewOverlay from "~~/app/components/workspace/review/CrmChangeReviewOverlay.vue";
|
||||||
import meQuery from "~~/graphql/operations/me.graphql?raw";
|
import meQuery from "~~/graphql/operations/me.graphql?raw";
|
||||||
import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
|
import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
|
||||||
@@ -424,6 +425,9 @@ const pilotTranscribing = ref(false);
|
|||||||
const pilotMicSupported = ref(false);
|
const pilotMicSupported = ref(false);
|
||||||
const pilotMicError = ref<string | null>(null);
|
const pilotMicError = ref<string | null>(null);
|
||||||
const pilotWaveContainer = ref<HTMLDivElement | null>(null);
|
const pilotWaveContainer = ref<HTMLDivElement | null>(null);
|
||||||
|
function setPilotWaveContainerRef(element: HTMLDivElement | null) {
|
||||||
|
pilotWaveContainer.value = element;
|
||||||
|
}
|
||||||
const livePilotUserText = ref("");
|
const livePilotUserText = ref("");
|
||||||
const livePilotAssistantText = ref("");
|
const livePilotAssistantText = ref("");
|
||||||
const contextPickerEnabled = ref(false);
|
const contextPickerEnabled = ref(false);
|
||||||
@@ -4750,254 +4754,50 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="grid h-full min-h-0 grid-cols-1 gap-0 lg:grid-cols-[320px_minmax(0,1fr)]">
|
<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">
|
<CrmPilotSidebar
|
||||||
<div class="flex h-full min-h-0 flex-col p-0">
|
:pilot-header-text="pilotHeaderText"
|
||||||
<div class="pilot-header">
|
:chat-switching="chatSwitching"
|
||||||
<div>
|
:chat-threads-loading="chatThreadsLoading"
|
||||||
<h2 class="text-sm font-semibold text-white/75">{{ pilotHeaderText }}</h2>
|
:chat-conversations="chatConversations"
|
||||||
</div>
|
:auth-me="authMe"
|
||||||
</div>
|
:chat-creating="chatCreating"
|
||||||
<div class="pilot-threads">
|
:rendered-pilot-messages="renderedPilotMessages"
|
||||||
<div class="flex w-full items-center justify-between gap-2">
|
:pilot-live-logs="pilotLiveLogs"
|
||||||
<button
|
:pilot-live-logs-expanded="pilotLiveLogsExpanded"
|
||||||
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"
|
:pilot-live-log-hidden-count="pilotLiveLogHiddenCount"
|
||||||
:disabled="chatSwitching || chatThreadsLoading || chatConversations.length === 0"
|
:pilot-visible-log-count="pilotVisibleLogCount"
|
||||||
:title="authMe?.conversation?.title || 'Thread'"
|
:pilot-visible-live-logs="pilotVisibleLiveLogs"
|
||||||
@click="toggleChatThreadPicker"
|
:chat-thread-picker-open="chatThreadPickerOpen"
|
||||||
>
|
:selected-chat-id="selectedChatId"
|
||||||
<span class="truncate">{{ authMe?.conversation?.title || "Thread" }}</span>
|
:chat-archiving-id="chatArchivingId"
|
||||||
<svg viewBox="0 0 20 20" class="ml-1 h-3.5 w-3.5 fill-current opacity-80">
|
:pilot-input-ref="pilotInput"
|
||||||
<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" />
|
:pilot-recording="pilotRecording"
|
||||||
</svg>
|
:context-scope-chips="contextScopeChips"
|
||||||
</button>
|
:context-picker-enabled="contextPickerEnabled"
|
||||||
<button
|
:pilot-transcribing="pilotTranscribing"
|
||||||
class="btn btn-ghost btn-xs h-7 min-h-7 px-1 text-white/85 hover:bg-white/10"
|
:pilot-sending="pilotSending"
|
||||||
:disabled="chatCreating"
|
:pilot-mic-supported="pilotMicSupported"
|
||||||
title="New chat"
|
:pilot-mic-error="pilotMicError"
|
||||||
@click="createNewChatConversation"
|
:toggle-chat-thread-picker="toggleChatThreadPicker"
|
||||||
>
|
:create-new-chat-conversation="createNewChatConversation"
|
||||||
{{ chatCreating ? "…" : "+" }}
|
:pilot-role-badge="pilotRoleBadge"
|
||||||
</button>
|
:pilot-role-name="pilotRoleName"
|
||||||
</div>
|
:format-pilot-stamp="formatPilotStamp"
|
||||||
</div>
|
:summarize-change-actions="summarizeChangeActions"
|
||||||
|
:summarize-change-entities="summarizeChangeEntities"
|
||||||
<div class="pilot-stream-wrap min-h-0 flex-1">
|
:open-change-review="openChangeReview"
|
||||||
<div class="pilot-timeline min-h-0 h-full overflow-y-auto">
|
:toggle-pilot-live-logs-expanded="togglePilotLiveLogsExpanded"
|
||||||
<div
|
:close-chat-thread-picker="closeChatThreadPicker"
|
||||||
v-for="message in renderedPilotMessages"
|
:switch-chat-conversation="switchChatConversation"
|
||||||
:key="message.id"
|
:format-chat-thread-meta="formatChatThreadMeta"
|
||||||
class="pilot-row"
|
:archive-chat-conversation="archiveChatConversation"
|
||||||
>
|
:handle-pilot-composer-enter="handlePilotComposerEnter"
|
||||||
<div class="pilot-avatar" :class="message.role === 'user' ? 'pilot-avatar-user' : ''">
|
:set-pilot-wave-container-ref="setPilotWaveContainerRef"
|
||||||
{{ pilotRoleBadge(message.role) }}
|
:toggle-context-picker="toggleContextPicker"
|
||||||
</div>
|
:remove-context-scope="removeContextScope"
|
||||||
|
:toggle-pilot-recording="togglePilotRecording"
|
||||||
<div class="pilot-body">
|
:handle-pilot-send-action="handlePilotSendAction"
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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">
|
<main class="relative min-h-0 bg-base-100">
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
@@ -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 {
|
.comm-input-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -7009,56 +6550,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
padding: 14px;
|
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 {
|
.feed-chart-wrap {
|
||||||
display: flex;
|
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