docs: 补充 AI 打字机输出、小程序灵感卡片、脚本主页布局等设计文档和计划
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
export const useTypewriterStream = ({ interval = 18, step = 1 } = {}) => {
|
||||
const [visibleText, setVisibleText] = useState('');
|
||||
const [targetText, setTargetText] = useState('');
|
||||
const [received, setReceived] = useState(false);
|
||||
const [backendDone, setBackendDone] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const visibleRef = useRef('');
|
||||
const targetRef = useRef('');
|
||||
const doneRef = useRef(false);
|
||||
const failedRef = useRef(false);
|
||||
const timerRef = useRef(null);
|
||||
const waitersRef = useRef([]);
|
||||
|
||||
const stopTimer = useCallback(() => {
|
||||
if (timerRef.current) {
|
||||
window.clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resolveWaiters = useCallback(() => {
|
||||
if (!waitersRef.current.length) return;
|
||||
const waiters = waitersRef.current;
|
||||
waitersRef.current = [];
|
||||
waiters.forEach(resolve => resolve(visibleRef.current));
|
||||
}, []);
|
||||
|
||||
const isFullyRendered = useCallback(() => {
|
||||
return doneRef.current && visibleRef.current.length >= targetRef.current.length;
|
||||
}, []);
|
||||
|
||||
const tick = useCallback(() => {
|
||||
if (visibleRef.current.length < targetRef.current.length) {
|
||||
const nextLength = Math.min(targetRef.current.length, visibleRef.current.length + step);
|
||||
const nextText = targetRef.current.slice(0, nextLength);
|
||||
visibleRef.current = nextText;
|
||||
setVisibleText(nextText);
|
||||
return;
|
||||
}
|
||||
if (doneRef.current || failedRef.current) {
|
||||
stopTimer();
|
||||
if (isFullyRendered() || failedRef.current) resolveWaiters();
|
||||
}
|
||||
}, [isFullyRendered, resolveWaiters, step, stopTimer]);
|
||||
|
||||
const ensureTimer = useCallback(() => {
|
||||
if (!timerRef.current) timerRef.current = window.setInterval(tick, interval);
|
||||
}, [interval, tick]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
stopTimer();
|
||||
visibleRef.current = '';
|
||||
targetRef.current = '';
|
||||
doneRef.current = false;
|
||||
failedRef.current = false;
|
||||
setVisibleText('');
|
||||
setTargetText('');
|
||||
setReceived(false);
|
||||
setBackendDone(false);
|
||||
setFailed(false);
|
||||
resolveWaiters();
|
||||
}, [resolveWaiters, stopTimer]);
|
||||
|
||||
const push = useCallback((nextText = '') => {
|
||||
const next = String(nextText || '');
|
||||
if (next.length < visibleRef.current.length) reset();
|
||||
targetRef.current = next;
|
||||
setTargetText(next);
|
||||
setReceived(true);
|
||||
ensureTimer();
|
||||
}, [ensureTimer, reset]);
|
||||
|
||||
const finish = useCallback((finalText) => {
|
||||
if (typeof finalText === 'string') {
|
||||
targetRef.current = finalText;
|
||||
setTargetText(finalText);
|
||||
}
|
||||
doneRef.current = true;
|
||||
setBackendDone(true);
|
||||
if (targetRef.current.length > visibleRef.current.length) ensureTimer();
|
||||
else {
|
||||
stopTimer();
|
||||
resolveWaiters();
|
||||
}
|
||||
}, [ensureTimer, resolveWaiters, stopTimer]);
|
||||
|
||||
const fail = useCallback((message) => {
|
||||
failedRef.current = true;
|
||||
doneRef.current = true;
|
||||
setFailed(true);
|
||||
setBackendDone(true);
|
||||
if (!visibleRef.current && message) {
|
||||
visibleRef.current = message;
|
||||
targetRef.current = message;
|
||||
setVisibleText(message);
|
||||
setTargetText(message);
|
||||
}
|
||||
stopTimer();
|
||||
resolveWaiters();
|
||||
}, [resolveWaiters, stopTimer]);
|
||||
|
||||
const waitForDone = useCallback(() => {
|
||||
if (isFullyRendered() || failedRef.current) {
|
||||
return Promise.resolve(visibleRef.current);
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
waitersRef.current.push(resolve);
|
||||
ensureTimer();
|
||||
});
|
||||
}, [ensureTimer, isFullyRendered]);
|
||||
|
||||
useEffect(() => stopTimer, [stopTimer]);
|
||||
|
||||
return useMemo(() => ({
|
||||
visibleText,
|
||||
targetText,
|
||||
isWaiting: !received && !backendDone && !failed,
|
||||
isStreaming: received && (visibleText.length < targetText.length || !backendDone),
|
||||
isDraining: backendDone && visibleText.length < targetText.length,
|
||||
isDone: backendDone && visibleText.length >= targetText.length && !failed,
|
||||
reset,
|
||||
push,
|
||||
finish,
|
||||
waitForDone,
|
||||
fail
|
||||
}), [backendDone, failed, finish, push, received, reset, targetText, visibleText, waitForDone, fail]);
|
||||
};
|
||||
|
||||
export default useTypewriterStream;
|
||||
Reference in New Issue
Block a user