|
| 1 | +<script setup lang="ts"> |
| 2 | +import type { HTMLAttributes } from 'vue' |
| 3 | +import { Collapsible } from '@repo/shadcn-vue/components/ui/collapsible' |
| 4 | +import { cn } from '@repo/shadcn-vue/lib/utils' |
| 5 | +import { useVModel } from '@vueuse/core' |
| 6 | +import { computed, provide, ref, watch } from 'vue' |
| 7 | +import { ReasoningKey } from './context' |
| 8 | +
|
| 9 | +interface Props { |
| 10 | + class?: HTMLAttributes['class'] |
| 11 | + isStreaming?: boolean |
| 12 | + open?: boolean |
| 13 | + defaultOpen?: boolean |
| 14 | + duration?: number |
| 15 | +} |
| 16 | +
|
| 17 | +const props = withDefaults(defineProps<Props>(), { |
| 18 | + isStreaming: false, |
| 19 | + defaultOpen: true, |
| 20 | + duration: undefined, |
| 21 | +}) |
| 22 | +
|
| 23 | +const emit = defineEmits<{ |
| 24 | + (e: 'update:open', value: boolean): void |
| 25 | + (e: 'update:duration', value: number): void |
| 26 | +}>() |
| 27 | +
|
| 28 | +const isOpen = useVModel(props, 'open', emit, { |
| 29 | + defaultValue: props.defaultOpen, |
| 30 | + passive: true, |
| 31 | +}) |
| 32 | +
|
| 33 | +const internalDuration = ref<number | undefined>(props.duration) |
| 34 | +
|
| 35 | +watch(() => props.duration, (newVal) => { |
| 36 | + internalDuration.value = newVal |
| 37 | +}) |
| 38 | +
|
| 39 | +function updateDuration(val: number) { |
| 40 | + internalDuration.value = val |
| 41 | + emit('update:duration', val) |
| 42 | +} |
| 43 | +
|
| 44 | +const hasAutoClosed = ref(false) |
| 45 | +const startTime = ref<number | null>(null) |
| 46 | +
|
| 47 | +const MS_IN_S = 1000 |
| 48 | +const AUTO_CLOSE_DELAY = 1000 |
| 49 | +
|
| 50 | +// Track duration when streaming starts and ends |
| 51 | +watch(() => props.isStreaming, (streaming) => { |
| 52 | + if (streaming) { |
| 53 | + // Auto-open when streaming starts |
| 54 | + isOpen.value = true |
| 55 | +
|
| 56 | + if (startTime.value === null) { |
| 57 | + startTime.value = Date.now() |
| 58 | + } |
| 59 | + } |
| 60 | + else if (startTime.value !== null) { |
| 61 | + const calculatedDuration = Math.ceil((Date.now() - startTime.value) / MS_IN_S) |
| 62 | + updateDuration(calculatedDuration) |
| 63 | + startTime.value = null |
| 64 | + } |
| 65 | +}) |
| 66 | +
|
| 67 | +// Auto-close logic |
| 68 | +watch([() => props.isStreaming, isOpen, () => props.defaultOpen, hasAutoClosed], (_, __, onCleanup) => { |
| 69 | + if (props.defaultOpen && !props.isStreaming && isOpen.value && !hasAutoClosed.value) { |
| 70 | + const timer = setTimeout(() => { |
| 71 | + isOpen.value = false |
| 72 | + hasAutoClosed.value = true |
| 73 | + }, AUTO_CLOSE_DELAY) |
| 74 | +
|
| 75 | + onCleanup(() => clearTimeout(timer)) |
| 76 | + } |
| 77 | +}, { immediate: true }) |
| 78 | +
|
| 79 | +provide(ReasoningKey, { |
| 80 | + isStreaming: computed(() => props.isStreaming), |
| 81 | + isOpen, |
| 82 | + setIsOpen: (val: boolean) => { isOpen.value = val }, |
| 83 | + duration: computed(() => internalDuration.value), |
| 84 | +}) |
| 85 | +</script> |
| 86 | + |
| 87 | +<template> |
| 88 | + <Collapsible |
| 89 | + v-model:open="isOpen" |
| 90 | + :class="cn('not-prose mb-4', props.class)" |
| 91 | + > |
| 92 | + <slot /> |
| 93 | + </Collapsible> |
| 94 | +</template> |
0 commit comments