feat: life-script AI 运行时和视图优化

This commit is contained in:
2026-05-26 20:50:05 +08:00
parent c289097ca0
commit a51d225897
4 changed files with 46 additions and 8 deletions
+43 -5
View File
@@ -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;
}
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);