feat: Add FloatingTranscriptDebug with pixel art dark theme
Floating chat window reusing ChatContainer with draggable/resizable window, agent/session selector overlay, and pixel art decorations (galaxy, minecraft dirt block, LED strip) on black transparent backdrop.
This commit is contained in:
@@ -6,6 +6,7 @@ import TorchButton from './components/TorchButton.vue'
|
|||||||
import FloatingTerminal from './components/FloatingTerminal.vue'
|
import FloatingTerminal from './components/FloatingTerminal.vue'
|
||||||
import FloatingResponse from './components/FloatingResponse.vue'
|
import FloatingResponse from './components/FloatingResponse.vue'
|
||||||
import FloatingVoice from './components/FloatingVoice.vue'
|
import FloatingVoice from './components/FloatingVoice.vue'
|
||||||
|
import FloatingTranscriptDebug from './components/FloatingTranscriptDebug.vue'
|
||||||
import AgentBar from './components/AgentBar.vue'
|
import AgentBar from './components/AgentBar.vue'
|
||||||
import PwaInstallBanner from './components/PwaInstallBanner.vue'
|
import PwaInstallBanner from './components/PwaInstallBanner.vue'
|
||||||
import HooksApprovalModal from './components/HooksApprovalModal.vue'
|
import HooksApprovalModal from './components/HooksApprovalModal.vue'
|
||||||
@@ -23,6 +24,7 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const showTerminal = ref(false)
|
const showTerminal = ref(false)
|
||||||
const showVoice = ref(false)
|
const showVoice = ref(false)
|
||||||
|
const showTranscriptDebug = ref(false)
|
||||||
const showDebugConsole = ref(false)
|
const showDebugConsole = ref(false)
|
||||||
const toolbarVisible = ref(true)
|
const toolbarVisible = ref(true)
|
||||||
const forceWco = ref(false)
|
const forceWco = ref(false)
|
||||||
@@ -450,7 +452,7 @@ watch(() => route.name, (newPage) => {
|
|||||||
'session-start': showSessionStart,
|
'session-start': showSessionStart,
|
||||||
notification: showNotification,
|
notification: showNotification,
|
||||||
'tool-flash': showToolFlash,
|
'tool-flash': showToolFlash,
|
||||||
'sheet-open': showTerminal || showVoice,
|
'sheet-open': showTerminal || showVoice || showTranscriptDebug,
|
||||||
'keyboard-visible': keyboardVisible
|
'keyboard-visible': keyboardVisible
|
||||||
}"
|
}"
|
||||||
@click="showTerminal = !showTerminal"
|
@click="showTerminal = !showTerminal"
|
||||||
@@ -521,10 +523,22 @@ watch(() => route.name, (newPage) => {
|
|||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Transcript Debug FAB Button -->
|
||||||
|
<button
|
||||||
|
class="transcript-fab"
|
||||||
|
:class="{ active: showTranscriptDebug, 'sheet-open': showTerminal || showVoice || showTranscriptDebug, 'keyboard-visible': keyboardVisible }"
|
||||||
|
@click="showTranscriptDebug = !showTranscriptDebug"
|
||||||
|
title="Transcript Debug"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Voice FAB Button -->
|
<!-- Voice FAB Button -->
|
||||||
<button
|
<button
|
||||||
class="voice-fab"
|
class="voice-fab"
|
||||||
:class="{ active: showVoice, 'sheet-open': showTerminal || showVoice, 'ptt-active': voicePTTActive, 'keyboard-visible': keyboardVisible }"
|
:class="{ active: showVoice, 'sheet-open': showTerminal || showVoice || showTranscriptDebug, 'ptt-active': voicePTTActive, 'keyboard-visible': keyboardVisible }"
|
||||||
@click="handleVoiceFabClick"
|
@click="handleVoiceFabClick"
|
||||||
@touchstart="handleVoiceFabTouchStart"
|
@touchstart="handleVoiceFabTouchStart"
|
||||||
@touchend="handleVoiceFabTouchEnd"
|
@touchend="handleVoiceFabTouchEnd"
|
||||||
@@ -552,6 +566,9 @@ watch(() => route.name, (newPage) => {
|
|||||||
<!-- Floating Voice Input -->
|
<!-- Floating Voice Input -->
|
||||||
<FloatingVoice ref="voiceRef" v-model="showVoice" />
|
<FloatingVoice ref="voiceRef" v-model="showVoice" />
|
||||||
|
|
||||||
|
<!-- Floating Transcript Debug -->
|
||||||
|
<FloatingTranscriptDebug v-model="showTranscriptDebug" />
|
||||||
|
|
||||||
<!-- Global Hooks Approval Modal -->
|
<!-- Global Hooks Approval Modal -->
|
||||||
<HooksApprovalModal />
|
<HooksApprovalModal />
|
||||||
|
|
||||||
@@ -1309,6 +1326,49 @@ watch(() => route.name, (newPage) => {
|
|||||||
50% { box-shadow: 0 0 50px rgba(249, 115, 22, 0.9); }
|
50% { box-shadow: 0 0 50px rgba(249, 115, 22, 0.9); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Transcript Debug FAB */
|
||||||
|
.transcript-fab {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 80px;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(15, 15, 20, 0.85);
|
||||||
|
color: #818cf8;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
z-index: 9998;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
touch-action: manipulation;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-fab:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(99, 102, 241, 0.4);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5), 0 0 12px rgba(99, 102, 241, 0.15);
|
||||||
|
color: #a5b4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-fab.active {
|
||||||
|
background: rgba(99, 102, 241, 0.15);
|
||||||
|
border-color: rgba(99, 102, 241, 0.4);
|
||||||
|
color: #c7d2fe;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 0 8px rgba(99, 102, 241, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-fab.active:hover {
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5), 0 0 12px rgba(99, 102, 241, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.terminal-fab {
|
.terminal-fab {
|
||||||
bottom: 80px;
|
bottom: 80px;
|
||||||
@@ -1340,28 +1400,39 @@ watch(() => route.name, (newPage) => {
|
|||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transcript-fab {
|
||||||
|
bottom: 80px;
|
||||||
|
left: 68px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile: FABs above bottom sheets */
|
/* Mobile: FABs above bottom sheets */
|
||||||
@media (max-width: 1024px) and (pointer: coarse) {
|
@media (max-width: 1024px) and (pointer: coarse) {
|
||||||
.terminal-fab,
|
.terminal-fab,
|
||||||
.voice-fab {
|
.voice-fab,
|
||||||
|
.transcript-fab {
|
||||||
z-index: 10001;
|
z-index: 10001;
|
||||||
transition: bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s ease;
|
transition: bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-fab.sheet-open,
|
.terminal-fab.sheet-open,
|
||||||
.voice-fab.sheet-open {
|
.voice-fab.sheet-open,
|
||||||
|
.transcript-fab.sheet-open {
|
||||||
bottom: calc(15vh + 100px);
|
bottom: calc(15vh + 100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-fab.keyboard-visible,
|
.terminal-fab.keyboard-visible,
|
||||||
.voice-fab.keyboard-visible {
|
.voice-fab.keyboard-visible,
|
||||||
|
.transcript-fab.keyboard-visible {
|
||||||
bottom: 35vh;
|
bottom: 35vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-fab.keyboard-visible.sheet-open,
|
.terminal-fab.keyboard-visible.sheet-open,
|
||||||
.voice-fab.keyboard-visible.sheet-open {
|
.voice-fab.keyboard-visible.sheet-open,
|
||||||
|
.transcript-fab.keyboard-visible.sheet-open {
|
||||||
bottom: 45vh;
|
bottom: 45vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
928
frontend/src/components/FloatingTranscriptDebug.vue
Normal file
928
frontend/src/components/FloatingTranscriptDebug.vue
Normal file
@@ -0,0 +1,928 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
|
import { useTranscriptDebug } from '@/composables/transcript-debug'
|
||||||
|
import { ChatContainer } from '@/components/transcript-debug'
|
||||||
|
import type { AgentName } from '@/types/transcript-debug'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isOpen = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TRANSCRIPT DEBUG STATE (composable)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedAgent,
|
||||||
|
sessions,
|
||||||
|
selectedSessionId,
|
||||||
|
conversation,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
isRealtime,
|
||||||
|
processing,
|
||||||
|
init,
|
||||||
|
switchAgent,
|
||||||
|
selectSession,
|
||||||
|
disconnectRealtime,
|
||||||
|
sendPrompt
|
||||||
|
} = useTranscriptDebug()
|
||||||
|
|
||||||
|
const agents: { id: AgentName; label: string }[] = [
|
||||||
|
{ id: 'ejecutor', label: 'Ejecutor' },
|
||||||
|
{ id: 'nucleo000', label: 'nucleo000' },
|
||||||
|
{ id: 'claude', label: 'Claude' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const showSelector = ref(false)
|
||||||
|
let initialized = false
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DRAG STATE
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const windowRef = ref<HTMLElement | null>(null)
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const position = ref({ x: 0, y: 0 })
|
||||||
|
const hasCustomPosition = ref(false)
|
||||||
|
const dragOffset = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
// Resize state
|
||||||
|
const isResizing = ref(false)
|
||||||
|
const size = ref({ w: 480, h: 600 })
|
||||||
|
|
||||||
|
// Mobile bottom sheet state
|
||||||
|
const isMobile = ref(false)
|
||||||
|
const sheetHeight = ref(55)
|
||||||
|
const isDraggingSheet = ref(false)
|
||||||
|
const sheetDragStart = ref({ y: 0, height: 0 })
|
||||||
|
const snapPoints = [20, 55, 85]
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MOBILE DETECTION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function checkMobile() {
|
||||||
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0
|
||||||
|
const isSmallScreen = window.innerWidth <= 1024
|
||||||
|
isMobile.value = isTouchDevice && isSmallScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNearestSnap(height: number): number {
|
||||||
|
return snapPoints.reduce((prev, curr) =>
|
||||||
|
Math.abs(curr - height) < Math.abs(prev - height) ? curr : prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MOBILE SHEET DRAG
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function startSheetDrag(e: TouchEvent) {
|
||||||
|
if (!isMobile.value) return
|
||||||
|
const touch = e.touches[0]
|
||||||
|
if (!touch) return
|
||||||
|
isDraggingSheet.value = true
|
||||||
|
sheetDragStart.value = { y: touch.clientY, height: sheetHeight.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSheetDrag(e: TouchEvent) {
|
||||||
|
if (!isDraggingSheet.value || !isMobile.value) return
|
||||||
|
const touch = e.touches[0]
|
||||||
|
if (!touch) return
|
||||||
|
const deltaY = sheetDragStart.value.y - touch.clientY
|
||||||
|
const deltaPercent = (deltaY / window.innerHeight) * 100
|
||||||
|
sheetHeight.value = Math.max(15, Math.min(90, sheetDragStart.value.height + deltaPercent))
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSheetDrag() {
|
||||||
|
if (!isDraggingSheet.value) return
|
||||||
|
isDraggingSheet.value = false
|
||||||
|
sheetHeight.value = findNearestSnap(sheetHeight.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WINDOW DRAG
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function startDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
if ((e.target as HTMLElement).closest('.window-controls')) return
|
||||||
|
if ((e.target as HTMLElement).closest('.selector-overlay')) return
|
||||||
|
|
||||||
|
if (isMobile.value) {
|
||||||
|
if (e instanceof TouchEvent) startSheetDrag(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isDragging.value = true
|
||||||
|
const rect = windowRef.value?.getBoundingClientRect()
|
||||||
|
const touch = e instanceof TouchEvent ? e.touches[0] : null
|
||||||
|
const clientX = e instanceof MouseEvent ? e.clientX : (touch?.clientX ?? 0)
|
||||||
|
const clientY = e instanceof MouseEvent ? e.clientY : (touch?.clientY ?? 0)
|
||||||
|
|
||||||
|
if (rect) {
|
||||||
|
if (!hasCustomPosition.value) {
|
||||||
|
position.value = { x: rect.left, y: rect.top }
|
||||||
|
}
|
||||||
|
dragOffset.value = { x: clientX - rect.left, y: clientY - rect.top }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onDrag)
|
||||||
|
document.addEventListener('mouseup', stopDrag)
|
||||||
|
document.addEventListener('touchmove', onDrag, { passive: false })
|
||||||
|
document.addEventListener('touchend', stopDrag)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
if (e instanceof TouchEvent) e.preventDefault()
|
||||||
|
|
||||||
|
const touch = e instanceof TouchEvent ? e.touches[0] : null
|
||||||
|
const clientX = e instanceof MouseEvent ? e.clientX : (touch?.clientX ?? 0)
|
||||||
|
const clientY = e instanceof MouseEvent ? e.clientY : (touch?.clientY ?? 0)
|
||||||
|
|
||||||
|
const w = windowRef.value?.offsetWidth || 480
|
||||||
|
const h = windowRef.value?.offsetHeight || 600
|
||||||
|
const minX = -w * 0.75
|
||||||
|
const maxX = window.innerWidth - w * 0.25
|
||||||
|
const minY = -h * 0.75
|
||||||
|
const maxY = window.innerHeight - h * 0.25
|
||||||
|
|
||||||
|
position.value = {
|
||||||
|
x: Math.max(minX, Math.min(clientX - dragOffset.value.x, maxX)),
|
||||||
|
y: Math.max(minY, Math.min(clientY - dragOffset.value.y, maxY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrag() {
|
||||||
|
isDragging.value = false
|
||||||
|
hasCustomPosition.value = true
|
||||||
|
document.removeEventListener('mousemove', onDrag)
|
||||||
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
|
document.removeEventListener('touchmove', onDrag)
|
||||||
|
document.removeEventListener('touchend', stopDrag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WINDOW RESIZE
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const resizeStart = ref({ x: 0, y: 0, w: 0, h: 0 })
|
||||||
|
|
||||||
|
function startResize(e: MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
isResizing.value = true
|
||||||
|
resizeStart.value = { x: e.clientX, y: e.clientY, w: size.value.w, h: size.value.h }
|
||||||
|
document.addEventListener('mousemove', onResize)
|
||||||
|
document.addEventListener('mouseup', stopResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResize(e: MouseEvent) {
|
||||||
|
if (!isResizing.value) return
|
||||||
|
size.value = {
|
||||||
|
w: Math.max(360, Math.min(resizeStart.value.w + e.clientX - resizeStart.value.x, window.innerWidth - 40)),
|
||||||
|
h: Math.max(300, Math.min(resizeStart.value.h + e.clientY - resizeStart.value.y, window.innerHeight - 40))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopResize() {
|
||||||
|
isResizing.value = false
|
||||||
|
document.removeEventListener('mousemove', onResize)
|
||||||
|
document.removeEventListener('mouseup', stopResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// COMPUTED STYLE
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const windowStyle = computed((): Record<string, string> => {
|
||||||
|
if (isMobile.value) {
|
||||||
|
return {
|
||||||
|
position: 'fixed',
|
||||||
|
left: '0',
|
||||||
|
right: '0',
|
||||||
|
bottom: '0',
|
||||||
|
width: '100%',
|
||||||
|
height: `${sheetHeight.value}vh`,
|
||||||
|
maxHeight: '90vh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasCustomPosition.value) {
|
||||||
|
return {
|
||||||
|
width: `${size.value.w}px`,
|
||||||
|
height: `${size.value.h}px`,
|
||||||
|
bottom: '16px',
|
||||||
|
left: '90px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: `${size.value.w}px`,
|
||||||
|
height: `${size.value.h}px`,
|
||||||
|
top: `${position.value.y}px`,
|
||||||
|
left: `${position.value.x}px`,
|
||||||
|
bottom: 'auto',
|
||||||
|
right: 'auto'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ACTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
isOpen.value = false
|
||||||
|
showSelector.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAgentSwitch(agent: AgentName) {
|
||||||
|
switchAgent(agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSessionSelect(sessionId: string) {
|
||||||
|
selectSession(sessionId)
|
||||||
|
showSelector.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSend(message: string) {
|
||||||
|
sendPrompt(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WATCHERS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
watch(isOpen, async (open) => {
|
||||||
|
if (open && !initialized) {
|
||||||
|
initialized = true
|
||||||
|
await nextTick()
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// LIFECYCLE
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkMobile()
|
||||||
|
window.addEventListener('resize', checkMobile)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
disconnectRealtime()
|
||||||
|
document.removeEventListener('mousemove', onDrag)
|
||||||
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
|
document.removeEventListener('touchmove', onDrag)
|
||||||
|
document.removeEventListener('touchend', stopDrag)
|
||||||
|
document.removeEventListener('mousemove', onResize)
|
||||||
|
document.removeEventListener('mouseup', stopResize)
|
||||||
|
window.removeEventListener('resize', checkMobile)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="win-slide">
|
||||||
|
<div
|
||||||
|
v-show="isOpen"
|
||||||
|
ref="windowRef"
|
||||||
|
class="aero-win"
|
||||||
|
:class="{
|
||||||
|
dragging: isDragging,
|
||||||
|
resizing: isResizing,
|
||||||
|
mobile: isMobile,
|
||||||
|
'sheet-dragging': isDraggingSheet
|
||||||
|
}"
|
||||||
|
:style="windowStyle"
|
||||||
|
>
|
||||||
|
<div class="glass">
|
||||||
|
<!-- Mobile drag handle -->
|
||||||
|
<div
|
||||||
|
v-if="isMobile"
|
||||||
|
class="sheet-handle"
|
||||||
|
@touchstart.passive="startSheetDrag"
|
||||||
|
@touchmove.prevent="onSheetDrag"
|
||||||
|
@touchend="stopSheetDrag"
|
||||||
|
>
|
||||||
|
<div class="handle-bar"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Titlebar -->
|
||||||
|
<div
|
||||||
|
class="titlebar"
|
||||||
|
@mousedown="startDrag"
|
||||||
|
@touchstart.passive="startDrag"
|
||||||
|
@touchmove="onSheetDrag"
|
||||||
|
@touchend="stopSheetDrag"
|
||||||
|
>
|
||||||
|
<div class="left">
|
||||||
|
<svg class="title-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<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 class="title-label">Transcript</span>
|
||||||
|
<i class="dot" :class="{ on: isRealtime }"></i>
|
||||||
|
<span v-if="selectedAgent" class="agent-badge">{{ selectedAgent }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="window-controls">
|
||||||
|
<button @click.stop="showSelector = !showSelector" :class="{ active: showSelector }" title="Agent/Session">
|
||||||
|
<svg width="10" height="10" 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>
|
||||||
|
<button class="x" @click="close" title="Close">
|
||||||
|
<svg width="8" height="8" viewBox="0 0 10 10">
|
||||||
|
<line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
<line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Agent/Session Selector Overlay -->
|
||||||
|
<Transition name="selector-slide">
|
||||||
|
<div v-if="showSelector" class="selector-overlay" @click.self="showSelector = false">
|
||||||
|
<div class="selector-panel">
|
||||||
|
<div class="selector-section">
|
||||||
|
<label class="selector-label">Agent</label>
|
||||||
|
<div class="agent-selector">
|
||||||
|
<button
|
||||||
|
v-for="a in agents"
|
||||||
|
:key="a.id"
|
||||||
|
:class="['agent-btn', { active: selectedAgent === a.id }]"
|
||||||
|
@click="handleAgentSwitch(a.id)"
|
||||||
|
>
|
||||||
|
{{ a.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="selector-section">
|
||||||
|
<label class="selector-label">Session</label>
|
||||||
|
<select
|
||||||
|
class="session-select"
|
||||||
|
:value="selectedSessionId || ''"
|
||||||
|
@change="handleSessionSelect(($event.target as HTMLSelectElement).value)"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select session...</option>
|
||||||
|
<option v-for="s in sessions" :key="s.id" :value="s.id">
|
||||||
|
{{ s.firstUserMessage ? (s.firstUserMessage.length > 50 ? s.firstUserMessage.slice(0, 50) + '...' : s.firstUserMessage) : s.id.slice(0, 8) + '...' }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<span v-if="loading" class="spinner-sm"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<div v-if="error" class="error-bar">{{ error }}</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="content">
|
||||||
|
<ChatContainer
|
||||||
|
v-if="conversation"
|
||||||
|
:conversation="conversation"
|
||||||
|
:processing="processing"
|
||||||
|
@send="handleSend"
|
||||||
|
/>
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<svg width="32" height="32" 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>Select a session to begin</span>
|
||||||
|
<small>{{ sessions.length }} sessions available</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resize handle -->
|
||||||
|
<div v-if="!isMobile" class="resize-handle" @mousedown="startResize"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.aero-win {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 360px;
|
||||||
|
min-height: 300px;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgba(0, 0, 0, 0.78);
|
||||||
|
backdrop-filter: blur(16px) saturate(1.2);
|
||||||
|
-webkit-backdrop-filter: blur(16px) saturate(1.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(0,0,0,0.6),
|
||||||
|
0 16px 56px rgba(0,0,0,0.6),
|
||||||
|
0 4px 16px rgba(0,0,0,0.4);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Titlebar ── */
|
||||||
|
.titlebar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 5px 0 8px;
|
||||||
|
background:
|
||||||
|
/* Pixel art: Minecraft-style dirt/grass block */
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' shape-rendering='crispEdges'%3E%3Crect width='24' height='24' fill='%23866043' opacity='0.35'/%3E%3Crect y='0' width='24' height='4' fill='%2355a630' opacity='0.45'/%3E%3Crect x='0' y='4' width='2' height='2' fill='%2355a630' opacity='0.35'/%3E%3Crect x='6' y='4' width='4' height='2' fill='%2355a630' opacity='0.3'/%3E%3Crect x='16' y='4' width='2' height='2' fill='%2355a630' opacity='0.35'/%3E%3Crect x='20' y='4' width='4' height='2' fill='%2355a630' opacity='0.25'/%3E%3Crect x='2' y='2' width='2' height='2' fill='%2367c23a' opacity='0.3'/%3E%3Crect x='8' y='0' width='2' height='2' fill='%2367c23a' opacity='0.35'/%3E%3Crect x='14' y='2' width='4' height='2' fill='%2367c23a' opacity='0.25'/%3E%3Crect x='4' y='8' width='2' height='2' fill='%23724b32' opacity='0.3'/%3E%3Crect x='10' y='10' width='4' height='2' fill='%23966b4a' opacity='0.25'/%3E%3Crect x='18' y='8' width='2' height='4' fill='%23724b32' opacity='0.25'/%3E%3Crect x='2' y='14' width='2' height='2' fill='%23966b4a' opacity='0.2'/%3E%3Crect x='12' y='16' width='2' height='2' fill='%23724b32' opacity='0.25'/%3E%3Crect x='8' y='20' width='4' height='2' fill='%23966b4a' opacity='0.2'/%3E%3Crect x='20' y='18' width='2' height='2' fill='%23724b32' opacity='0.2'/%3E%3C/svg%3E") no-repeat calc(100% - 6px) center / 22px 22px,
|
||||||
|
rgba(255,255,255,0.02);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aero-win.dragging .titlebar { cursor: grabbing; }
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
font: 500 10px/1 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
opacity: 0.4;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-label {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #818cf8;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
/* No gradient - pixel art aesthetic = flat color */
|
||||||
|
-webkit-text-fill-color: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.on {
|
||||||
|
background: #22c55e;
|
||||||
|
box-shadow: 0 0 6px #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-badge {
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 0;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||||
|
background: rgba(99, 102, 241, 0.12);
|
||||||
|
color: #a5b4fc;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button {
|
||||||
|
width: 20px;
|
||||||
|
height: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid rgba(255,255,255,0.06);
|
||||||
|
border-radius: 0;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button.active {
|
||||||
|
background: rgba(99, 102, 241, 0.2);
|
||||||
|
border-color: rgba(99, 102, 241, 0.3);
|
||||||
|
color: #a5b4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button.x:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.3);
|
||||||
|
border-color: rgba(239, 68, 68, 0.4);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Selector Overlay ── */
|
||||||
|
.selector-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 28px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background: rgba(8, 8, 12, 0.92);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-label {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
min-width: 48px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-btn:not(:last-child) {
|
||||||
|
border-right: 1px solid rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
color: rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-btn.active {
|
||||||
|
background: rgba(99, 102, 241, 0.35);
|
||||||
|
color: #c7d2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 0;
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(99, 102, 241, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-select option {
|
||||||
|
background: #0a0a10;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-sm {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(255,255,255,0.1);
|
||||||
|
border-top-color: #6366f1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Error Bar ── */
|
||||||
|
.error-bar {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
color: #fca5a5;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Content ── */
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
/* Pixel art galaxy: spiral arms, stars, nebula in bottom-right corner */
|
||||||
|
background:
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120' shape-rendering='crispEdges'%3E%3C!-- galaxy core --%3E%3Crect x='56' y='56' width='4' height='4' fill='%23fef3c7' opacity='0.35'/%3E%3Crect x='60' y='56' width='4' height='4' fill='%23fde68a' opacity='0.3'/%3E%3Crect x='56' y='60' width='4' height='4' fill='%23fde68a' opacity='0.28'/%3E%3Crect x='60' y='60' width='4' height='4' fill='%23fef9c3' opacity='0.4'/%3E%3Crect x='52' y='56' width='4' height='4' fill='%23c4b5fd' opacity='0.2'/%3E%3Crect x='64' y='56' width='4' height='4' fill='%23a78bfa' opacity='0.18'/%3E%3Crect x='56' y='52' width='4' height='4' fill='%23818cf8' opacity='0.2'/%3E%3Crect x='60' y='64' width='4' height='4' fill='%23c4b5fd' opacity='0.18'/%3E%3C!-- spiral arm top-right --%3E%3Crect x='68' y='48' width='4' height='4' fill='%236366f1' opacity='0.18'/%3E%3Crect x='72' y='44' width='4' height='4' fill='%23818cf8' opacity='0.14'/%3E%3Crect x='76' y='40' width='4' height='4' fill='%236366f1' opacity='0.12'/%3E%3Crect x='80' y='38' width='4' height='2' fill='%23818cf8' opacity='0.1'/%3E%3Crect x='84' y='36' width='4' height='2' fill='%236366f1' opacity='0.08'/%3E%3Crect x='88' y='36' width='2' height='2' fill='%23a78bfa' opacity='0.06'/%3E%3C!-- spiral arm bottom-left --%3E%3Crect x='48' y='68' width='4' height='4' fill='%23818cf8' opacity='0.16'/%3E%3Crect x='44' y='72' width='4' height='4' fill='%236366f1' opacity='0.14'/%3E%3Crect x='40' y='76' width='4' height='4' fill='%23818cf8' opacity='0.12'/%3E%3Crect x='36' y='78' width='4' height='2' fill='%236366f1' opacity='0.1'/%3E%3Crect x='32' y='80' width='4' height='2' fill='%23a78bfa' opacity='0.07'/%3E%3C!-- spiral arm top-left --%3E%3Crect x='48' y='48' width='4' height='4' fill='%23c084fc' opacity='0.15'/%3E%3Crect x='44' y='44' width='4' height='4' fill='%23a855f7' opacity='0.12'/%3E%3Crect x='40' y='40' width='4' height='4' fill='%23c084fc' opacity='0.1'/%3E%3Crect x='36' y='38' width='4' height='2' fill='%23a855f7' opacity='0.08'/%3E%3C!-- spiral arm bottom-right --%3E%3Crect x='68' y='68' width='4' height='4' fill='%23a855f7' opacity='0.14'/%3E%3Crect x='72' y='72' width='4' height='4' fill='%23c084fc' opacity='0.12'/%3E%3Crect x='76' y='76' width='4' height='4' fill='%23a855f7' opacity='0.1'/%3E%3Crect x='80' y='78' width='4' height='2' fill='%23c084fc' opacity='0.07'/%3E%3C!-- nebula clouds --%3E%3Crect x='50' y='40' width='8' height='2' fill='%23f0abfc' opacity='0.07'/%3E%3Crect x='66' y='62' width='6' height='2' fill='%2367e8f9' opacity='0.06'/%3E%3Crect x='42' y='64' width='6' height='2' fill='%23f0abfc' opacity='0.05'/%3E%3Crect x='64' y='44' width='4' height='2' fill='%2367e8f9' opacity='0.06'/%3E%3C!-- scattered stars --%3E%3Crect x='20' y='18' width='2' height='2' fill='white' opacity='0.2'/%3E%3Crect x='95' y='22' width='2' height='2' fill='white' opacity='0.15'/%3E%3Crect x='14' y='90' width='2' height='2' fill='white' opacity='0.12'/%3E%3Crect x='100' y='88' width='2' height='2' fill='white' opacity='0.18'/%3E%3Crect x='30' y='105' width='2' height='2' fill='%23c4b5fd' opacity='0.1'/%3E%3Crect x='108' y='14' width='2' height='2' fill='%23fde68a' opacity='0.12'/%3E%3Crect x='10' y='50' width='2' height='2' fill='white' opacity='0.1'/%3E%3Crect x='110' y='60' width='2' height='2' fill='%2367e8f9' opacity='0.12'/%3E%3Crect x='70' y='20' width='2' height='2' fill='white' opacity='0.08'/%3E%3Crect x='50' y='100' width='2' height='2' fill='white' opacity='0.1'/%3E%3Crect x='85' y='105' width='2' height='2' fill='%23fde68a' opacity='0.08'/%3E%3Crect x='25' y='30' width='2' height='2' fill='%23c4b5fd' opacity='0.08'/%3E%3C/svg%3E") no-repeat center center / 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override ChatContainer backgrounds for black transparency */
|
||||||
|
.content :deep(.chat-container) {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.chat-header) {
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.messages-scroll) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.chat-title-row) {
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.session-id) {
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :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 :deep(.meta-badge.model) {
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
color: #a5b4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.meta-cwd),
|
||||||
|
.content :deep(.meta-duration),
|
||||||
|
.content :deep(.meta-count) {
|
||||||
|
color: rgba(255,255,255,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.copy-id-btn) {
|
||||||
|
color: rgba(255,255,255,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.copy-id-btn:hover) {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.select-mode-btn) {
|
||||||
|
border-color: rgba(255,255,255,0.08);
|
||||||
|
color: rgba(255,255,255,0.35);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.select-mode-btn:hover) {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
color: rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.select-mode-btn.active) {
|
||||||
|
background: rgba(99, 102, 241, 0.25);
|
||||||
|
border-color: rgba(99, 102, 241, 0.35);
|
||||||
|
color: #c7d2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* UserInput dark overrides with LED strip pixel art */
|
||||||
|
.content :deep(.user-input-area),
|
||||||
|
.content :deep(.input-wrapper) {
|
||||||
|
background:
|
||||||
|
/* Pixel LED strip: row of colored square LEDs */
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='6' viewBox='0 0 200 6' shape-rendering='crispEdges'%3E%3Crect x='4' y='1' width='4' height='4' fill='%23ef4444' opacity='0.25'/%3E%3Crect x='12' y='1' width='4' height='4' fill='%23f97316' opacity='0.22'/%3E%3Crect x='20' y='1' width='4' height='4' fill='%23eab308' opacity='0.25'/%3E%3Crect x='28' y='1' width='4' height='4' fill='%2322c55e' opacity='0.25'/%3E%3Crect x='36' y='1' width='4' height='4' fill='%2306b6d4' opacity='0.22'/%3E%3Crect x='44' y='1' width='4' height='4' fill='%236366f1' opacity='0.25'/%3E%3Crect x='52' y='1' width='4' height='4' fill='%23a855f7' opacity='0.22'/%3E%3Crect x='60' y='1' width='4' height='4' fill='%23ec4899' opacity='0.2'/%3E%3Crect x='68' y='1' width='4' height='4' fill='%23ef4444' opacity='0.18'/%3E%3Crect x='76' y='1' width='4' height='4' fill='%23f97316' opacity='0.2'/%3E%3Crect x='84' y='1' width='4' height='4' fill='%23eab308' opacity='0.22'/%3E%3Crect x='92' y='1' width='4' height='4' fill='%2322c55e' opacity='0.2'/%3E%3Crect x='100' y='1' width='4' height='4' fill='%2306b6d4' opacity='0.18'/%3E%3Crect x='108' y='1' width='4' height='4' fill='%236366f1' opacity='0.22'/%3E%3Crect x='116' y='1' width='4' height='4' fill='%23a855f7' opacity='0.2'/%3E%3Crect x='124' y='1' width='4' height='4' fill='%23ec4899' opacity='0.18'/%3E%3Crect x='132' y='1' width='4' height='4' fill='%23ef4444' opacity='0.15'/%3E%3Crect x='140' y='1' width='4' height='4' fill='%23f97316' opacity='0.18'/%3E%3Crect x='148' y='1' width='4' height='4' fill='%23eab308' opacity='0.2'/%3E%3Crect x='156' y='1' width='4' height='4' fill='%2322c55e' opacity='0.18'/%3E%3Crect x='164' y='1' width='4' height='4' fill='%2306b6d4' opacity='0.15'/%3E%3Crect x='172' y='1' width='4' height='4' fill='%236366f1' opacity='0.18'/%3E%3Crect x='180' y='1' width='4' height='4' fill='%23a855f7' opacity='0.15'/%3E%3Crect x='188' y='1' width='4' height='4' fill='%23ec4899' opacity='0.12'/%3E%3C/svg%3E") repeat-x left top;
|
||||||
|
border-top-color: rgba(255,255,255,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.user-input-area textarea),
|
||||||
|
.content :deep(.input-wrapper textarea) {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-color: rgba(255,255,255,0.06);
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
border-radius: 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.user-input-area textarea:focus),
|
||||||
|
.content :deep(.input-wrapper textarea:focus) {
|
||||||
|
border-color: rgba(99, 102, 241, 0.3);
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection bar */
|
||||||
|
.content :deep(.selection-bar) {
|
||||||
|
background: rgba(8, 8, 12, 0.92);
|
||||||
|
border-color: rgba(255,255,255,0.06);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.selection-count) {
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.selection-btn.toggle-all) {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(.message-wrapper.selected) {
|
||||||
|
background: rgba(99, 102, 241, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Empty State ── */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
gap: 8px;
|
||||||
|
color: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state svg {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state span {
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: rgba(255,255,255,0.35);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state small {
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Resize Handle ── */
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
background:
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' shape-rendering='crispEdges'%3E%3Crect x='12' y='12' width='2' height='2' fill='%23818cf8' opacity='0.25'/%3E%3Crect x='8' y='12' width='2' height='2' fill='%23818cf8' opacity='0.15'/%3E%3Crect x='12' y='8' width='2' height='2' fill='%23818cf8' opacity='0.15'/%3E%3Crect x='4' y='12' width='2' height='2' fill='%23818cf8' opacity='0.08'/%3E%3Crect x='12' y='4' width='2' height='2' fill='%23818cf8' opacity='0.08'/%3E%3Crect x='8' y='8' width='2' height='2' fill='%23818cf8' opacity='0.08'/%3E%3C/svg%3E") no-repeat center center;
|
||||||
|
border-radius: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover {
|
||||||
|
background:
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' shape-rendering='crispEdges'%3E%3Crect x='12' y='12' width='2' height='2' fill='%23a5b4fc' opacity='0.4'/%3E%3Crect x='8' y='12' width='2' height='2' fill='%23a5b4fc' opacity='0.25'/%3E%3Crect x='12' y='8' width='2' height='2' fill='%23a5b4fc' opacity='0.25'/%3E%3Crect x='4' y='12' width='2' height='2' fill='%23a5b4fc' opacity='0.15'/%3E%3Crect x='12' y='4' width='2' height='2' fill='%23a5b4fc' opacity='0.15'/%3E%3Crect x='8' y='8' width='2' height='2' fill='%23a5b4fc' opacity='0.15'/%3E%3C/svg%3E") no-repeat center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aero-win.resizing {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aero-win.resizing .content {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Transitions ── */
|
||||||
|
.win-slide-enter-active,
|
||||||
|
.win-slide-leave-active {
|
||||||
|
transition: all .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win-slide-enter-from,
|
||||||
|
.win-slide-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-slide-enter-active,
|
||||||
|
.selector-slide-leave-active {
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-slide-enter-from,
|
||||||
|
.selector-slide-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Mobile ── */
|
||||||
|
.aero-win.mobile {
|
||||||
|
min-width: unset;
|
||||||
|
min-height: unset;
|
||||||
|
max-width: 100%;
|
||||||
|
transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aero-win.mobile.sheet-dragging {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aero-win.mobile .glass {
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-handle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 24px;
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-bar {
|
||||||
|
width: 36px;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255,255,255,0.12);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aero-win.mobile .titlebar {
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aero-win.mobile .resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aero-win.mobile .content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 100px;
|
||||||
|
touch-action: pan-y;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aero-win.mobile.win-slide-enter-from,
|
||||||
|
.aero-win.mobile.win-slide-leave-to {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user