feat: life-script AI 运行时和视图优化
This commit is contained in:
@@ -18,6 +18,33 @@ const parseSseFrame = (frame) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findOverlapLength = (current, next) => {
|
||||
const max = Math.min(current.length, next.length);
|
||||
for (let size = max; size > 0; size -= 1) {
|
||||
if (current.slice(-size) === next.slice(0, size)) return size;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const mergeStreamOutput = (current, chunk) => {
|
||||
const next = String(chunk || '');
|
||||
if (!next) return { output: current, delta: '' };
|
||||
if (!current) return { output: next, delta: next };
|
||||
if (next === current) return { output: current, delta: '' };
|
||||
if (next.length >= 16 && current.endsWith(next)) return { output: current, delta: '' };
|
||||
if (next.startsWith(current)) {
|
||||
return { output: next, delta: next.slice(current.length) };
|
||||
}
|
||||
const currentIndex = next.length > current.length ? next.indexOf(current) : -1;
|
||||
if (currentIndex >= 0) {
|
||||
return { output: next, delta: next.slice(currentIndex + current.length) };
|
||||
}
|
||||
const overlap = findOverlapLength(current, next);
|
||||
if (overlap < 8) return { output: current + next, delta: next };
|
||||
const delta = next.slice(overlap);
|
||||
return { output: current + delta, delta };
|
||||
};
|
||||
|
||||
const authHeaders = () => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
@@ -73,6 +100,7 @@ export const streamAiScene = async ({
|
||||
let output = '';
|
||||
let closed = false;
|
||||
let recovered = false;
|
||||
let streamStarted = false;
|
||||
let recoveryTimer;
|
||||
let recoveryPromise;
|
||||
|
||||
@@ -92,6 +120,7 @@ export const streamAiScene = async ({
|
||||
|
||||
const completeFromRecoveredOutput = async () => {
|
||||
if (closed) return;
|
||||
if (streamStarted || output.trim()) return;
|
||||
try {
|
||||
const recoveredOutput = await recoverOnce();
|
||||
if (closed) return;
|
||||
@@ -107,7 +136,7 @@ export const streamAiScene = async ({
|
||||
|
||||
recoveryTimer = setTimeout(() => {
|
||||
completeFromRecoveredOutput();
|
||||
}, 8000);
|
||||
}, 25000);
|
||||
|
||||
const finishRecovered = (event, message) => {
|
||||
if (!output.trim()) return false;
|
||||
@@ -127,13 +156,13 @@ export const streamAiScene = async ({
|
||||
};
|
||||
|
||||
const recoverOrThrow = async (message, event) => {
|
||||
if (finishRecovered(event, message)) return;
|
||||
try {
|
||||
output = await recoverOnce();
|
||||
recovered = true;
|
||||
closed = true;
|
||||
clearRecoveryTimer();
|
||||
} catch (error) {
|
||||
if (finishRecovered(event, message || error?.message)) return;
|
||||
const finalMessage = message || error?.message || 'AI 生成结果暂时没有返回';
|
||||
onError?.(finalMessage, event);
|
||||
throw new Error(finalMessage);
|
||||
@@ -148,12 +177,19 @@ export const streamAiScene = async ({
|
||||
const event = parseSseFrame(frame);
|
||||
if (!event) return;
|
||||
if (event.type === 'start') {
|
||||
streamStarted = true;
|
||||
clearRecoveryTimer();
|
||||
onStart?.(event);
|
||||
} else if (event.type === 'delta') {
|
||||
const delta = event.content || '';
|
||||
output += delta;
|
||||
onDelta?.(delta, output, event);
|
||||
streamStarted = true;
|
||||
clearRecoveryTimer();
|
||||
const merged = mergeStreamOutput(output, event.content);
|
||||
output = merged.output;
|
||||
if (merged.delta) {
|
||||
onDelta?.(merged.delta, output, event);
|
||||
}
|
||||
} else if (event.type === 'done') {
|
||||
streamStarted = true;
|
||||
closed = true;
|
||||
clearRecoveryTimer();
|
||||
onDone?.(event, output);
|
||||
@@ -191,6 +227,8 @@ export const streamAiScene = async ({
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
streamStarted = true;
|
||||
clearRecoveryTimer();
|
||||
consumeText(decoder.decode(value, { stream: true }));
|
||||
if (closed || recovered) break;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const PathView = ({ onGoToScript }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [streamPath, setStreamPath] = useState('');
|
||||
const pathWriter = useTypewriterStream({ interval: 18, step: 1 });
|
||||
const pathWriter = useTypewriterStream({ interval: 30, step: 1 });
|
||||
|
||||
const selectedScript = getSelectedScript();
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
const [length, setLength] = useState(scriptLengths[0].value);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [streamContent, setStreamContent] = useState('');
|
||||
const scriptWriter = useTypewriterStream({ interval: 18, step: 1 });
|
||||
const scriptWriter = useTypewriterStream({ interval: 30, step: 1 });
|
||||
|
||||
// 编辑模态框状态
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
|
||||
@@ -94,7 +94,7 @@ const TimelineView = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [streamFeedback, setStreamFeedback] = useState('');
|
||||
const feedbackWriter = useTypewriterStream({ interval: 18, step: 1 });
|
||||
const feedbackWriter = useTypewriterStream({ interval: 30, step: 1 });
|
||||
|
||||
// 编辑模式状态:null 表示新增模式,有值表示编辑模式(存储事件 ID)
|
||||
const [editingEventId, setEditingEventId] = useState(null);
|
||||
|
||||
Reference in New Issue
Block a user