|
|
|
|
@@ -1,8 +1,10 @@
|
|
|
|
|
import type {
|
|
|
|
|
ChatTimeAssistantItem,
|
|
|
|
|
ChatTimeHistoryRecord,
|
|
|
|
|
ChatTimeRoundMeta,
|
|
|
|
|
ChatTimeThinkingStatus,
|
|
|
|
|
ChatTimeTimelineItem,
|
|
|
|
|
ChatTimeTimelineItemBase,
|
|
|
|
|
ChatTimeToolItem,
|
|
|
|
|
ChatTimeToolMutationPayload,
|
|
|
|
|
ChatTimeToolStatus,
|
|
|
|
|
@@ -28,17 +30,71 @@ class ChatTimeTimelineBuilder {
|
|
|
|
|
content?: string;
|
|
|
|
|
created?: number | string;
|
|
|
|
|
id?: string;
|
|
|
|
|
messageKind?: string;
|
|
|
|
|
roundId?: number | string;
|
|
|
|
|
roundNo?: number;
|
|
|
|
|
senderName?: string;
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
items.push({
|
|
|
|
|
const item: ChatTimeTimelineItem = {
|
|
|
|
|
content: normalizePlainText(payload.content),
|
|
|
|
|
created: normalizeTimestamp(payload.created),
|
|
|
|
|
id: payload.id || uuid(),
|
|
|
|
|
placement: 'end',
|
|
|
|
|
role: 'user',
|
|
|
|
|
senderName: payload.senderName,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
applyRoundMeta(item, payload);
|
|
|
|
|
items.push(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 将最新一条待绑定的用户消息补齐到当前轮次。
|
|
|
|
|
*/
|
|
|
|
|
static bindLatestPendingUserMessage(
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
meta?: ChatTimeRoundMeta,
|
|
|
|
|
) {
|
|
|
|
|
const roundId = normalizeRoundId(meta?.roundId);
|
|
|
|
|
if (!roundId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
const item = items[index];
|
|
|
|
|
if (!item) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (item.role !== 'user') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (item.roundId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
applyRoundMeta(item, {
|
|
|
|
|
roundId,
|
|
|
|
|
roundNo: meta?.roundNo,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新指定轮次的可切换状态。
|
|
|
|
|
*/
|
|
|
|
|
static setRoundSwitchable(
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
roundId: number | string | undefined,
|
|
|
|
|
switchable: boolean,
|
|
|
|
|
) {
|
|
|
|
|
const normalizedRoundId = normalizeRoundId(roundId);
|
|
|
|
|
if (!normalizedRoundId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
if (item.roundId === normalizedRoundId && item.role !== 'user') {
|
|
|
|
|
item.switchable = switchable;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -48,12 +104,14 @@ class ChatTimeTimelineBuilder {
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
delta?: string,
|
|
|
|
|
created?: number | string,
|
|
|
|
|
meta?: ChatTimeRoundMeta,
|
|
|
|
|
) {
|
|
|
|
|
const normalizedDelta = normalizePlainText(delta);
|
|
|
|
|
if (!normalizedDelta) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const assistant = ensureAssistantTail(items, created);
|
|
|
|
|
prepareRoundVariant(items, meta);
|
|
|
|
|
const assistant = ensureAssistantTail(items, created, meta);
|
|
|
|
|
const tail = assistant.segments[assistant.segments.length - 1];
|
|
|
|
|
if (tail?.type === 'thinking' && tail.status === 'thinking') {
|
|
|
|
|
tail.content += normalizedDelta;
|
|
|
|
|
@@ -77,12 +135,14 @@ class ChatTimeTimelineBuilder {
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
delta?: string,
|
|
|
|
|
created?: number | string,
|
|
|
|
|
meta?: ChatTimeRoundMeta,
|
|
|
|
|
) {
|
|
|
|
|
const normalizedDelta = normalizeAssistantText(delta);
|
|
|
|
|
if (!normalizedDelta) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const assistant = ensureAssistantTail(items, created);
|
|
|
|
|
prepareRoundVariant(items, meta);
|
|
|
|
|
const assistant = ensureAssistantTail(items, created, meta);
|
|
|
|
|
stopThinkingForAssistant(assistant);
|
|
|
|
|
const tail = assistant.segments[assistant.segments.length - 1];
|
|
|
|
|
if (tail?.type === 'text') {
|
|
|
|
|
@@ -117,12 +177,14 @@ class ChatTimeTimelineBuilder {
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
payload: ChatTimeToolMutationPayload,
|
|
|
|
|
) {
|
|
|
|
|
prepareRoundVariant(items, payload);
|
|
|
|
|
this.stopThinking(items);
|
|
|
|
|
const toolItem = ensureToolItem(
|
|
|
|
|
items,
|
|
|
|
|
payload.toolCallId,
|
|
|
|
|
payload.created,
|
|
|
|
|
payload.name,
|
|
|
|
|
payload,
|
|
|
|
|
);
|
|
|
|
|
toolItem.arguments = normalizePayloadValue(payload.value);
|
|
|
|
|
toolItem.content = '';
|
|
|
|
|
@@ -136,11 +198,13 @@ class ChatTimeTimelineBuilder {
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
payload: ChatTimeToolMutationPayload,
|
|
|
|
|
) {
|
|
|
|
|
prepareRoundVariant(items, payload);
|
|
|
|
|
const toolItem = ensureToolItem(
|
|
|
|
|
items,
|
|
|
|
|
payload.toolCallId,
|
|
|
|
|
payload.created,
|
|
|
|
|
payload.name,
|
|
|
|
|
payload,
|
|
|
|
|
);
|
|
|
|
|
toolItem.result = normalizePayloadValue(payload.result);
|
|
|
|
|
toolItem.content = toolItem.result;
|
|
|
|
|
@@ -178,7 +242,7 @@ class ChatTimeTimelineBuilder {
|
|
|
|
|
* 结束当前轮的 assistant 状态。
|
|
|
|
|
*/
|
|
|
|
|
static finalize(items: ChatTimeTimelineItem[]) {
|
|
|
|
|
const last = items[items.length - 1];
|
|
|
|
|
const last = findLastAssistant(items);
|
|
|
|
|
if (!isAssistantItem(last)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -186,6 +250,26 @@ class ChatTimeTimelineBuilder {
|
|
|
|
|
last.loading = false;
|
|
|
|
|
last.typing = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按轮次替换当前主线可见的 assistant/tool 片段。
|
|
|
|
|
*/
|
|
|
|
|
static replaceRoundMessages(
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
roundId: number | string | undefined,
|
|
|
|
|
nextMessages: ChatTimeTimelineItem[],
|
|
|
|
|
) {
|
|
|
|
|
const normalizedRoundId = normalizeRoundId(roundId);
|
|
|
|
|
if (!normalizedRoundId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const range = resolveRoundReplaceRange(items, normalizedRoundId);
|
|
|
|
|
if (range) {
|
|
|
|
|
items.splice(range.start, range.deleteCount, ...nextMessages);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
items.splice(resolveRoundInsertIndex(items, normalizedRoundId), 0, ...nextMessages);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -196,7 +280,9 @@ class ChatTimeHistoryMapper {
|
|
|
|
|
* 从聊天历史记录恢复时间线。
|
|
|
|
|
*/
|
|
|
|
|
static fromHistoryRecords(records: ChatTimeHistoryRecord[]) {
|
|
|
|
|
return records.flatMap((record) => this.fromHistoryRecord(record));
|
|
|
|
|
return normalizeVisibleHistoryRecords(records).flatMap((record) =>
|
|
|
|
|
this.fromHistoryRecord(record),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -249,8 +335,15 @@ class ChatTimeHistoryMapper {
|
|
|
|
|
const assistant = createAssistantItem(record.created, {
|
|
|
|
|
id: record.id == null ? undefined : String(record.id),
|
|
|
|
|
loading: record.loading,
|
|
|
|
|
messageKind: record.messageKind,
|
|
|
|
|
roundId: normalizeRoundId(record.roundId),
|
|
|
|
|
roundNo: record.roundNo,
|
|
|
|
|
selectedVariantIndex: record.selectedVariantIndex,
|
|
|
|
|
senderName: record.senderName,
|
|
|
|
|
switchable: record.switchable,
|
|
|
|
|
typing: record.typing,
|
|
|
|
|
variantCount: record.variantCount,
|
|
|
|
|
variantIndex: record.variantIndex,
|
|
|
|
|
});
|
|
|
|
|
const tools: ChatTimeTimelineItem[] = [];
|
|
|
|
|
|
|
|
|
|
@@ -267,7 +360,7 @@ class ChatTimeHistoryMapper {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toolItem = createToolItemFromChain(rawChain, record.created);
|
|
|
|
|
const toolItem = createToolItemFromChain(rawChain, record.created, record);
|
|
|
|
|
if (toolItem) {
|
|
|
|
|
tools.push(toolItem);
|
|
|
|
|
}
|
|
|
|
|
@@ -316,6 +409,7 @@ class ChatTimeHistoryMapper {
|
|
|
|
|
rawMessage,
|
|
|
|
|
toolMetaMap,
|
|
|
|
|
record.created,
|
|
|
|
|
record,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
@@ -325,11 +419,84 @@ class ChatTimeHistoryMapper {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeVisibleHistoryRecords(records: ChatTimeHistoryRecord[]) {
|
|
|
|
|
const dedupedRecords = dedupeHistoryRecords(records);
|
|
|
|
|
const userSelectedVariantByRound = new Map<string, number>();
|
|
|
|
|
const assistantSelectedVariantByRound = new Map<string, number>();
|
|
|
|
|
const fallbackVariantByRound = new Map<string, number>();
|
|
|
|
|
for (const record of dedupedRecords) {
|
|
|
|
|
const roundId = normalizeRoundId(record.roundId);
|
|
|
|
|
if (!roundId) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const selectedVariantIndex = normalizePositiveInteger(
|
|
|
|
|
record.selectedVariantIndex,
|
|
|
|
|
);
|
|
|
|
|
if (selectedVariantIndex) {
|
|
|
|
|
if (isUserHistoryRecord(record)) {
|
|
|
|
|
userSelectedVariantByRound.set(roundId, selectedVariantIndex);
|
|
|
|
|
} else {
|
|
|
|
|
assistantSelectedVariantByRound.set(roundId, selectedVariantIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const variantIndex = normalizePositiveInteger(record.variantIndex);
|
|
|
|
|
if (!isUserHistoryRecord(record) && variantIndex) {
|
|
|
|
|
fallbackVariantByRound.set(roundId, variantIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return dedupedRecords.filter((record) => {
|
|
|
|
|
const roundId = normalizeRoundId(record.roundId);
|
|
|
|
|
if (!roundId || isUserHistoryRecord(record)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const variantIndex = normalizePositiveInteger(record.variantIndex);
|
|
|
|
|
if (!variantIndex) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const selectedVariantIndex =
|
|
|
|
|
userSelectedVariantByRound.get(roundId) ||
|
|
|
|
|
assistantSelectedVariantByRound.get(roundId) ||
|
|
|
|
|
fallbackVariantByRound.get(roundId);
|
|
|
|
|
return !selectedVariantIndex || variantIndex === selectedVariantIndex;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dedupeHistoryRecords(records: ChatTimeHistoryRecord[]) {
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
const result: ChatTimeHistoryRecord[] = [];
|
|
|
|
|
for (const record of records) {
|
|
|
|
|
const key = resolveHistoryRecordKey(record);
|
|
|
|
|
if (seen.has(key)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
seen.add(key);
|
|
|
|
|
result.push(record);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveHistoryRecordKey(record: ChatTimeHistoryRecord) {
|
|
|
|
|
if (record.id != null) {
|
|
|
|
|
return `id:${String(record.id)}`;
|
|
|
|
|
}
|
|
|
|
|
return [
|
|
|
|
|
'fallback',
|
|
|
|
|
normalizeRoundId(record.roundId) || '',
|
|
|
|
|
normalizeRole(record.senderRole || record.role),
|
|
|
|
|
normalizePositiveInteger(record.variantIndex) || '',
|
|
|
|
|
normalizePlainText(record.contentText || record.content),
|
|
|
|
|
].join(':');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isUserHistoryRecord(record: ChatTimeHistoryRecord) {
|
|
|
|
|
return normalizeRole(record.senderRole || record.role) === 'user';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createAssistantItem(
|
|
|
|
|
created?: number | string,
|
|
|
|
|
patch?: Partial<ChatTimeAssistantItem>,
|
|
|
|
|
patch?: Omit<Partial<ChatTimeAssistantItem>, 'roundId'> & ChatTimeRoundMeta,
|
|
|
|
|
): ChatTimeAssistantItem {
|
|
|
|
|
return {
|
|
|
|
|
const item: ChatTimeAssistantItem = {
|
|
|
|
|
content: patch?.content || '',
|
|
|
|
|
created: normalizeTimestamp(created),
|
|
|
|
|
id: patch?.id || uuid(),
|
|
|
|
|
@@ -340,6 +507,8 @@ function createAssistantItem(
|
|
|
|
|
senderName: patch?.senderName,
|
|
|
|
|
typing: patch?.typing,
|
|
|
|
|
};
|
|
|
|
|
applyRoundMeta(item, patch);
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createAssistantItemFromStructuredMessage(
|
|
|
|
|
@@ -360,8 +529,15 @@ function createAssistantItemFromStructuredMessage(
|
|
|
|
|
? undefined
|
|
|
|
|
: `${String(record.id)}-assistant-${assistantIndex}`,
|
|
|
|
|
loading: false,
|
|
|
|
|
messageKind: record.messageKind,
|
|
|
|
|
roundId: normalizeRoundId(record.roundId),
|
|
|
|
|
roundNo: record.roundNo,
|
|
|
|
|
selectedVariantIndex: record.selectedVariantIndex,
|
|
|
|
|
senderName: record.senderName,
|
|
|
|
|
switchable: record.switchable,
|
|
|
|
|
typing: false,
|
|
|
|
|
variantCount: record.variantCount,
|
|
|
|
|
variantIndex: record.variantIndex,
|
|
|
|
|
});
|
|
|
|
|
if (reasoning) {
|
|
|
|
|
assistant.segments.push({
|
|
|
|
|
@@ -381,6 +557,7 @@ function createAssistantItemFromStructuredMessage(
|
|
|
|
|
function createToolItemFromChain(
|
|
|
|
|
rawChain: Record<string, any>,
|
|
|
|
|
created?: number | string,
|
|
|
|
|
record?: ChatTimeHistoryRecord,
|
|
|
|
|
) {
|
|
|
|
|
const toolCallId = normalizePlainText(rawChain.id);
|
|
|
|
|
const name = normalizePlainText(rawChain.name);
|
|
|
|
|
@@ -393,10 +570,17 @@ function createToolItemFromChain(
|
|
|
|
|
arguments: status === 'TOOL_CALL' ? argumentsValue : undefined,
|
|
|
|
|
created,
|
|
|
|
|
id: toolCallId || uuid(),
|
|
|
|
|
messageKind: record?.messageKind,
|
|
|
|
|
name,
|
|
|
|
|
roundId: record?.roundId,
|
|
|
|
|
roundNo: record?.roundNo,
|
|
|
|
|
result: status === 'TOOL_RESULT' ? argumentsValue : undefined,
|
|
|
|
|
selectedVariantIndex: record?.selectedVariantIndex,
|
|
|
|
|
status,
|
|
|
|
|
switchable: record?.switchable,
|
|
|
|
|
toolCallId,
|
|
|
|
|
variantCount: record?.variantCount,
|
|
|
|
|
variantIndex: record?.variantIndex,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -404,6 +588,7 @@ function createToolItemFromStructuredMessage(
|
|
|
|
|
rawMessage: Record<string, any>,
|
|
|
|
|
toolMetaMap: Map<string, ChatTimeToolMeta>,
|
|
|
|
|
created?: number | string,
|
|
|
|
|
record?: ChatTimeHistoryRecord,
|
|
|
|
|
) {
|
|
|
|
|
const toolCallId = normalizePlainText(
|
|
|
|
|
rawMessage.toolCallId ?? rawMessage.tool_call_id,
|
|
|
|
|
@@ -414,10 +599,17 @@ function createToolItemFromStructuredMessage(
|
|
|
|
|
arguments: toolMeta?.arguments,
|
|
|
|
|
created,
|
|
|
|
|
id: toolCallId || uuid(),
|
|
|
|
|
messageKind: record?.messageKind,
|
|
|
|
|
name: toolMeta?.name,
|
|
|
|
|
roundId: record?.roundId,
|
|
|
|
|
roundNo: record?.roundNo,
|
|
|
|
|
result,
|
|
|
|
|
selectedVariantIndex: record?.selectedVariantIndex,
|
|
|
|
|
status: 'TOOL_RESULT',
|
|
|
|
|
switchable: record?.switchable,
|
|
|
|
|
toolCallId,
|
|
|
|
|
variantCount: record?.variantCount,
|
|
|
|
|
variantIndex: record?.variantIndex,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -429,12 +621,19 @@ function createToolItemFromTopLevelRecord(record: ChatTimeHistoryRecord) {
|
|
|
|
|
return createToolItem({
|
|
|
|
|
created: record.created,
|
|
|
|
|
id: record.id == null ? toolCallId || uuid() : String(record.id),
|
|
|
|
|
messageKind: record.messageKind,
|
|
|
|
|
name: normalizePlainText(payload.name),
|
|
|
|
|
roundId: record.roundId,
|
|
|
|
|
roundNo: record.roundNo,
|
|
|
|
|
result: normalizePayloadValue(
|
|
|
|
|
payload.content ?? payload.result ?? record.contentText ?? record.content,
|
|
|
|
|
),
|
|
|
|
|
selectedVariantIndex: record.selectedVariantIndex,
|
|
|
|
|
status: 'TOOL_RESULT',
|
|
|
|
|
switchable: record.switchable,
|
|
|
|
|
toolCallId,
|
|
|
|
|
variantCount: record.variantCount,
|
|
|
|
|
variantIndex: record.variantIndex,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -442,12 +641,19 @@ function createToolItem(payload: {
|
|
|
|
|
arguments?: string;
|
|
|
|
|
created?: number | string;
|
|
|
|
|
id?: string;
|
|
|
|
|
messageKind?: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
roundId?: number | string;
|
|
|
|
|
roundNo?: number;
|
|
|
|
|
result?: string;
|
|
|
|
|
selectedVariantIndex?: number;
|
|
|
|
|
status: ChatTimeToolStatus;
|
|
|
|
|
switchable?: boolean;
|
|
|
|
|
toolCallId?: string;
|
|
|
|
|
variantCount?: number;
|
|
|
|
|
variantIndex?: number;
|
|
|
|
|
}): ChatTimeToolItem {
|
|
|
|
|
return {
|
|
|
|
|
const item: ChatTimeToolItem = {
|
|
|
|
|
arguments: payload.arguments,
|
|
|
|
|
content: payload.result || '',
|
|
|
|
|
created: normalizeTimestamp(payload.created),
|
|
|
|
|
@@ -459,10 +665,12 @@ function createToolItem(payload: {
|
|
|
|
|
status: payload.status,
|
|
|
|
|
toolCallId: payload.toolCallId || payload.id || uuid(),
|
|
|
|
|
};
|
|
|
|
|
applyRoundMeta(item, payload);
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createUserItem(record: ChatTimeHistoryRecord): ChatTimeTimelineItem {
|
|
|
|
|
return {
|
|
|
|
|
const item: ChatTimeTimelineItem = {
|
|
|
|
|
content: normalizePlainText(record.contentText || record.content),
|
|
|
|
|
created: normalizeTimestamp(record.created),
|
|
|
|
|
id: record.id == null ? uuid() : String(record.id),
|
|
|
|
|
@@ -472,6 +680,8 @@ function createUserItem(record: ChatTimeHistoryRecord): ChatTimeTimelineItem {
|
|
|
|
|
senderName: record.senderName,
|
|
|
|
|
typing: record.typing,
|
|
|
|
|
};
|
|
|
|
|
applyRoundMeta(item, record);
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendAssistantText(item: ChatTimeAssistantItem, content: string) {
|
|
|
|
|
@@ -507,14 +717,17 @@ function collectToolMeta(
|
|
|
|
|
function ensureAssistantTail(
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
created?: number | string,
|
|
|
|
|
meta?: ChatTimeRoundMeta,
|
|
|
|
|
) {
|
|
|
|
|
const last = items[items.length - 1];
|
|
|
|
|
if (isAssistantItem(last)) {
|
|
|
|
|
if (isAssistantItem(last) && isSameRoundVariant(last, meta)) {
|
|
|
|
|
applyRoundMeta(last, meta);
|
|
|
|
|
return last;
|
|
|
|
|
}
|
|
|
|
|
const assistant = createAssistantItem(created, {
|
|
|
|
|
loading: true,
|
|
|
|
|
typing: true,
|
|
|
|
|
...normalizeRoundMeta(meta),
|
|
|
|
|
});
|
|
|
|
|
items.push(assistant);
|
|
|
|
|
return assistant;
|
|
|
|
|
@@ -525,38 +738,64 @@ function ensureToolItem(
|
|
|
|
|
toolCallId?: string,
|
|
|
|
|
created?: number | string,
|
|
|
|
|
name?: string,
|
|
|
|
|
meta?: ChatTimeRoundMeta,
|
|
|
|
|
) {
|
|
|
|
|
const normalizedToolCallId = normalizePlainText(toolCallId);
|
|
|
|
|
const found = findToolItem(items, normalizedToolCallId);
|
|
|
|
|
const found = findToolItem(items, normalizedToolCallId, meta);
|
|
|
|
|
if (found) {
|
|
|
|
|
if (name) {
|
|
|
|
|
found.name = name;
|
|
|
|
|
}
|
|
|
|
|
applyRoundMeta(found, meta);
|
|
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
const toolItem = createToolItem({
|
|
|
|
|
created,
|
|
|
|
|
id: normalizedToolCallId || uuid(),
|
|
|
|
|
messageKind: meta?.messageKind,
|
|
|
|
|
name,
|
|
|
|
|
roundId: meta?.roundId,
|
|
|
|
|
roundNo: meta?.roundNo,
|
|
|
|
|
selectedVariantIndex: meta?.selectedVariantIndex,
|
|
|
|
|
status: 'TOOL_CALL',
|
|
|
|
|
switchable: meta?.switchable,
|
|
|
|
|
toolCallId: normalizedToolCallId,
|
|
|
|
|
variantCount: meta?.variantCount,
|
|
|
|
|
variantIndex: meta?.variantIndex,
|
|
|
|
|
});
|
|
|
|
|
items.push(toolItem);
|
|
|
|
|
return toolItem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findToolItem(items: ChatTimeTimelineItem[], toolCallId?: string) {
|
|
|
|
|
function findToolItem(
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
toolCallId?: string,
|
|
|
|
|
meta?: ChatTimeRoundMeta,
|
|
|
|
|
) {
|
|
|
|
|
const normalizedRoundId = normalizeRoundId(meta?.roundId);
|
|
|
|
|
const normalizedVariantIndex = normalizePositiveInteger(meta?.variantIndex);
|
|
|
|
|
if (toolCallId) {
|
|
|
|
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
const item = items[index];
|
|
|
|
|
if (isToolItem(item) && item.toolCallId === toolCallId) {
|
|
|
|
|
if (
|
|
|
|
|
isToolItem(item) &&
|
|
|
|
|
item.toolCallId === toolCallId &&
|
|
|
|
|
matchesRoundVariant(item, normalizedRoundId, normalizedVariantIndex)
|
|
|
|
|
) {
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
const item = items[index];
|
|
|
|
|
if (isToolItem(item) && item.status === 'TOOL_CALL') {
|
|
|
|
|
if (!item) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
isToolItem(item) &&
|
|
|
|
|
item.status === 'TOOL_CALL' &&
|
|
|
|
|
matchesRoundVariant(item, normalizedRoundId, normalizedVariantIndex)
|
|
|
|
|
) {
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -584,6 +823,182 @@ function isToolItem(item?: ChatTimeTimelineItem): item is ChatTimeToolItem {
|
|
|
|
|
return item?.role === 'tool';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findLastAssistant(items: ChatTimeTimelineItem[]) {
|
|
|
|
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
const item = items[index];
|
|
|
|
|
if (isAssistantItem(item)) {
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function prepareRoundVariant(
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
meta?: ChatTimeRoundMeta,
|
|
|
|
|
) {
|
|
|
|
|
const normalizedRoundId = normalizeRoundId(meta?.roundId);
|
|
|
|
|
const normalizedVariantIndex = normalizePositiveInteger(meta?.variantIndex);
|
|
|
|
|
if (!normalizedRoundId || !normalizedVariantIndex) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const assistant = items.find(
|
|
|
|
|
(item) => item.role === 'assistant' && item.roundId === normalizedRoundId,
|
|
|
|
|
);
|
|
|
|
|
if (assistant?.variantIndex === normalizedVariantIndex) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
const item = items[index];
|
|
|
|
|
if (!item) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (item.roundId === normalizedRoundId && item.role !== 'user') {
|
|
|
|
|
items.splice(index, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveRoundInsertIndex(
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
roundId: string,
|
|
|
|
|
) {
|
|
|
|
|
const firstRoundItemIndex = items.findIndex(
|
|
|
|
|
(item) => item.roundId === roundId && item.role !== 'user',
|
|
|
|
|
);
|
|
|
|
|
if (firstRoundItemIndex >= 0) {
|
|
|
|
|
return firstRoundItemIndex;
|
|
|
|
|
}
|
|
|
|
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
const item = items[index];
|
|
|
|
|
if (!item) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (item.roundId === roundId && item.role === 'user') {
|
|
|
|
|
return index + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return items.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveRoundReplaceRange(
|
|
|
|
|
items: ChatTimeTimelineItem[],
|
|
|
|
|
roundId: string,
|
|
|
|
|
) {
|
|
|
|
|
let start = -1;
|
|
|
|
|
let end = -1;
|
|
|
|
|
for (let index = 0; index < items.length; index += 1) {
|
|
|
|
|
const item = items[index];
|
|
|
|
|
if (item?.roundId === roundId && item.role !== 'user') {
|
|
|
|
|
if (start < 0) {
|
|
|
|
|
start = index;
|
|
|
|
|
}
|
|
|
|
|
end = index;
|
|
|
|
|
} else if (start >= 0) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (start < 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
deleteCount: end - start + 1,
|
|
|
|
|
start,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function matchesRoundVariant(
|
|
|
|
|
item: ChatTimeTimelineItem,
|
|
|
|
|
roundId?: string,
|
|
|
|
|
variantIndex?: number,
|
|
|
|
|
) {
|
|
|
|
|
if (roundId && item.roundId !== roundId) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (variantIndex && item.variantIndex && item.variantIndex !== variantIndex) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSameRoundVariant(
|
|
|
|
|
item: ChatTimeTimelineItem,
|
|
|
|
|
meta?: ChatTimeRoundMeta,
|
|
|
|
|
) {
|
|
|
|
|
const normalizedRoundId = normalizeRoundId(meta?.roundId);
|
|
|
|
|
const normalizedVariantIndex = normalizePositiveInteger(meta?.variantIndex);
|
|
|
|
|
if (!normalizedRoundId || !normalizedVariantIndex) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
item.roundId === normalizedRoundId &&
|
|
|
|
|
normalizePositiveInteger(item.variantIndex) === normalizedVariantIndex
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyRoundMeta(
|
|
|
|
|
target: Partial<ChatTimeTimelineItemBase>,
|
|
|
|
|
source?: ChatTimeRoundMeta | null,
|
|
|
|
|
) {
|
|
|
|
|
if (!source) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const roundId = normalizeRoundId(source.roundId);
|
|
|
|
|
if (roundId) {
|
|
|
|
|
target.roundId = roundId;
|
|
|
|
|
}
|
|
|
|
|
const roundNo = normalizePositiveInteger(source.roundNo);
|
|
|
|
|
if (roundNo) {
|
|
|
|
|
target.roundNo = roundNo;
|
|
|
|
|
}
|
|
|
|
|
const variantIndex = normalizePositiveInteger(source.variantIndex);
|
|
|
|
|
if (variantIndex) {
|
|
|
|
|
target.variantIndex = variantIndex;
|
|
|
|
|
}
|
|
|
|
|
const variantCount = normalizePositiveInteger(source.variantCount);
|
|
|
|
|
if (variantCount) {
|
|
|
|
|
target.variantCount = variantCount;
|
|
|
|
|
}
|
|
|
|
|
const selectedVariantIndex = normalizePositiveInteger(
|
|
|
|
|
source.selectedVariantIndex,
|
|
|
|
|
);
|
|
|
|
|
if (selectedVariantIndex) {
|
|
|
|
|
target.selectedVariantIndex = selectedVariantIndex;
|
|
|
|
|
}
|
|
|
|
|
if (typeof source.switchable === 'boolean') {
|
|
|
|
|
target.switchable = source.switchable;
|
|
|
|
|
}
|
|
|
|
|
const messageKind = normalizePlainText(source.messageKind).trim();
|
|
|
|
|
if (messageKind) {
|
|
|
|
|
target.messageKind = messageKind;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeRoundMeta(meta?: ChatTimeRoundMeta): ChatTimeRoundMeta {
|
|
|
|
|
return {
|
|
|
|
|
messageKind: meta?.messageKind,
|
|
|
|
|
roundId: normalizeRoundId(meta?.roundId),
|
|
|
|
|
roundNo: meta?.roundNo,
|
|
|
|
|
selectedVariantIndex: meta?.selectedVariantIndex,
|
|
|
|
|
switchable: meta?.switchable,
|
|
|
|
|
variantCount: meta?.variantCount,
|
|
|
|
|
variantIndex: meta?.variantIndex,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeRoundId(value: any) {
|
|
|
|
|
const normalized = normalizePlainText(value).trim();
|
|
|
|
|
return normalized || undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePositiveInteger(value: any) {
|
|
|
|
|
if (value == null || value === '') {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
const parsed = Number.parseInt(String(value), 10);
|
|
|
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeAssistantText(value: any) {
|
|
|
|
|
return normalizePlainText(value)
|
|
|
|
|
.replace(/^Final Answer:\s*/i, '')
|
|
|
|
|
|