960 lines
31 KiB
Vue
960 lines
31 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useTranscriptDebug } from '@/composables/transcript-debug'
|
|
import { useVoiceInput } from '@/composables/useVoiceInput'
|
|
import { useSessionState } from '@/stores/session-state'
|
|
import { ChatContainer, AquaticBackground, AgentBadge, NewSessionModal } from '@/components/transcript-debug'
|
|
import type { AgentName } from '@/types/transcript-debug'
|
|
import { isTauri, isMobileTauri, getTauriWindow } from '@/lib/tauri'
|
|
import { usePipWindow } from '@/composables/usePipWindow'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const sessionState = useSessionState()
|
|
|
|
const {
|
|
selectedAgent,
|
|
sessions,
|
|
selectedSessionId,
|
|
conversation,
|
|
loading,
|
|
transitioning,
|
|
transitionError,
|
|
error,
|
|
isRealtime,
|
|
processing,
|
|
ephemeral,
|
|
terminalReady,
|
|
hookMeta,
|
|
openTerminals,
|
|
activeTerminalSessionId,
|
|
init,
|
|
switchAgent,
|
|
selectSession,
|
|
createNewSession,
|
|
switchToTerminal,
|
|
closeTerminal,
|
|
disconnectRealtime,
|
|
sendPrompt
|
|
} = useTranscriptDebug()
|
|
|
|
const voice = useVoiceInput({ language: 'es-419' })
|
|
const {
|
|
isRecording: voiceRecording,
|
|
transcript: voiceTranscript,
|
|
interimTranscript: voiceInterim,
|
|
voiceMode,
|
|
whisperStatus,
|
|
audioDevices,
|
|
selectedDeviceId,
|
|
lastAudioUrl,
|
|
isPlayingAudio,
|
|
} = voice
|
|
|
|
const agents: { id: AgentName; label: string }[] = [
|
|
{ id: 'ejecutor', label: 'Ejecutor' },
|
|
{ id: 'nucleo000', label: 'nucleo000' },
|
|
{ id: 'claude', label: 'Claude' }
|
|
]
|
|
|
|
const chatRef = ref<InstanceType<typeof ChatContainer> | null>(null)
|
|
const showSelector = ref(false)
|
|
const showNewSessionModal = ref(false)
|
|
const isPipWindow = computed(() => route.query.pip === '1')
|
|
|
|
// Readability overlay
|
|
const savedOverlay = localStorage.getItem('transcript-overlay-opacity')
|
|
const overlayOpacity = ref(savedOverlay !== null ? parseFloat(savedOverlay) : 0.55)
|
|
function setOverlayOpacity(val: number) {
|
|
overlayOpacity.value = val
|
|
localStorage.setItem('transcript-overlay-opacity', String(val))
|
|
}
|
|
|
|
// Input max lines
|
|
const savedMaxLines = localStorage.getItem('transcript-input-max-lines')
|
|
const inputMaxLines = ref(savedMaxLines !== null ? parseInt(savedMaxLines) : 6)
|
|
function setInputMaxLines(val: number) {
|
|
inputMaxLines.value = val
|
|
localStorage.setItem('transcript-input-max-lines', String(val))
|
|
}
|
|
|
|
// Scroll nav mode
|
|
type ScrollNavMode = 'scrollbar' | 'buttons' | 'none'
|
|
const savedScrollNav = localStorage.getItem('transcript-scroll-nav') as ScrollNavMode | null
|
|
const scrollNavMode = ref<ScrollNavMode>(
|
|
savedScrollNav && ['scrollbar', 'buttons', 'none'].includes(savedScrollNav) ? savedScrollNav : 'buttons'
|
|
)
|
|
function setScrollNavMode(val: ScrollNavMode) {
|
|
scrollNavMode.value = val
|
|
localStorage.setItem('transcript-scroll-nav', val)
|
|
}
|
|
|
|
// Scroll jump percent
|
|
const savedScrollJump = localStorage.getItem('transcript-scroll-jump')
|
|
const scrollJumpPercent = ref(savedScrollJump !== null ? parseInt(savedScrollJump) : 50)
|
|
function setScrollJumpPercent(val: number) {
|
|
scrollJumpPercent.value = val
|
|
localStorage.setItem('transcript-scroll-jump', String(val))
|
|
}
|
|
|
|
// Active terminal index label
|
|
const terminalLabel = computed(() => {
|
|
if (!activeTerminalSessionId.value) return null
|
|
const idx = sessionState.terminalRegistry.findIndex(
|
|
e => e.transcriptSessionId === activeTerminalSessionId.value
|
|
)
|
|
return idx >= 0 ? `T${idx + 1}` : null
|
|
})
|
|
|
|
function handleSend(message: string) {
|
|
voice.clearTranscript()
|
|
sendPrompt(message)
|
|
}
|
|
|
|
function handleAgentSwitch(agent: AgentName) {
|
|
switchAgent(agent)
|
|
}
|
|
|
|
function handleSessionSelect(sessionId: string) {
|
|
selectSession(sessionId)
|
|
showSelector.value = false
|
|
}
|
|
|
|
function handleCreateSession() {
|
|
showNewSessionModal.value = true
|
|
}
|
|
|
|
async function handleModalCreateNew(agent: AgentName, initialPrompt: string) {
|
|
showNewSessionModal.value = false
|
|
if (agent !== selectedAgent.value) {
|
|
switchAgent(agent)
|
|
}
|
|
await createNewSession()
|
|
if (initialPrompt.trim()) {
|
|
sendPrompt(initialPrompt.trim())
|
|
}
|
|
}
|
|
|
|
function handleTerminalSwitch(sessionId: string) {
|
|
const idx = sessionState.terminalRegistry.findIndex(
|
|
e => e.transcriptSessionId === sessionId
|
|
)
|
|
if (idx >= 0) {
|
|
router.push({ name: 'transcript-debug-terminal', params: { terminalIndex: String(idx + 1) } })
|
|
}
|
|
}
|
|
|
|
// Resolve terminalIndex param → sessionId and switch
|
|
function syncTerminalFromRoute() {
|
|
const param = route.params.terminalIndex
|
|
if (!param) return
|
|
const idx = parseInt(param as string) - 1
|
|
const entry = sessionState.terminalRegistry[idx]
|
|
if (entry && entry.transcriptSessionId !== activeTerminalSessionId.value) {
|
|
switchToTerminal(entry.transcriptSessionId)
|
|
}
|
|
}
|
|
|
|
watch(() => route.params.terminalIndex, () => syncTerminalFromRoute())
|
|
|
|
// Also react when registry populates (it may arrive after mount)
|
|
watch(() => sessionState.terminalRegistry.length, () => {
|
|
if (route.params.terminalIndex) syncTerminalFromRoute()
|
|
})
|
|
|
|
const isAndroid = isMobileTauri()
|
|
const { openPip, closePip, isPipOpen } = usePipWindow()
|
|
|
|
const currentTerminalIndex = computed(() => {
|
|
return parseInt(route.params.terminalIndex as string) || 1
|
|
})
|
|
const pipOpen = computed(() => isPipOpen(currentTerminalIndex.value))
|
|
|
|
async function closePipWindow() {
|
|
try {
|
|
const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow')
|
|
await getCurrentWebviewWindow().close()
|
|
} catch {
|
|
window.close()
|
|
}
|
|
}
|
|
|
|
const pipMini = ref(false)
|
|
async function togglePipMini() {
|
|
try {
|
|
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
|
const win = getCurrentWindow()
|
|
if (pipMini.value) {
|
|
await win.setSize(new (await import('@tauri-apps/api/dpi')).LogicalSize(380, 620))
|
|
pipMini.value = false
|
|
} else {
|
|
await win.setSize(new (await import('@tauri-apps/api/dpi')).LogicalSize(320, 180))
|
|
pipMini.value = true
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
async function enterPip() {
|
|
// Android: native PiP
|
|
if (isAndroid) {
|
|
const bridge = (window as any).AgentUI
|
|
if (bridge?.enterPip) bridge.enterPip()
|
|
return
|
|
}
|
|
|
|
// Desktop Tauri: toggle pip window using shared composable
|
|
if (!isTauri) return
|
|
const idx = currentTerminalIndex.value
|
|
|
|
if (pipOpen.value) {
|
|
await closePip(idx)
|
|
return
|
|
}
|
|
|
|
await openPip(idx)
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await init()
|
|
await voice.init()
|
|
syncTerminalFromRoute()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
disconnectRealtime()
|
|
voice.cleanup()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="['transcript-debug-page', { 'pip-mode': isPipWindow }]">
|
|
<!-- PiP window title bar with drag + close -->
|
|
<div v-if="isPipWindow" class="pip-titlebar">
|
|
<span class="pip-title">Agent UI</span>
|
|
<div class="pip-controls">
|
|
<button class="pip-btn pip-mini" @click="togglePipMini" :title="pipMini ? 'Restore' : 'Mini'">
|
|
<svg v-if="!pipMini" width="10" height="10" viewBox="0 0 10 10">
|
|
<rect x="0.5" y="4.5" width="9" height="5" fill="none" stroke="currentColor" stroke-width="1" />
|
|
</svg>
|
|
<svg v-else width="10" height="10" viewBox="0 0 10 10">
|
|
<rect x="0.5" y="0.5" width="9" height="9" fill="none" stroke="currentColor" stroke-width="1" />
|
|
</svg>
|
|
</button>
|
|
<button class="pip-btn pip-close" @click="closePipWindow" title="Close">
|
|
<svg width="10" height="10" viewBox="0 0 10 10">
|
|
<line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.4" />
|
|
<line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.4" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Terminal selector strip (hidden in PiP) -->
|
|
<div v-if="!isPipWindow" class="terminal-strip">
|
|
<div class="strip-left">
|
|
<button
|
|
v-for="(entry, idx) in sessionState.terminalRegistry"
|
|
:key="entry.transcriptSessionId"
|
|
:class="[
|
|
'strip-terminal-btn',
|
|
{
|
|
active: String(idx + 1) === route.params.terminalIndex || (!route.params.terminalIndex && entry.transcriptSessionId === activeTerminalSessionId),
|
|
dead: !entry.alive
|
|
}
|
|
]"
|
|
@click="handleTerminalSwitch(entry.transcriptSessionId)"
|
|
:title="entry.label"
|
|
>
|
|
<span :class="['strip-dot', { alive: entry.alive, dead: !entry.alive }]"></span>
|
|
T{{ idx + 1 }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="strip-right">
|
|
<AgentBadge
|
|
v-if="selectedAgent"
|
|
:agent="selectedAgent"
|
|
:connected="!!activeTerminalSessionId"
|
|
:terminals="openTerminals"
|
|
:active-session-id="activeTerminalSessionId"
|
|
:model="conversation?.model"
|
|
:version="conversation?.version"
|
|
@switch-terminal="handleTerminalSwitch"
|
|
@close-terminal="closeTerminal"
|
|
/>
|
|
<span :class="['realtime-dot', { connected: isRealtime }]">
|
|
<svg width="6" height="6" viewBox="0 0 8 8"><circle cx="4" cy="4" r="4" fill="currentColor"/></svg>
|
|
</span>
|
|
<button
|
|
@click.stop="chatRef?.collapseAllExceptLast()"
|
|
:class="['strip-btn', { active: chatRef?.allCollapsed }]"
|
|
title="Collapse all except last"
|
|
>
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline v-if="chatRef?.allCollapsed" points="6 9 12 15 18 9"/>
|
|
<template v-else><polyline points="18 15 12 9 6 15"/></template>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
@click.stop="chatRef?.toggleSelectMode()"
|
|
:class="['strip-btn', { active: chatRef?.selectMode }]"
|
|
title="Select messages"
|
|
>
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline v-if="chatRef?.selectMode" points="20 6 9 17 4 12"/>
|
|
<template v-else>
|
|
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
|
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
|
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
|
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
|
</template>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
v-if="isTauri"
|
|
@click.stop="enterPip"
|
|
:class="['strip-btn', 'pip-btn', { active: pipOpen }]"
|
|
title="Picture-in-Picture"
|
|
>
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
|
<rect x="12" y="9" width="8" height="6" rx="1" fill="currentColor" opacity="0.3"/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
@click.stop="showSelector = !showSelector"
|
|
:class="['strip-btn', { active: showSelector }]"
|
|
title="Agent/Session selector"
|
|
>
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="3"/>
|
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<div v-if="error" class="error-bar">{{ error }}</div>
|
|
|
|
<!-- Content -->
|
|
<div :class="['content-area', { 'selector-open': showSelector }, `nav-${scrollNavMode}`]">
|
|
<AquaticBackground />
|
|
<div class="readability-overlay" :style="{ background: `rgba(0, 0, 0, ${overlayOpacity})` }" />
|
|
|
|
<Transition name="terminal-loading">
|
|
<div v-if="transitioning" class="loading-overlay">
|
|
<div class="loading-spinner" />
|
|
</div>
|
|
</Transition>
|
|
|
|
<Transition name="terminal-loading">
|
|
<div v-if="transitionError" class="error-overlay" @click="transitionError = null">
|
|
<div class="error-content">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f87171" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
</svg>
|
|
<span class="error-msg">{{ transitionError }}</span>
|
|
<span class="error-hint">Click to dismiss</span>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
|
|
<ChatContainer
|
|
ref="chatRef"
|
|
v-if="conversation"
|
|
:conversation="conversation"
|
|
:processing="processing"
|
|
:terminal-ready="terminalReady"
|
|
:terminal="ephemeral"
|
|
:show-selector="showSelector"
|
|
:agents="agents"
|
|
:selected-agent="selectedAgent"
|
|
:sessions="sessions"
|
|
:selected-session-id="selectedSessionId"
|
|
:sessions-loading="loading"
|
|
:voice-mode="voiceMode"
|
|
:whisper-status="whisperStatus"
|
|
:audio-devices="audioDevices"
|
|
:selected-device-id="selectedDeviceId"
|
|
:is-recording="voiceRecording"
|
|
:voice-transcript="voiceTranscript + voiceInterim"
|
|
:last-audio-url="lastAudioUrl"
|
|
:is-playing-audio="isPlayingAudio"
|
|
:overlay-opacity="overlayOpacity"
|
|
:input-max-lines="inputMaxLines"
|
|
:scroll-jump-percent="scrollJumpPercent"
|
|
:scroll-nav-mode="scrollNavMode"
|
|
:hook-permission-mode="hookMeta.permissionMode"
|
|
@send="handleSend"
|
|
@switch-agent="handleAgentSwitch"
|
|
@select-session="handleSessionSelect"
|
|
@create-session="handleCreateSession"
|
|
@close-session="closeTerminal"
|
|
@start-recording="voice.startRecording()"
|
|
@stop-recording="voice.stopRecording()"
|
|
@set-voice-mode="voice.setMode($event)"
|
|
@select-microphone="voice.selectMicrophone($event)"
|
|
@play-last-audio="voice.playLastAudio()"
|
|
@update:overlay-opacity="setOverlayOpacity"
|
|
@update:input-max-lines="setInputMaxLines"
|
|
@update:scroll-jump-percent="setScrollJumpPercent"
|
|
@update:scroll-nav-mode="setScrollNavMode"
|
|
/>
|
|
|
|
<div v-else-if="!transitioning" class="empty-state">
|
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
</svg>
|
|
<span>No active terminal</span>
|
|
<small v-if="!sessionState.terminalRegistry.length">No terminals registered</small>
|
|
<small v-else>Select a terminal above to begin</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New session modal -->
|
|
<NewSessionModal
|
|
v-if="showNewSessionModal"
|
|
:agents="agents"
|
|
:default-agent="selectedAgent"
|
|
@create="handleModalCreateNew"
|
|
@close="showNewSessionModal = false"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.transcript-debug-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* ── Terminal strip ── */
|
|
|
|
.terminal-strip {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 3px 0.75rem;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border-color);
|
|
flex-shrink: 0;
|
|
gap: 0.5rem;
|
|
min-height: 32px;
|
|
}
|
|
|
|
.strip-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
}
|
|
|
|
.strip-terminal-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 3px 10px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-secondary);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
font-family: 'Courier New', monospace;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.strip-terminal-btn:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
border-color: var(--accent, #0ea5e9);
|
|
}
|
|
|
|
.strip-terminal-btn.active {
|
|
background: var(--accent, #0ea5e9);
|
|
color: white;
|
|
border-color: var(--accent, #0ea5e9);
|
|
}
|
|
|
|
.strip-terminal-btn.dead:not(.active) {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.strip-dot {
|
|
width: 5px;
|
|
height: 5px;
|
|
border-radius: 50%;
|
|
}
|
|
.strip-dot.alive { background: #22c55e; }
|
|
.strip-dot.dead { background: #ef4444; }
|
|
|
|
.strip-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.realtime-dot {
|
|
color: var(--text-muted);
|
|
opacity: 0.4;
|
|
transition: all 0.3s;
|
|
}
|
|
.realtime-dot.connected {
|
|
color: #22c55e;
|
|
opacity: 1;
|
|
animation: pulse-glow 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse-glow {
|
|
0%, 100% { filter: drop-shadow(0 0 2px currentColor); }
|
|
50% { filter: drop-shadow(0 0 6px currentColor); }
|
|
}
|
|
|
|
.strip-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 26px;
|
|
height: 26px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
.strip-btn:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
.strip-btn.active {
|
|
background: var(--accent, #0ea5e9);
|
|
color: white;
|
|
}
|
|
|
|
/* ── Error bar ── */
|
|
|
|
.error-bar {
|
|
padding: 0.5rem 1rem;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border-bottom: 1px solid rgba(239, 68, 68, 0.3);
|
|
color: #ef4444;
|
|
font-size: 13px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Content area (mirrors FloatingTranscriptDebug .content) ── */
|
|
|
|
.content-area {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
}
|
|
|
|
.readability-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ── Loading / Error overlays ── */
|
|
|
|
.loading-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 5;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(0, 0, 0, 0.35);
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 28px;
|
|
height: 28px;
|
|
border: 2.5px solid rgba(255, 255, 255, 0.15);
|
|
border-top-color: rgba(255, 255, 255, 0.7);
|
|
border-radius: 50%;
|
|
animation: tl-spin 0.7s linear infinite;
|
|
}
|
|
|
|
@keyframes tl-spin { to { transform: rotate(360deg); } }
|
|
|
|
.terminal-loading-enter-active { transition: opacity 0.15s ease; }
|
|
.terminal-loading-leave-active { transition: opacity 0.25s ease; }
|
|
.terminal-loading-enter-from,
|
|
.terminal-loading-leave-to { opacity: 0; }
|
|
|
|
.error-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 5;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(0, 0, 0, 0.45);
|
|
backdrop-filter: blur(6px);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.error-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
max-width: 80%;
|
|
padding: 1.2rem 1.5rem;
|
|
background: rgba(30, 30, 30, 0.85);
|
|
border: 1px solid rgba(248, 113, 113, 0.3);
|
|
border-radius: 10px;
|
|
}
|
|
.error-msg { font-size: 12px; color: #fca5a5; text-align: center; line-height: 1.4; word-break: break-word; }
|
|
.error-hint { font-size: 10px; color: rgba(255, 255, 255, 0.35); }
|
|
|
|
/* ── Empty state ── */
|
|
|
|
.empty-state {
|
|
position: relative;
|
|
z-index: 2;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 1;
|
|
gap: 0.75rem;
|
|
color: var(--text-muted);
|
|
}
|
|
.empty-state svg { opacity: 0.4; }
|
|
.empty-state span { font-size: 15px; color: var(--text-secondary); }
|
|
.empty-state small { font-size: 13px; }
|
|
|
|
/* ══════════════════════════════════════════════════════════════════════════════
|
|
ChatContainer glass-transparent overrides (mirrored from FloatingTranscriptDebug)
|
|
══════════════════════════════════════════════════════════════════════════════ */
|
|
|
|
.content-area :deep(.chat-container) {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
border-radius: 0 !important;
|
|
position: relative;
|
|
z-index: 1;
|
|
flex: 1 !important;
|
|
min-height: 0 !important;
|
|
}
|
|
|
|
/* Chat header: absolute overlay, floats over messages */
|
|
.content-area :deep(.chat-header) {
|
|
position: absolute !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
right: 0 !important;
|
|
z-index: 3 !important;
|
|
background: rgba(0, 6, 18, 0.5) !important;
|
|
backdrop-filter: blur(8px) !important;
|
|
-webkit-backdrop-filter: blur(8px) !important;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06) !important;
|
|
padding: 0.3rem 0.6rem !important;
|
|
transition: opacity 0.35s ease, transform 0.35s ease !important;
|
|
}
|
|
|
|
/* Hidden by default, shown only when selector-open */
|
|
.content-area:not(.selector-open) :deep(.chat-header) {
|
|
opacity: 0 !important;
|
|
transform: translateY(-150%) !important;
|
|
pointer-events: none !important;
|
|
}
|
|
|
|
/* Messages: fill entire container, pad for overlaid header / input */
|
|
.content-area :deep(.messages-scroll) {
|
|
background: transparent !important;
|
|
padding-top: 3.5rem !important;
|
|
padding-bottom: 5rem !important;
|
|
flex: 1 !important;
|
|
}
|
|
|
|
/* ── Scroll modes: hide scrollbar for buttons / none ── */
|
|
.content-area:not(.nav-scrollbar) :deep(.messages-scroll) {
|
|
scrollbar-width: none !important;
|
|
}
|
|
.content-area:not(.nav-scrollbar) :deep(.messages-scroll)::-webkit-scrollbar {
|
|
display: none !important;
|
|
}
|
|
|
|
/* ── Pixel art scrollbar (only in scrollbar mode) ── */
|
|
.content-area.nav-scrollbar :deep(.messages-scroll) {
|
|
scrollbar-gutter: stable !important;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: rgba(14, 165, 233, 0.3) rgba(12, 45, 74, 0.4);
|
|
}
|
|
|
|
.content-area.nav-scrollbar :deep(.messages-scroll)::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.content-area.nav-scrollbar :deep(.messages-scroll)::-webkit-scrollbar-track {
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='8' height='8' fill='%230c2d4a' opacity='0.4'/%3E%3Crect x='2' y='2' width='2' height='2' fill='%23075985' opacity='0.15'/%3E%3Crect x='6' y='6' width='2' height='2' fill='%23075985' opacity='0.1'/%3E%3C/svg%3E") repeat;
|
|
}
|
|
|
|
.content-area.nav-scrollbar :deep(.messages-scroll)::-webkit-scrollbar-thumb {
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='16' viewBox='0 0 8 16' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='8' height='16' fill='%230ea5e9' opacity='0.3'/%3E%3Crect x='2' y='2' width='4' height='2' fill='%2322d3ee' opacity='0.25'/%3E%3Crect x='2' y='6' width='4' height='2' fill='%2367e8f9' opacity='0.2'/%3E%3Crect x='2' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.25'/%3E%3Crect x='2' y='14' width='4' height='2' fill='%2367e8f9' opacity='0.15'/%3E%3C/svg%3E") repeat;
|
|
border: 1px solid rgba(14, 165, 233, 0.15);
|
|
}
|
|
|
|
.content-area.nav-scrollbar :deep(.messages-scroll)::-webkit-scrollbar-thumb:hover {
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='16' viewBox='0 0 8 16' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='8' height='16' fill='%230ea5e9' opacity='0.45'/%3E%3Crect x='2' y='2' width='4' height='2' fill='%2322d3ee' opacity='0.35'/%3E%3Crect x='2' y='6' width='4' height='2' fill='%2367e8f9' opacity='0.3'/%3E%3Crect x='2' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.35'/%3E%3Crect x='2' y='14' width='4' height='2' fill='%2367e8f9' opacity='0.25'/%3E%3C/svg%3E") repeat;
|
|
border-color: rgba(14, 165, 233, 0.3);
|
|
}
|
|
|
|
/* Bottom overlay: absolute container for lifecycle + input + status */
|
|
.content-area :deep(.bottom-overlay) {
|
|
position: absolute !important;
|
|
bottom: 0 !important;
|
|
left: 0 !important;
|
|
right: 0 !important;
|
|
z-index: 3 !important;
|
|
display: flex !important;
|
|
flex-direction: column !important;
|
|
background: rgba(0, 6, 18, 0.5) !important;
|
|
backdrop-filter: blur(8px) !important;
|
|
-webkit-backdrop-filter: blur(8px) !important;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.06) !important;
|
|
/* Idle: slide down but keep lifecycle ribbon visible at bottom */
|
|
transform: translateY(calc(100% - 26px));
|
|
transition: transform 0.3s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Lifecycle ribbon always interactive even when idle */
|
|
.content-area :deep(.lifecycle-ribbon) {
|
|
pointer-events: auto !important;
|
|
}
|
|
|
|
/* Show on hover anywhere in the content area or when input is focused */
|
|
.content-area:hover :deep(.bottom-overlay),
|
|
.content-area:has(:focus-within) :deep(.bottom-overlay),
|
|
.content-area :deep(.bottom-overlay:focus-within) {
|
|
transform: translateY(0);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.content-area :deep(.status-bar) {
|
|
background: transparent !important;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.04) !important;
|
|
border-bottom: none !important;
|
|
padding: 0.15rem 0.5rem !important;
|
|
}
|
|
|
|
.content-area :deep(.status-id) {
|
|
color: rgba(255,255,255,0.35) !important;
|
|
}
|
|
|
|
.content-area :deep(.status-bar .copy-id-btn) {
|
|
color: rgba(255,255,255,0.25) !important;
|
|
}
|
|
.content-area :deep(.status-bar .copy-id-btn:hover) {
|
|
color: rgba(255,255,255,0.6) !important;
|
|
}
|
|
|
|
.content-area :deep(.status-bar .meta-badge) {
|
|
border-radius: 0 !important;
|
|
font-family: 'Courier New', monospace !important;
|
|
}
|
|
|
|
.content-area :deep(.status-bar .meta-badge.model) {
|
|
background: rgba(99, 102, 241, 0.1) !important;
|
|
color: #a5b4fc !important;
|
|
}
|
|
|
|
.content-area :deep(.status-bar .meta-badge.version) {
|
|
background: rgba(255,255,255,0.04) !important;
|
|
color: rgba(255,255,255,0.3) !important;
|
|
}
|
|
|
|
.content-area :deep(.status-bar .meta-count),
|
|
.content-area :deep(.status-bar .meta-duration) {
|
|
color: rgba(255,255,255,0.2) !important;
|
|
}
|
|
|
|
/* UserInput */
|
|
.content-area :deep(.user-input) {
|
|
background: transparent !important;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.04) !important;
|
|
padding: 0.3rem 0.5rem !important;
|
|
}
|
|
|
|
/* Lifecycle ribbon */
|
|
.content-area :deep(.lifecycle-ribbon) {
|
|
background: transparent !important;
|
|
}
|
|
|
|
/* Input container */
|
|
.content-area :deep(.input-container) {
|
|
background: rgba(0, 6, 18, 0.8) !important;
|
|
border-color: rgba(14, 165, 233, 0.1) !important;
|
|
border-radius: 0 !important;
|
|
padding: 0.3rem 0.4rem !important;
|
|
}
|
|
.content-area :deep(.input-container:focus-within) {
|
|
border-color: rgba(14, 165, 233, 0.25) !important;
|
|
background: rgba(0, 6, 18, 0.85) !important;
|
|
}
|
|
|
|
.content-area :deep(.input-field) {
|
|
color: rgba(255,255,255,0.85);
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Send button: pixel art daytime ocean */
|
|
.content-area :deep(.send-btn) {
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28' shape-rendering='crispEdges'%3E%3Crect width='28' height='6' fill='%2387ceeb'/%3E%3Crect y='6' width='28' height='4' fill='%2356b3d9'/%3E%3Crect y='10' width='28' height='4' fill='%232d9abf'/%3E%3Crect y='14' width='28' height='4' fill='%231a7fa5'/%3E%3Crect y='18' width='28' height='4' fill='%23106888'/%3E%3Crect y='22' width='28' height='6' fill='%23c2b280'/%3E%3Crect x='24' y='4' width='3' height='3' fill='%23fffde0' opacity='0.8'/%3E%3Crect x='25' y='3' width='2' height='1' fill='%23fffde0' opacity='0.5'/%3E%3Crect x='2' y='5' width='4' height='2' fill='white' opacity='0.35'/%3E%3Crect x='10' y='4' width='6' height='2' fill='white' opacity='0.25'/%3E%3Crect x='20' y='6' width='3' height='1' fill='white' opacity='0.2'/%3E%3Crect x='5' y='12' width='3' height='2' fill='%23f97316' opacity='0.7'/%3E%3Crect x='4' y='13' width='1' height='1' fill='%23fdba74' opacity='0.5'/%3E%3Crect x='18' y='16' width='2' height='1' fill='%232563eb' opacity='0.5'/%3E%3Crect x='20' y='16' width='1' height='1' fill='%2393c5fd' opacity='0.4'/%3E%3Crect x='8' y='18' width='1' height='1' fill='white' opacity='0.2'/%3E%3Crect x='22' y='12' width='1' height='1' fill='white' opacity='0.2'/%3E%3Crect x='14' y='20' width='1' height='1' fill='white' opacity='0.15'/%3E%3Crect x='3' y='23' width='4' height='3' fill='%23059669' opacity='0.5'/%3E%3Crect x='4' y='22' width='2' height='1' fill='%2310b981' opacity='0.4'/%3E%3Crect x='20' y='24' width='3' height='2' fill='%23059669' opacity='0.4'/%3E%3Crect x='12' y='25' width='3' height='2' fill='%23ec4899' opacity='0.45'/%3E%3Crect x='13' y='24' width='2' height='1' fill='%23f472b6' opacity='0.35'/%3E%3C/svg%3E") !important;
|
|
border: none !important;
|
|
border-radius: 0 !important;
|
|
width: 28px !important;
|
|
height: 28px !important;
|
|
color: white !important;
|
|
image-rendering: pixelated;
|
|
box-shadow: none !important;
|
|
transition: color 0.15s ease !important;
|
|
}
|
|
.content-area :deep(.send-btn:hover:not(:disabled)) {
|
|
color: #fffde0 !important;
|
|
filter: none !important;
|
|
box-shadow: 0 0 12px rgba(135, 206, 235, 0.5), 0 0 4px rgba(255, 253, 224, 0.3) !important;
|
|
}
|
|
.content-area :deep(.send-btn:disabled) {
|
|
opacity: 0.25 !important;
|
|
filter: saturate(0.3) !important;
|
|
}
|
|
|
|
/* Meta badges */
|
|
.content-area :deep(.meta-badge) {
|
|
background: rgba(255,255,255,0.04);
|
|
color: rgba(255,255,255,0.4);
|
|
border-radius: 0;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
.content-area :deep(.meta-badge.model) {
|
|
background: rgba(99, 102, 241, 0.1);
|
|
color: #a5b4fc;
|
|
}
|
|
.content-area :deep(.meta-cwd),
|
|
.content-area :deep(.meta-duration),
|
|
.content-area :deep(.meta-count) {
|
|
color: rgba(255,255,255,0.25);
|
|
}
|
|
|
|
.content-area :deep(.copy-id-btn) {
|
|
color: rgba(255,255,255,0.25);
|
|
}
|
|
.content-area :deep(.copy-id-btn:hover) {
|
|
background: rgba(255,255,255,0.06);
|
|
color: rgba(255,255,255,0.6);
|
|
}
|
|
|
|
/* Select mode */
|
|
.content-area :deep(.select-mode-btn) {
|
|
border-color: rgba(255,255,255,0.08);
|
|
color: rgba(255,255,255,0.35);
|
|
border-radius: 0;
|
|
}
|
|
.content-area :deep(.select-mode-btn:hover) {
|
|
background: rgba(255,255,255,0.06);
|
|
color: rgba(255,255,255,0.7);
|
|
}
|
|
.content-area :deep(.select-mode-btn.active) {
|
|
background: rgba(99, 102, 241, 0.25);
|
|
border-color: rgba(99, 102, 241, 0.35);
|
|
color: #c7d2fe;
|
|
}
|
|
|
|
/* Selection bar */
|
|
.content-area :deep(.selection-bar) {
|
|
background: rgba(8, 8, 12, 0.92);
|
|
border-color: rgba(255,255,255,0.06);
|
|
border-radius: 0;
|
|
}
|
|
.content-area :deep(.selection-count) {
|
|
color: rgba(255,255,255,0.4);
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
.content-area :deep(.selection-btn.toggle-all) {
|
|
background: rgba(255,255,255,0.06);
|
|
color: rgba(255,255,255,0.5);
|
|
border-radius: 0;
|
|
}
|
|
.content-area :deep(.message-wrapper.selected) {
|
|
background: rgba(99, 102, 241, 0.06);
|
|
}
|
|
|
|
/* PiP window titlebar */
|
|
.pip-titlebar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 28px;
|
|
padding: 0 4px 0 10px;
|
|
background: transparent;
|
|
-webkit-app-region: drag;
|
|
app-region: drag;
|
|
flex-shrink: 0;
|
|
user-select: none;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
.pip-title {
|
|
font-size: 11px;
|
|
color: var(--text-muted, #666);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.pip-controls {
|
|
display: flex;
|
|
-webkit-app-region: no-drag;
|
|
app-region: no-drag;
|
|
}
|
|
|
|
.pip-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
padding: 0;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-secondary, #a1a1aa);
|
|
cursor: pointer;
|
|
-webkit-app-region: no-drag;
|
|
app-region: no-drag;
|
|
transition: background 0.1s, color 0.1s;
|
|
}
|
|
|
|
.pip-btn:hover {
|
|
background: var(--bg-hover, #1e1e28);
|
|
color: var(--text-primary, #e4e4e7);
|
|
}
|
|
|
|
.pip-close:hover {
|
|
background: #e81123;
|
|
color: #fff;
|
|
}
|
|
</style>
|