Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

186 lines
5.1 KiB
TypeScript

"use client";
import {
AssistantRuntimeProvider,
type FeedbackAdapter,
SimpleImageAttachmentAdapter,
useAssistantInstructions,
useAui,
useAuiEvent,
} from "@assistant-ui/react";
import {
useChatRuntime,
AssistantChatTransport,
getThreadMessageTokenUsage,
} from "@assistant-ui/react-ai-sdk";
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
import { useEffect, useRef, type ReactNode } from "react";
import { useCurrentPage } from "@/components/docs/contexts/current-page";
import { analytics } from "@/lib/analytics";
import {
consumeRunStartedAt,
pruneStaleRunStarts,
queueMicrotaskSafe,
recordRunStartedAt,
} from "@/lib/assistant-analytics-helpers";
import { countToolCalls, getTextLength } from "@/lib/assistant-metrics";
type ThreadMessagePart = { type: string; text?: string };
const RUN_STARTED_AT_STALE_THRESHOLD_MS = 30 * 60_000;
const RUN_STARTED_AT_CLEANUP_INTERVAL_MS = 60_000;
function getLastAssistantMessage(
messages: readonly {
role?: string;
content?: readonly ThreadMessagePart[];
status?: { type: string; reason?: string };
metadata?: unknown;
}[],
) {
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
const message = messages[idx];
if (message?.role === "assistant") return message;
}
return undefined;
}
// Stateless adapter - safe to share across instances
const feedbackAdapter: FeedbackAdapter = {
submit: () => {
// Feedback is tracked via analytics in AssistantActionBar
// The runtime automatically updates message.metadata.submittedFeedback
},
};
function AssistantPageContext() {
const currentPage = useCurrentPage();
const pathname = currentPage?.pathname;
useAssistantInstructions({
instruction: pathname
? `The user is currently viewing: ${pathname}`
: "The user is on the docs site.",
disabled: !pathname,
});
return null;
}
function AssistantAnalyticsTracker() {
const aui = useAui();
const currentPage = useCurrentPage();
const pathname = currentPage?.pathname;
const pathnameRef = useRef<string | undefined>(pathname);
useEffect(() => {
pathnameRef.current = pathname;
}, [pathname]);
const runStartedAtRef = useRef(new Map<string, number[]>());
useEffect(() => {
const interval = setInterval(() => {
pruneStaleRunStarts(
runStartedAtRef.current,
Date.now(),
RUN_STARTED_AT_STALE_THRESHOLD_MS,
);
}, RUN_STARTED_AT_CLEANUP_INTERVAL_MS);
return () => clearInterval(interval);
}, []);
useAuiEvent("thread.runStart", (event) => {
recordRunStartedAt(runStartedAtRef.current, event.threadId, Date.now());
});
useAuiEvent("thread.runEnd", (event) => {
const startedAt = consumeRunStartedAt(
runStartedAtRef.current,
event.threadId,
);
const latencyMs =
startedAt === undefined ? undefined : Date.now() - startedAt;
queueMicrotaskSafe(() => {
const messages = (() => {
try {
return aui.thread().getState().messages;
} catch {
return [];
}
})();
const lastAssistant = getLastAssistantMessage(messages);
const responseLength = getTextLength(lastAssistant?.content ?? []);
const toolCallsCount = countToolCalls(lastAssistant?.content ?? []);
const status = lastAssistant?.status;
const tokenUsage = getThreadMessageTokenUsage(lastAssistant);
let modelName: string | undefined;
try {
modelName = aui.thread().getModelContext()?.config?.modelName;
} catch {
// ignore
}
const payload: Parameters<
typeof analytics.assistant.responseCompleted
>[0] = {
threadId: event.threadId,
response_length: responseLength,
tool_calls_count: toolCallsCount,
...(latencyMs === undefined ? {} : { latency_ms: latencyMs }),
...(status?.reason ? { status_reason: status.reason } : {}),
...(tokenUsage?.totalTokens === undefined
? {}
: { response_total_tokens: tokenUsage.totalTokens }),
...(tokenUsage?.inputTokens === undefined
? {}
: { response_input_tokens: tokenUsage.inputTokens }),
...(tokenUsage?.outputTokens === undefined
? {}
: { response_output_tokens: tokenUsage.outputTokens }),
...(pathnameRef.current ? { pathname: pathnameRef.current } : {}),
...(modelName ? { model_name: modelName } : {}),
};
if (status?.type === "incomplete") {
analytics.assistant.responseFailed(payload);
return;
}
analytics.assistant.responseCompleted(payload);
});
});
return null;
}
export function DocsAssistantRuntimeProvider({
children,
}: {
children: ReactNode;
}) {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({
api: "/api/doc/chat",
}),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
adapters: {
feedback: feedbackAdapter,
attachments: new SimpleImageAttachmentAdapter(),
},
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<AssistantAnalyticsTracker />
<AssistantPageContext />
{children}
</AssistantRuntimeProvider>
);
}