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:
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