diff --git a/frontend/src/components/FloatingTranscriptDebug.vue b/frontend/src/components/FloatingTranscriptDebug.vue index 437ddcf..4c5bb11 100644 --- a/frontend/src/components/FloatingTranscriptDebug.vue +++ b/frontend/src/components/FloatingTranscriptDebug.vue @@ -1,7 +1,7 @@ + + + + diff --git a/frontend/src/components/transcript-debug/ToolResultBlock.vue b/frontend/src/components/transcript-debug/ToolResultBlock.vue index 4fcee36..aadcdb0 100644 --- a/frontend/src/components/transcript-debug/ToolResultBlock.vue +++ b/frontend/src/components/transcript-debug/ToolResultBlock.vue @@ -1,43 +1,36 @@ - - diff --git a/frontend/src/components/transcript-debug/aquaticBackground/AquaticBackground.vue b/frontend/src/components/transcript-debug/aquaticBackground/AquaticBackground.vue new file mode 100644 index 0000000..4827919 --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/AquaticBackground.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/frontend/src/components/transcript-debug/aquaticBackground/index.ts b/frontend/src/components/transcript-debug/aquaticBackground/index.ts new file mode 100644 index 0000000..0f04534 --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/index.ts @@ -0,0 +1 @@ +export { default as AquaticBackground } from './AquaticBackground.vue' diff --git a/frontend/src/components/transcript-debug/aquaticBackground/layers/BubbleStream.vue b/frontend/src/components/transcript-debug/aquaticBackground/layers/BubbleStream.vue new file mode 100644 index 0000000..9615f4a --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/layers/BubbleStream.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/frontend/src/components/transcript-debug/aquaticBackground/layers/EdgeFade.vue b/frontend/src/components/transcript-debug/aquaticBackground/layers/EdgeFade.vue new file mode 100644 index 0000000..6a96a85 --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/layers/EdgeFade.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/components/transcript-debug/aquaticBackground/layers/EventOverlay.vue b/frontend/src/components/transcript-debug/aquaticBackground/layers/EventOverlay.vue new file mode 100644 index 0000000..6a868df --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/layers/EventOverlay.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/frontend/src/components/transcript-debug/aquaticBackground/layers/FishSchool.vue b/frontend/src/components/transcript-debug/aquaticBackground/layers/FishSchool.vue new file mode 100644 index 0000000..8b31a8b --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/layers/FishSchool.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/frontend/src/components/transcript-debug/aquaticBackground/layers/JellyfishDrift.vue b/frontend/src/components/transcript-debug/aquaticBackground/layers/JellyfishDrift.vue new file mode 100644 index 0000000..95de015 --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/layers/JellyfishDrift.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/frontend/src/components/transcript-debug/aquaticBackground/layers/OceanScene.vue b/frontend/src/components/transcript-debug/aquaticBackground/layers/OceanScene.vue new file mode 100644 index 0000000..3640cf9 --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/layers/OceanScene.vue @@ -0,0 +1,361 @@ + + + + + diff --git a/frontend/src/components/transcript-debug/aquaticBackground/layers/index.ts b/frontend/src/components/transcript-debug/aquaticBackground/layers/index.ts new file mode 100644 index 0000000..89dbcbf --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/layers/index.ts @@ -0,0 +1,9 @@ +// The unified background scene (gradient + light + floor + corals + seaweed + decorations) +export { default as OceanScene } from './OceanScene.vue' + +// Independent dynamic overlay layers +export { default as BubbleStream } from './BubbleStream.vue' +export { default as FishSchool } from './FishSchool.vue' +export { default as JellyfishDrift } from './JellyfishDrift.vue' +export { default as EventOverlay } from './EventOverlay.vue' +export { default as EdgeFade } from './EdgeFade.vue' diff --git a/frontend/src/components/transcript-debug/aquaticBackground/svgPatterns.ts b/frontend/src/components/transcript-debug/aquaticBackground/svgPatterns.ts new file mode 100644 index 0000000..f78afc6 --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/svgPatterns.ts @@ -0,0 +1,139 @@ +/** Encode an SVG string to a CSS-usable data URI */ +export function svgDataUri(svg: string): string { + return `url("data:image/svg+xml,${encodeURIComponent(svg)}")` +} + +/** Generate a pixel art fish SVG */ +export function fishSvg(opts: { + w: number + h: number + body: string + accent: string + eye?: string + facing?: 'left' | 'right' +}): string { + const { w, h, body, accent, facing = 'right' } = opts + const eye = opts.eye ?? '#000' + // Build a generic pixel fish: body rect, tail, eye + const hw = Math.floor(w / 2) + const hh = Math.floor(h / 2) + const tailW = Math.floor(w * 0.2) + const bodyW = w - tailW + const eyeX = facing === 'right' ? bodyW - 3 : tailW + 1 + const tailX = facing === 'right' ? 0 : bodyW + + return `` + // Tail + + `` + + `` + // Body + + `` + // Stripe + + `` + // Eye + + `` + + `` + + `` +} + +/** Generate a coral SVG of a given type */ +export function coralSvg(type: 'branching' | 'brain' | 'fan', color: string, accent: string): string { + if (type === 'branching') { + return `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + } + if (type === 'brain') { + return `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + } + // fan + return `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` +} + +/** Generate a seaweed stalk SVG */ +export function seaweedSvg(height: number, color: string, accent: string): string { + const segments: string[] = [] + for (let y = 0; y < height; y += 4) { + const xOff = (y / 4) % 2 === 0 ? 0 : 1 + const c = y % 8 === 0 ? color : accent + const op = 0.3 + (y / height) * 0.25 + segments.push(``) + } + // Leaf accents every 12px + for (let y = 6; y < height - 4; y += 12) { + const side = (y / 12) % 2 === 0 ? 0 : 3 + segments.push(``) + } + return `` + + segments.join('') + + `` +} + +/** Generate a jellyfish SVG */ +export function jellyfishSvg(bell: string, tentacle: string): string { + return `` + // Bell + + `` + + `` + + `` + + `` + // Inner glow + + `` + // Tentacles + + `` + + `` + + `` + + `` + // Tentacle wiggles + + `` + + `` + + `` + + `` +} + +/** Generate a starfish SVG */ +export function starfishSvg(color: string): string { + return `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` +} + +/** Generate a small shell SVG */ +export function shellSvg(color: string): string { + return `` + + `` + + `` + + `` + + `` + + `` + + `` +} diff --git a/frontend/src/components/transcript-debug/aquaticBackground/types.ts b/frontend/src/components/transcript-debug/aquaticBackground/types.ts new file mode 100644 index 0000000..2bb837c --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/types.ts @@ -0,0 +1,24 @@ +export type EventFrequency = 'minutes' | 'hours' | 'days' | 'months' +export type TimeOfDay = 'day' | 'twilight' | 'night' +export type Season = 'spring' | 'summer' | 'autumn' | 'winter' +export type DepthZone = 'surface' | 'midwater' | 'deep' + +export interface AquaticEvent { + id: string + name: string + frequency: EventFrequency + /** Min interval in milliseconds */ + minInterval: number + /** Max interval in milliseconds */ + maxInterval: number + /** Duration of the event in milliseconds */ + duration: number + /** CSS class applied to EventOverlay when active */ + cssClass: string +} + +export interface ActiveEvent { + event: AquaticEvent + startedAt: number + endsAt: number +} diff --git a/frontend/src/components/transcript-debug/aquaticBackground/useAquaticEvents.ts b/frontend/src/components/transcript-debug/aquaticBackground/useAquaticEvents.ts new file mode 100644 index 0000000..111943b --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/useAquaticEvents.ts @@ -0,0 +1,176 @@ +import { ref, readonly } from 'vue' +import { useAquaticState } from './useAquaticState' +import type { AquaticEvent, ActiveEvent, EventFrequency, TimeOfDay, Season, DepthZone } from './types' + +// ── Event catalog ── + +const MINUTE_EVENTS: AquaticEvent[] = [ + { id: 'school-of-fish', name: 'School of Fish', frequency: 'minutes', minInterval: 120_000, maxInterval: 480_000, duration: 15_000, cssClass: 'evt-school-of-fish' }, + { id: 'bubble-burst', name: 'Bubble Burst', frequency: 'minutes', minInterval: 120_000, maxInterval: 480_000, duration: 8_000, cssClass: 'evt-bubble-burst' }, + { id: 'bioluminescent-flash', name: 'Bioluminescent Flash', frequency: 'minutes', minInterval: 180_000, maxInterval: 480_000, duration: 10_000, cssClass: 'evt-bioluminescent-flash' }, + { id: 'plankton-drift', name: 'Plankton Drift', frequency: 'minutes', minInterval: 150_000, maxInterval: 420_000, duration: 12_000, cssClass: 'evt-plankton-drift' }, + { id: 'fish-chase', name: 'Fish Chase', frequency: 'minutes', minInterval: 120_000, maxInterval: 360_000, duration: 10_000, cssClass: 'evt-fish-chase' }, +] + +const HOUR_EVENTS: AquaticEvent[] = [ + { id: 'whale-shadow', name: 'Whale Shadow', frequency: 'hours', minInterval: 3_600_000, maxInterval: 14_400_000, duration: 20_000, cssClass: 'evt-whale-shadow' }, + { id: 'current-change', name: 'Current Change', frequency: 'hours', minInterval: 3_600_000, maxInterval: 10_800_000, duration: 60_000, cssClass: 'evt-current-change' }, + { id: 'color-shift', name: 'Color Shift', frequency: 'hours', minInterval: 3_600_000, maxInterval: 14_400_000, duration: 30_000, cssClass: 'evt-color-shift' }, + { id: 'turtle-crossing', name: 'Turtle Crossing', frequency: 'hours', minInterval: 7_200_000, maxInterval: 14_400_000, duration: 25_000, cssClass: 'evt-turtle-crossing' }, + { id: 'manta-ray', name: 'Manta Ray', frequency: 'hours', minInterval: 5_400_000, maxInterval: 14_400_000, duration: 18_000, cssClass: 'evt-manta-ray' }, +] + +const DAY_EVENTS: AquaticEvent[] = [ + { id: 'day-night-shift', name: 'Day/Night Shift', frequency: 'days', minInterval: 86_400_000, maxInterval: 259_200_000, duration: 120_000, cssClass: 'evt-day-night-shift' }, + { id: 'seasonal-bloom', name: 'Seasonal Bloom', frequency: 'days', minInterval: 86_400_000, maxInterval: 259_200_000, duration: 180_000, cssClass: 'evt-seasonal-bloom' }, + { id: 'depth-change', name: 'Depth Change', frequency: 'days', minInterval: 86_400_000, maxInterval: 172_800_000, duration: 90_000, cssClass: 'evt-depth-change' }, + { id: 'kelp-forest', name: 'Kelp Forest', frequency: 'days', minInterval: 129_600_000, maxInterval: 259_200_000, duration: 150_000, cssClass: 'evt-kelp-forest' }, +] + +const MONTH_EVENTS: AquaticEvent[] = [ + { id: 'aurora-underwater', name: 'Aurora Underwater', frequency: 'months', minInterval: 2_592_000_000, maxInterval: 10_368_000_000, duration: 300_000, cssClass: 'evt-aurora-underwater' }, + { id: 'mythical-creature', name: 'Mythical Creature', frequency: 'months', minInterval: 2_592_000_000, maxInterval: 10_368_000_000, duration: 30_000, cssClass: 'evt-mythical-creature' }, + { id: 'volcanic-vent', name: 'Volcanic Vent', frequency: 'months', minInterval: 2_592_000_000, maxInterval: 7_776_000_000, duration: 120_000, cssClass: 'evt-volcanic-vent' }, + { id: 'crystal-formation', name: 'Crystal Formation', frequency: 'months', minInterval: 2_592_000_000, maxInterval: 10_368_000_000, duration: 240_000, cssClass: 'evt-crystal-formation' }, +] + +const STORAGE_KEY = 'aquatic-event-timestamps' + +// ── Helpers ── + +function pickRandom(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)] +} + +function randomBetween(min: number, max: number): number { + return min + Math.random() * (max - min) +} + +function loadTimestamps(): Record { + try { + return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') + } catch { + return {} + } +} + +function saveTimestamp(frequency: EventFrequency) { + const ts = loadTimestamps() + ts[frequency] = Date.now() + localStorage.setItem(STORAGE_KEY, JSON.stringify(ts)) +} + +// ── Composable ── + +export function useAquaticEvents() { + const { setEventModifier, clearEventModifier, setTimeOfDay, setSeason, setDepthZone } = useAquaticState() + const activeEvents = ref([]) + const timerIds: number[] = [] + + const TIERS: { frequency: EventFrequency; events: AquaticEvent[] }[] = [ + { frequency: 'minutes', events: MINUTE_EVENTS }, + { frequency: 'hours', events: HOUR_EVENTS }, + { frequency: 'days', events: DAY_EVENTS }, + { frequency: 'months', events: MONTH_EVENTS }, + ] + + function start() { + const timestamps = loadTimestamps() + const now = Date.now() + + for (const tier of TIERS) { + const lastFired = timestamps[tier.frequency] || 0 + const elapsed = now - lastFired + // Pick a representative event to check interval + const sample = tier.events[0] + const minWait = sample.minInterval + + if (elapsed >= minWait) { + // Enough time passed — trigger one soon (5-30s from now) + const initialDelay = randomBetween(5_000, 30_000) + const id = window.setTimeout(() => { + triggerRandomFromTier(tier.frequency, tier.events) + scheduleTier(tier.frequency, tier.events) + }, initialDelay) + timerIds.push(id) + } else { + // Wait for remaining time, then start normal cycle + const remaining = minWait - elapsed + const id = window.setTimeout(() => { + scheduleTier(tier.frequency, tier.events) + }, remaining) + timerIds.push(id) + } + } + } + + function stop() { + timerIds.forEach(id => clearTimeout(id)) + timerIds.length = 0 + // Clean up active events + for (const ae of activeEvents.value) { + clearEventModifier(ae.event.id) + } + activeEvents.value = [] + } + + function scheduleTier(frequency: EventFrequency, events: AquaticEvent[]) { + const event = pickRandom(events) + const delay = randomBetween(event.minInterval, event.maxInterval) + const id = window.setTimeout(() => { + triggerRandomFromTier(frequency, events) + scheduleTier(frequency, events) + }, delay) + timerIds.push(id) + } + + function triggerRandomFromTier(frequency: EventFrequency, events: AquaticEvent[]) { + // Filter out already-active events + const available = events.filter(e => + !activeEvents.value.some(ae => ae.event.id === e.id) + ) + if (available.length === 0) return + + const event = pickRandom(available) + triggerEvent(event, frequency) + } + + function triggerEvent(event: AquaticEvent, frequency: EventFrequency) { + const now = Date.now() + const active: ActiveEvent = { + event, + startedAt: now, + endsAt: now + event.duration, + } + activeEvents.value = [...activeEvents.value, active] + setEventModifier(event.id, event.cssClass) + saveTimestamp(frequency) + + // Special state mutations for certain events + if (event.id === 'day-night-shift') { + const options: TimeOfDay[] = ['day', 'twilight', 'night'] + setTimeOfDay(pickRandom(options)) + } + if (event.id === 'seasonal-bloom') { + const options: Season[] = ['spring', 'summer', 'autumn', 'winter'] + setSeason(pickRandom(options)) + } + if (event.id === 'depth-change') { + const options: DepthZone[] = ['surface', 'midwater', 'deep'] + setDepthZone(pickRandom(options)) + } + + // Schedule end + const endId = window.setTimeout(() => { + activeEvents.value = activeEvents.value.filter(ae => ae !== active) + clearEventModifier(event.id) + }, event.duration) + timerIds.push(endId) + } + + return { + activeEvents: readonly(activeEvents), + start, + stop, + } +} diff --git a/frontend/src/components/transcript-debug/aquaticBackground/useAquaticState.ts b/frontend/src/components/transcript-debug/aquaticBackground/useAquaticState.ts new file mode 100644 index 0000000..6242561 --- /dev/null +++ b/frontend/src/components/transcript-debug/aquaticBackground/useAquaticState.ts @@ -0,0 +1,54 @@ +import { ref, readonly } from 'vue' +import type { TimeOfDay, Season, DepthZone } from './types' + +// ── Module-level singleton refs ── +const depthZone = ref('midwater') +const timeOfDay = ref('night') +const season = ref(getCurrentSeason()) +const activeEventModifiers = ref>(new Map()) + +function getCurrentSeason(): Season { + const month = new Date().getMonth() + if (month >= 2 && month <= 4) return 'spring' + if (month >= 5 && month <= 7) return 'summer' + if (month >= 8 && month <= 10) return 'autumn' + return 'winter' +} + +export function useAquaticState() { + function setDepthZone(zone: DepthZone) { + depthZone.value = zone + } + + function setTimeOfDay(tod: TimeOfDay) { + timeOfDay.value = tod + } + + function setSeason(s: Season) { + season.value = s + } + + function setEventModifier(eventId: string, cssClass: string) { + const next = new Map(activeEventModifiers.value) + next.set(eventId, cssClass) + activeEventModifiers.value = next + } + + function clearEventModifier(eventId: string) { + const next = new Map(activeEventModifiers.value) + next.delete(eventId) + activeEventModifiers.value = next + } + + return { + depthZone: readonly(depthZone), + timeOfDay: readonly(timeOfDay), + season: readonly(season), + activeEventModifiers: readonly(activeEventModifiers), + setDepthZone, + setTimeOfDay, + setSeason, + setEventModifier, + clearEventModifier, + } +} diff --git a/frontend/src/components/transcript-debug/index.ts b/frontend/src/components/transcript-debug/index.ts index 192d4d9..be12e57 100644 --- a/frontend/src/components/transcript-debug/index.ts +++ b/frontend/src/components/transcript-debug/index.ts @@ -11,4 +11,5 @@ export { default as SystemMessage } from './SystemMessage.vue' export { default as UserInput } from './UserInput.vue' export { default as PermissionApproval } from './PermissionApproval.vue' export { default as PlanApproval } from './PlanApproval.vue' -export { default as BackgroundPixelArt } from './BackgroundPixelArt.vue' +export { default as CodeBlock } from './CodeBlock.vue' +export { AquaticBackground } from './aquaticBackground' diff --git a/frontend/src/components/transcript-debug/toolCards/EditCard.vue b/frontend/src/components/transcript-debug/toolCards/EditCard.vue index bd190bc..d9e00b3 100644 --- a/frontend/src/components/transcript-debug/toolCards/EditCard.vue +++ b/frontend/src/components/transcript-debug/toolCards/EditCard.vue @@ -1,8 +1,8 @@ @@ -59,11 +56,11 @@ const newLineCount = computed(() => newString.value.split('\n').length)
- old
-

+          
         
+ new
-

+          
         
@@ -159,17 +156,4 @@ const newLineCount = computed(() => newString.value.split('\n').length) .diff-sign { font-size: 12px; font-weight: 700; font-family: 'SF Mono', 'Fira Code', monospace; } -.diff-code { - margin: 0; - padding: 0.35rem 0.6rem; - font-size: 11px; - line-height: 1.45; - color: var(--text-secondary); - white-space: pre-wrap; - word-break: break-all; - max-height: 180px; - overflow-y: auto; - font-family: 'SF Mono', 'Fira Code', monospace; - background: transparent; -} diff --git a/frontend/src/components/transcript-debug/toolCards/TaskCard.vue b/frontend/src/components/transcript-debug/toolCards/TaskCard.vue index f2d8f0a..de28e42 100644 --- a/frontend/src/components/transcript-debug/toolCards/TaskCard.vue +++ b/frontend/src/components/transcript-debug/toolCards/TaskCard.vue @@ -2,6 +2,7 @@ import { ref, computed } from 'vue' import type { ParsedToolCall } from '@/types/transcript-debug' import ToolResultBlock from '../ToolResultBlock.vue' +import CodeBlock from '../CodeBlock.vue' const props = defineProps<{ call: ParsedToolCall @@ -189,10 +190,10 @@ const hasBody = computed(() =>