comment changes

This commit is contained in:
Aadesh Kheria 2026-04-13 12:13:33 -07:00
parent 8c7bc548b8
commit 7a54be90b0
20 changed files with 223 additions and 168 deletions

View File

@ -16,7 +16,6 @@ export const POST = createSmartRouteHandler({
question: yupString().defined(),
answer: yupString().defined(),
publish: yupBoolean().defined(),
reviewedBy: yupString().defined(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
@ -40,13 +39,13 @@ export const POST = createSmartRouteHandler({
throw new StatusError(503, "SpacetimeDB unavailable");
}
const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me");
const token = getEnvVariable("STACK_MCP_LOG_TOKEN");
await conn.reducers.addManualQa({
token,
question: body.question,
answer: body.answer,
publish: body.publish,
reviewedBy: body.reviewedBy,
reviewedBy: fullReq.auth.user.display_name ?? fullReq.auth.user.primary_email ?? fullReq.auth.user.id,
});
return {

View File

@ -37,7 +37,7 @@ export const POST = createSmartRouteHandler({
throw new StatusError(503, "SpacetimeDB unavailable");
}
const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me");
const token = getEnvVariable("STACK_MCP_LOG_TOKEN");
await conn.reducers.deleteQaEntry({
token,
correlationId: body.correlationId,

View File

@ -14,7 +14,6 @@ export const POST = createSmartRouteHandler({
}).defined(),
body: yupObject({
correlationId: yupString().defined(),
reviewedBy: yupString().defined(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
@ -38,11 +37,11 @@ export const POST = createSmartRouteHandler({
throw new StatusError(503, "SpacetimeDB unavailable");
}
const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me");
const token = getEnvVariable("STACK_MCP_LOG_TOKEN");
await conn.reducers.markHumanReviewed({
token,
correlationId: body.correlationId,
reviewedBy: body.reviewedBy,
reviewedBy: fullReq.auth.user.display_name ?? fullReq.auth.user.primary_email ?? fullReq.auth.user.id,
});
return {

View File

@ -17,7 +17,6 @@ export const POST = createSmartRouteHandler({
correctedQuestion: yupString().defined(),
correctedAnswer: yupString().defined(),
publish: yupBoolean().defined(),
reviewedBy: yupString().defined(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
@ -41,14 +40,14 @@ export const POST = createSmartRouteHandler({
throw new StatusError(503, "SpacetimeDB unavailable");
}
const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me");
const token = getEnvVariable("STACK_MCP_LOG_TOKEN");
await conn.reducers.updateHumanCorrection({
token,
correlationId: body.correlationId,
correctedQuestion: body.correctedQuestion,
correctedAnswer: body.correctedAnswer,
publish: body.publish,
reviewedBy: body.reviewedBy,
reviewedBy: fullReq.auth.user.display_name ?? fullReq.auth.user.primary_email ?? fullReq.auth.user.id,
});
return {

View File

@ -113,8 +113,8 @@ You are Stack Auth's AI assistant. You help users with Stack Auth - a complete a
Think step by step about what to say. Being wrong is 100x worse than saying you don't know.
## PRIORITY ORDER:
1. **FIRST**, check the Human-Verified Knowledge Base (appended at the end of this prompt, if any). If the user's question matches or is similar to a verified Q&A, use that answer exactly do not search docs or use any other source.
2. **THEN**, use \`search_docs\` with relevant keywords to find related documentation
1. **FIRST**, check the Human-Verified Knowledge Base (appended at the end of this prompt, if any). If the user's question is an exact or near-exact match to a verified Q&A, you may use that answer verbatim without searching docs.
2. **OTHERWISE**, use \`search_docs\` with relevant keywords to find related documentation — this is mandatory when there is no exact verified-QA match.
3. **THEN**, use \`get_docs_by_id\` to retrieve the full content of the most relevant pages
4. Base your answer on the actual documentation content retrieved
5. When referring to API endpoints, **always cite the actual endpoint** (e.g., "GET /users/me") not the documentation URL

View File

@ -111,7 +111,7 @@ export async function reviewMcpCall(entry: {
});
const conversation = result.steps.map((step, i) => {
const toolCalls = step.toolCalls.map(tc => ({ toolName: tc.toolName, args: tc.input }));
const toolCalls = step.toolCalls.map(tc => ({ toolName: tc.toolName, toolCallId: tc.toolCallId, args: tc.input }));
const toolResults = step.toolResults.map(tr => ({
toolName: tr.toolName,
toolCallId: tr.toolCallId,
@ -129,7 +129,17 @@ export async function reviewMcpCall(entry: {
if (!jsonMatch) {
throw new Error("No JSON found in QA review response");
}
const parsed = JSON.parse(jsonMatch[0]) as {
const raw = JSON.parse(jsonMatch[0]);
if (
typeof raw.needsHumanReview !== "boolean" ||
typeof raw.answerCorrect !== "boolean" ||
typeof raw.answerRelevant !== "boolean" ||
!Array.isArray(raw.flags) ||
typeof raw.overallScore !== "number"
) {
throw new Error(`Invalid QA review response shape: ${JSON.stringify(raw).slice(0, 200)}`);
}
const parsed = raw as {
needsHumanReview: boolean,
answerCorrect: boolean,
answerRelevant: boolean,
@ -137,6 +147,7 @@ export async function reviewMcpCall(entry: {
improvementSuggestions: string,
overallScore: number,
};
parsed.overallScore = Math.max(0, Math.min(100, Math.round(parsed.overallScore)));
update = {
qaNeedsHumanReview: parsed.needsHumanReview,

View File

@ -23,29 +23,34 @@ function getDocsToolsBaseUrl(): string {
async function postDocsToolAction(action: Record<string, unknown>): Promise<string> {
const base = getDocsToolsBaseUrl();
const res = await fetch(`${base}/api/internal/docs-tools`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(action),
});
try {
const res = await fetch(`${base}/api/internal/docs-tools`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(action),
});
if (!res.ok) {
const errBody = await res.text();
captureError("docs-tools-http-error", new Error(`Stack Auth docs tools error (${res.status}): ${errBody}`));
return `Stack Auth docs tools error (${res.status}): ${errBody}`;
if (!res.ok) {
const errBody = await res.text();
captureError("docs-tools-http-error", new Error(`Stack Auth docs tools error (${res.status}): ${errBody}`));
return `Stack Auth docs tools error (${res.status}): ${errBody}`;
}
const data = (await res.json()) as DocsToolHttpResult;
const text = data.content
?.filter((c): c is { type: "text", text: string } => c.type === "text" && typeof c.text === "string")
.map((c) => c.text)
.join("\n") ?? "";
if (data.isError === true) {
return text || "Unknown docs tool error";
}
return text;
} catch (err) {
captureError("docs-tools-transport-error", err instanceof Error ? err : new Error(String(err)));
return `Stack Auth docs tools error: ${err instanceof Error ? err.message : String(err)}`;
}
const data = (await res.json()) as DocsToolHttpResult;
const text = data.content
?.filter((c): c is { type: "text", text: string } => c.type === "text" && typeof c.text === "string")
.map((c) => c.text)
.join("\n") ?? "";
if (data.isError === true) {
return text || "Unknown docs tool error";
}
return text;
}
/**

View File

@ -22,15 +22,19 @@ if (target === "prod" && !process.env.STACK_MCP_LOG_TOKEN) {
process.exit(1);
}
// Inject token
const inject = spawnSync("node", ["scripts/spacetime-token.mjs", "inject"], { stdio: "inherit" });
if (inject.status !== 0) {
process.exit(inject.status ?? 1);
}
let exitCode = 1;
try {
const publish = spawnSync("spacetime", args, { stdio: "inherit" });
process.exitCode = publish.status ?? 1;
const inject = spawnSync("node", ["scripts/spacetime-token.mjs", "inject"], { stdio: "inherit" });
if (inject.status !== 0) {
exitCode = inject.status ?? 1;
} else {
const publish = spawnSync("spacetime", args, { stdio: "inherit" });
exitCode = publish.status ?? 1;
}
} finally {
spawnSync("node", ["scripts/spacetime-token.mjs", "restore"], { stdio: "inherit" });
const restore = spawnSync("node", ["scripts/spacetime-token.mjs", "restore"], { stdio: "inherit" });
if (restore.status !== 0 && exitCode === 0) {
exitCode = restore.status ?? 1;
}
process.exitCode = exitCode;
}

View File

@ -13,12 +13,19 @@ const action = process.argv[2];
if (action === "inject") {
const token = process.env.STACK_MCP_LOG_TOKEN || "change-me";
if (existsSync(BACKUP)) {
console.error("Refusing to inject: backup already exists. Run restore first.");
process.exit(1);
}
const content = readFileSync(TARGET, "utf8");
writeFileSync(BACKUP, content, "utf8");
writeFileSync(TARGET, content.replaceAll(PLACEHOLDER, token), "utf8");
const escapedToken = JSON.stringify(token).slice(1, -1);
writeFileSync(TARGET, content.replaceAll(PLACEHOLDER, escapedToken), "utf8");
} else if (action === "restore") {
if (existsSync(BACKUP)) {
unlinkSync(TARGET);
if (existsSync(TARGET)) {
unlinkSync(TARGET);
}
renameSync(BACKUP, TARGET);
}
} else {

View File

@ -174,7 +174,7 @@ export const update_human_correction = spacetimedb.reducer(
humanReviewedAt: row.humanReviewedAt ?? ctx.timestamp,
humanReviewedBy: row.humanReviewedBy ?? args.reviewedBy,
publishedToQa: args.publish,
publishedAt: args.publish ? (row.publishedAt ?? ctx.timestamp) : row.publishedAt,
publishedAt: args.publish ? (row.publishedAt ?? ctx.timestamp) : undefined,
});
return;
}

View File

@ -63,7 +63,6 @@ export default function App() {
: null;
const currentUser = user;
const reviewedBy = currentUser.displayName ?? currentUser.primaryEmail ?? "unknown";
async function getApi() {
const { accessToken, refreshToken } = await currentUser.getAuthJson();
@ -132,10 +131,9 @@ export default function App() {
{showAddQa && (
<AddManualQa
onClose={() => setShowAddQa(false)}
onSave={(question, answer, publish) => {
getApi()
.then(api => api.addManual({ question, answer, publish, reviewedBy }))
.catch(() => { /* errors are surfaced by UI state */ });
onSave={async (question, answer, publish) => {
const api = await getApi();
await api.addManual({ question, answer, publish });
}}
/>
)}
@ -158,12 +156,12 @@ export default function App() {
onClose={() => setSelectedRow(null)}
onSaveCorrection={(correlationId, correctedQuestion, correctedAnswer, publish) => {
getApi()
.then(api => api.updateCorrection({ correlationId, correctedQuestion, correctedAnswer, publish, reviewedBy }))
.then(api => api.updateCorrection({ correlationId, correctedQuestion, correctedAnswer, publish }))
.catch(() => { /* errors are surfaced by UI state */ });
}}
onMarkReviewed={(correlationId) => {
getApi()
.then(api => api.markReviewed({ correlationId, reviewedBy }))
.then(api => api.markReviewed({ correlationId }))
.catch(() => { /* errors are surfaced by UI state */ });
}}
/>
@ -178,7 +176,7 @@ export default function App() {
rows={rows}
onSave={(correlationId, question, answer, publish) => {
getApi()
.then(api => api.updateCorrection({ correlationId, correctedQuestion: question, correctedAnswer: answer, publish, reviewedBy }))
.then(api => api.updateCorrection({ correlationId, correctedQuestion: question, correctedAnswer: answer, publish }))
.catch(() => { /* errors are surfaced by UI state */ });
}}
onDelete={(correlationId) => {

View File

@ -3,26 +3,36 @@ import { clsx } from "clsx";
export function AddManualQa({ onClose, onSave }: {
onClose: () => void;
onSave: (question: string, answer: string, publish: boolean) => void;
onSave: (question: string, answer: string, publish: boolean) => Promise<void>;
}) {
const [question, setQuestion] = useState("");
const [answer, setAnswer] = useState("");
const [saved, setSaved] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const canSave = question.trim().length > 0 && answer.trim().length > 0;
const canSave = question.trim().length > 0 && answer.trim().length > 0 && !isSaving;
const handleSave = (publish: boolean) => {
const handleSave = async (publish: boolean) => {
if (!canSave) return;
onSave(question.trim(), answer.trim(), publish);
setSaved(true);
setTimeout(() => {
setSaved(false);
setQuestion("");
setAnswer("");
if (publish) {
onClose();
}
}, 1500);
setIsSaving(true);
setError(null);
try {
await onSave(question.trim(), answer.trim(), publish);
setSaved(true);
setTimeout(() => {
setSaved(false);
setQuestion("");
setAnswer("");
if (publish) {
onClose();
}
}, 1500);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save");
} finally {
setIsSaving(false);
}
};
return (
@ -43,6 +53,11 @@ export function AddManualQa({ onClose, onSave }: {
Saved successfully
</div>
)}
{error && (
<div className="px-3 py-1.5 rounded text-xs font-medium bg-red-50 text-red-700">
{error}
</div>
)}
<div>
<label className="text-[10px] uppercase text-gray-400 font-medium mb-1 block tracking-wider">Question</label>

View File

@ -71,7 +71,7 @@ export function Analytics({ rows }: { rows: McpCallLogRow[] }) {
// Duration stats
const durations = rows.map(r => Number(r.durationMs)).filter(d => d > 0).sort((a, b) => a - b);
const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
const p95Duration = durations.length > 0 ? durations[Math.floor(durations.length * 0.95)] : 0;
const p95Duration = durations.length > 0 ? durations[Math.min(Math.floor(durations.length * 0.95), durations.length - 1)] : 0;
const maxDuration = durations.length > 0 ? durations[durations.length - 1] : 0;
// Tool usage

View File

@ -16,9 +16,12 @@ function CopyButton({ text }: { text: string }) {
<button
className="text-xs text-blue-500 hover:text-blue-700 ml-2"
onClick={() => {
navigator.clipboard.writeText(text).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 1500);
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, (err) => {
console.error("Clipboard write failed:", err);
});
}}
>
{copied ? "copied" : "copy"}
@ -32,8 +35,8 @@ export function CallLogDetail({ row, allRows, onClose, onSaveCorrection, onMarkR
row: McpCallLogRow;
allRows: McpCallLogRow[];
onClose: () => void;
onSaveCorrection?: (correlationId: string, correctedQuestion: string, correctedAnswer: string, publish: boolean) => void;
onMarkReviewed?: (correlationId: string) => void;
onSaveCorrection?: (correlationId: string, correctedQuestion: string, correctedAnswer: string, publish: boolean) => Promise<void> | void;
onMarkReviewed?: (correlationId: string) => Promise<void> | void;
}) {
const [showReplay, setShowReplay] = useState(false);
const isReviewed = row.humanReviewedAt != null;
@ -373,22 +376,32 @@ async function fetchDeepWikiAnswer(questionText: string): Promise<string> {
function HumanCorrectionCard({ row, onSave }: {
row: McpCallLogRow;
onSave?: (correlationId: string, correctedQuestion: string, correctedAnswer: string, publish: boolean) => void;
onSave?: (correlationId: string, correctedQuestion: string, correctedAnswer: string, publish: boolean) => Promise<void> | void;
}) {
const [question, setQuestion] = useState(row.humanCorrectedQuestion ?? "");
const [answer, setAnswer] = useState(row.humanCorrectedAnswer ?? "");
const [lastAction, setLastAction] = useState<"published" | "saved" | "deepwiki-error" | null>(null);
const [lastAction, setLastAction] = useState<"published" | "saved" | "deepwiki-error" | "error" | null>(null);
const [deepWikiLoading, setDeepWikiLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
setQuestion(row.humanCorrectedQuestion ?? "");
setAnswer(row.humanCorrectedAnswer ?? "");
}, [row.humanCorrectedQuestion, row.humanCorrectedAnswer, row.correlationId]);
const handleSave = (publish: boolean) => {
onSave?.(row.correlationId, question, answer, publish);
setLastAction(publish ? "published" : "saved");
setTimeout(() => setLastAction(null), 3000);
const handleSave = async (publish: boolean) => {
if (isSaving) return;
setIsSaving(true);
try {
await onSave?.(row.correlationId, question, answer, publish);
setLastAction(publish ? "published" : "saved");
setTimeout(() => setLastAction(null), 3000);
} catch {
setLastAction("error");
setTimeout(() => setLastAction(null), 3000);
} finally {
setIsSaving(false);
}
};
const hasUnsavedChanges =
@ -441,12 +454,13 @@ function HumanCorrectionCard({ row, onSave }: {
<div className={clsx(
"px-3 py-1.5 rounded text-xs font-medium",
lastAction === "published" ? "bg-green-100 text-green-700" :
lastAction === "deepwiki-error" ? "bg-red-100 text-red-700" :
lastAction === "deepwiki-error" || lastAction === "error" ? "bg-red-100 text-red-700" :
"bg-blue-100 text-blue-700"
)}>
{lastAction === "published" ? "Published to /questions" :
lastAction === "deepwiki-error" ? "Failed to fetch from DeepWiki" :
"Draft saved"}
lastAction === "error" ? "Failed to save" :
"Draft saved"}
</div>
)}
@ -627,16 +641,15 @@ function QaToolCard({ pair }: { pair: { toolName: string; args: unknown; result:
</div>
{resultStr != null && (
<div className="px-3 py-2 border-t border-indigo-100">
<button
className="w-full flex items-center justify-between text-[10px] uppercase text-gray-400 font-medium hover:text-gray-600"
onClick={() => setResultExpanded(prev => !prev)}
>
<span>Result ({formatByteSize(pair.result)})</span>
<span className="flex items-center gap-1">
{resultExpanded && <CopyButton text={resultStr} />}
<span>{resultExpanded ? "collapse" : "expand"}</span>
</span>
</button>
<div className="w-full flex items-center justify-between text-[10px] uppercase text-gray-400 font-medium">
<button
className="hover:text-gray-600"
onClick={() => setResultExpanded(prev => !prev)}
>
Result ({formatByteSize(pair.result)}) {resultExpanded ? "collapse" : "expand"}
</button>
{resultExpanded && <CopyButton text={resultStr} />}
</div>
{resultExpanded && (
<pre className="mt-1 text-xs text-gray-600 overflow-auto max-h-64 whitespace-pre-wrap">{resultStr}</pre>
)}

View File

@ -27,9 +27,12 @@ function CopyButton({ text }: { text: string }) {
className="shrink-0 rounded p-0.5 transition-colors text-gray-400 hover:text-gray-600 hover:bg-gray-100"
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(text).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 1500);
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, (err) => {
console.error("Clipboard write failed:", err);
});
}}
>
<span className="text-[10px]">{copied ? "copied" : "copy"}</span>

View File

@ -8,9 +8,12 @@ function CopyBtn({ text, size = "xs" }: { text: string; size?: "xs" | "sm" }) {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigator.clipboard.writeText(text).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 1500);
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, (err) => {
console.error("Clipboard write failed:", err);
});
}}
className={clsx(
"shrink-0 rounded transition-colors",
@ -66,8 +69,6 @@ const CodeBlock = memo(function CodeBlock({ children, className }: { children?:
});
const SmartLink = memo(function SmartLink({ href, children }: { href?: string; children?: React.ReactNode }) {
const displayText = String(children || href || "");
return (
<a
href={href}
@ -75,7 +76,7 @@ const SmartLink = memo(function SmartLink({ href, children }: { href?: string; c
target="_blank"
rel="noopener noreferrer"
>
{displayText}
{children ?? href ?? ""}
</a>
);
});

View File

@ -6,8 +6,13 @@ const IS_DEV = process.env.NODE_ENV === "development";
const PLACEHOLDER = "REPLACE_ME";
const rawHost = process.env.NEXT_PUBLIC_SPACETIMEDB_HOST;
const rawDbName = process.env.NEXT_PUBLIC_SPACETIMEDB_DB_NAME;
const HOST = (!rawHost || rawHost === PLACEHOLDER) ? (IS_DEV ? "ws://localhost:8139" : "") : rawHost;
const DB_NAME = (!rawDbName || rawDbName === PLACEHOLDER) ? (IS_DEV ? "stack-auth-llm" : "") : rawDbName;
function resolveEnv(raw: string | undefined, devDefault: string, name: string): string {
if (raw && raw !== PLACEHOLDER) return raw;
if (IS_DEV) return devDefault;
throw new Error(`${name} is not configured. Set it in .env.local or hosting platform env.`);
}
const HOST = resolveEnv(rawHost, "ws://localhost:8139", "NEXT_PUBLIC_SPACETIMEDB_HOST");
const DB_NAME = resolveEnv(rawDbName, "stack-auth-llm", "NEXT_PUBLIC_SPACETIMEDB_DB_NAME");
const TOKEN_KEY = `spacetimedb_${HOST}/${DB_NAME}/auth_token`;
const MAX_RETRIES = 5;
@ -39,76 +44,70 @@ export function useMcpCallLogs() {
retryTimer = setTimeout(() => {
retryTimer = null;
if (!cancelled) {
connect().catch(() => {});
connect();
}
}, RETRY_DELAY_MS);
}
async function connect() {
try {
const conn = DbConnection.builder()
.withUri(HOST)
.withDatabaseName(DB_NAME)
.withToken(localStorage.getItem(TOKEN_KEY) || undefined)
.onConnect((connInstance: DbConnection, _identity: unknown, token: string) => {
function connect() {
const conn = DbConnection.builder()
.withUri(HOST)
.withDatabaseName(DB_NAME)
.withToken(localStorage.getItem(TOKEN_KEY) || undefined)
.onConnect((connInstance: DbConnection, _identity: unknown, token: string) => {
if (cancelled) return;
console.log("[SpacetimeDB] Connected successfully");
retryCount = 0;
localStorage.setItem(TOKEN_KEY, token);
connRef.current = connInstance;
connInstance.subscriptionBuilder()
.onApplied((ctx: SubscriptionEventContext) => {
if (cancelled) return;
const initialRows: McpCallLogRow[] = [];
for (const row of ctx.db.mcpCallLog.iter()) {
initialRows.push(row);
}
initialRows.sort((a, b) => Number(b.id - a.id));
console.log("[SpacetimeDB] Loaded", initialRows.length, "rows");
setRows(initialRows);
setConnectionState("connected");
})
.subscribe(`SELECT * FROM mcp_call_log`);
connInstance.db.mcpCallLog.onInsert((_ctx: EventContext, row: McpCallLogRow) => {
if (cancelled) return;
console.log("[SpacetimeDB] Connected successfully");
retryCount = 0;
localStorage.setItem(TOKEN_KEY, token);
connRef.current = connInstance;
connInstance.subscriptionBuilder()
.onApplied((ctx: SubscriptionEventContext) => {
if (cancelled) return;
const initialRows: McpCallLogRow[] = [];
for (const row of ctx.db.mcpCallLog.iter()) {
initialRows.push(row);
}
initialRows.sort((a, b) => Number(b.id - a.id));
console.log("[SpacetimeDB] Loaded", initialRows.length, "rows");
setRows(initialRows);
setConnectionState("connected");
})
.subscribe(`SELECT * FROM mcp_call_log`);
connInstance.db.mcpCallLog.onInsert((_ctx: EventContext, row: McpCallLogRow) => {
if (cancelled) return;
setRows(prev => {
const existing = prev.findIndex(r => r.id === row.id);
if (existing >= 0) {
const updated = [...prev];
updated[existing] = row;
return updated;
}
return [row, ...prev];
});
setRows(prev => {
const existing = prev.findIndex(r => r.id === row.id);
if (existing >= 0) {
const updated = [...prev];
updated[existing] = row;
return updated;
}
return [row, ...prev];
});
});
connInstance.db.mcpCallLog.onDelete((_ctx: EventContext, row: McpCallLogRow) => {
if (cancelled) return;
setRows(prev => prev.filter(r => r.id !== row.id));
});
})
.onConnectError((_ctx: unknown, err: unknown) => {
console.error("[SpacetimeDB] Connection error:", err);
// Clear stale token if present
const storedToken = localStorage.getItem(TOKEN_KEY);
if (storedToken) {
console.log("[SpacetimeDB] Clearing stale token");
localStorage.removeItem(TOKEN_KEY);
}
retry();
})
.build();
connInstance.db.mcpCallLog.onDelete((_ctx: EventContext, row: McpCallLogRow) => {
if (cancelled) return;
setRows(prev => prev.filter(r => r.id !== row.id));
});
})
.onConnectError((_ctx: unknown, err: unknown) => {
console.error("[SpacetimeDB] Connection error:", err);
const storedToken = localStorage.getItem(TOKEN_KEY);
if (storedToken) {
console.log("[SpacetimeDB] Clearing stale token");
localStorage.removeItem(TOKEN_KEY);
}
retry();
})
.build();
connRef.current = conn;
} catch (err) {
console.error("[SpacetimeDB] Failed to build connection:", err);
retry();
}
connRef.current = conn;
}
connect().catch(() => {});
connect();
return () => {
cancelled = true;

View File

@ -36,7 +36,7 @@ async function post(path: string, body: unknown, authHeaders: Record<string, str
export function makeMcpReviewApi(authHeaders: Record<string, string>) {
return {
markReviewed: (body: { correlationId: string; reviewedBy: string }) =>
markReviewed: (body: { correlationId: string }) =>
post("mark-reviewed", body, authHeaders),
updateCorrection: (body: {
@ -44,14 +44,12 @@ export function makeMcpReviewApi(authHeaders: Record<string, string>) {
correctedQuestion: string;
correctedAnswer: string;
publish: boolean;
reviewedBy: string;
}) => post("update-correction", body, authHeaders),
addManual: (body: {
question: string;
answer: string;
publish: boolean;
reviewedBy: string;
}) => post("add-manual", body, authHeaders),
delete: (body: { correlationId: string }) =>

View File

@ -5,7 +5,10 @@
export function toDate(ts: unknown): Date {
if (ts instanceof Date) return ts;
if (typeof ts === "object" && ts !== null && "__timestamp_micros_since_unix_epoch__" in ts) {
const micros = (ts as { __timestamp_micros_since_unix_epoch__: bigint }).__timestamp_micros_since_unix_epoch__;
const micros = (ts as Record<string, unknown>).__timestamp_micros_since_unix_epoch__;
if (typeof micros !== "bigint") {
throw new TypeError(`Expected __timestamp_micros_since_unix_epoch__ to be bigint, got ${typeof micros}`);
}
return new Date(Number(micros / 1000n));
}
if (typeof ts === "bigint") {
@ -14,5 +17,5 @@ export function toDate(ts: unknown): Date {
if (typeof ts === "number") {
return new Date(ts);
}
return new Date(0);
throw new TypeError(`Cannot convert ${typeof ts} to Date`);
}

View File

@ -72,6 +72,7 @@ async function extractOpenApiDetails(
text: errorText,
},
],
isError: true,
};
}
}