From 4be62d1e2670cbef024027964545607e46e7cfbd Mon Sep 17 00:00:00 2001 From: inter Date: Mon, 8 Sep 2025 16:37:49 +0800 Subject: [PATCH] Add File --- frontend/src/views/chat/typed.ts | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 frontend/src/views/chat/typed.ts diff --git a/frontend/src/views/chat/typed.ts b/frontend/src/views/chat/typed.ts new file mode 100644 index 0000000..3058ac6 --- /dev/null +++ b/frontend/src/views/chat/typed.ts @@ -0,0 +1,88 @@ +import { computed, onWatcherCleanup, unref, watch, ref } from 'vue' +import type { Ref, VNode } from 'vue' + +function isString(str: any): str is string { + return typeof str === 'string' +} + +function useState>(defaultStateValue?: T | (() => T)): [R, (val: T) => void] { + const initValue: T = + typeof defaultStateValue === 'function' ? (defaultStateValue as any)() : defaultStateValue + + const innerValue = ref(initValue) as Ref + + function triggerChange(newValue: T) { + innerValue.value = newValue + } + + return [innerValue as unknown as R, triggerChange] +} +/** + * Return typed content and typing status when typing is enabled. + * Or return content directly. + */ +const useTypedEffect = ( + content: Ref, + typingEnabled: Ref, + typingStep: Ref, + typingInterval: Ref +): [typedContent: Ref, isTyping: Ref] => { + const [prevContent, setPrevContent] = useState('') + const [typingIndex, setTypingIndex] = useState(1) + + const mergedTypingEnabled = computed(() => typingEnabled.value && isString(content.value)) + + // Reset typing index when content changed + watch(content, () => { + const prevContentValue = unref(prevContent) + setPrevContent(content.value) + if (!mergedTypingEnabled.value && isString(content.value)) { + setTypingIndex(content.value.length) + } else if ( + isString(content.value) && + isString(prevContentValue) && + content.value.indexOf(prevContentValue) !== 0 + ) { + setTypingIndex(1) + } + }) + + // Start typing + watch( + [typingIndex, typingEnabled, content], + () => { + if ( + mergedTypingEnabled.value && + isString(content.value) && + unref(typingIndex) < content.value.length + ) { + const id = setTimeout(() => { + setTypingIndex(unref(typingIndex) + typingStep.value) + }, typingInterval.value) + + onWatcherCleanup(() => { + clearTimeout(id) + }) + } + }, + { immediate: true } + ) + + const mergedTypingContent = computed(() => + mergedTypingEnabled.value && isString(content.value) + ? content.value.slice(0, unref(typingIndex)) + : content.value + ) + + return [ + mergedTypingContent, + computed( + () => + mergedTypingEnabled.value && + isString(content.value) && + unref(typingIndex) < content.value.length + ), + ] +} + +export default useTypedEffect