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