bot fixes

This commit is contained in:
Aadesh Kheria 2026-04-20 15:14:34 -07:00
parent 26ce83f935
commit f9386a886e
8 changed files with 98 additions and 64 deletions

View File

@ -1,5 +1,4 @@
import { useUser } from "@stackframe/stack";
import { captureError } from "@stackframe/stack-shared/dist/utils/errors";
import { clsx } from "clsx";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { Identity } from "spacetimedb";
@ -205,21 +204,15 @@ export default function App() {
row={currentSelectedRow}
allRows={rows}
onClose={() => setSelectedRow(null)}
onSaveCorrection={(correlationId, correctedQuestion, correctedAnswer, publish) => {
getApi()
.then(api => api.updateCorrection({ correlationId, correctedQuestion, correctedAnswer, publish }))
.catch(err => captureError("internal-tool-update-correction", err));
}}
onMarkReviewed={(correlationId) => {
getApi()
.then(api => api.markReviewed({ correlationId }))
.catch(err => captureError("internal-tool-mark-reviewed", err));
}}
onUnmarkReviewed={(correlationId) => {
getApi()
.then(api => api.unmarkReviewed({ correlationId }))
.catch(err => captureError("internal-tool-unmark-reviewed", err));
}}
onSaveCorrection={(correlationId, correctedQuestion, correctedAnswer, publish) =>
getApi().then(api => api.updateCorrection({ correlationId, correctedQuestion, correctedAnswer, publish }))
}
onMarkReviewed={(correlationId) =>
getApi().then(api => api.markReviewed({ correlationId }))
}
onUnmarkReviewed={(correlationId) =>
getApi().then(api => api.unmarkReviewed({ correlationId }))
}
/>
</aside>
)}
@ -231,16 +224,12 @@ export default function App() {
<div className="p-6 max-w-4xl mx-auto">
<KnowledgeBase
rows={rows}
onSave={(correlationId, question, answer, publish) => {
getApi()
.then(api => api.updateCorrection({ correlationId, correctedQuestion: question, correctedAnswer: answer, publish }))
.catch(err => captureError("internal-tool-kb-save", err));
}}
onDelete={(correlationId) => {
getApi()
.then(api => api.delete({ correlationId }))
.catch(err => captureError("internal-tool-kb-delete", err));
}}
onSave={(correlationId, question, answer, publish) =>
getApi().then(api => api.updateCorrection({ correlationId, correctedQuestion: question, correctedAnswer: answer, publish }))
}
onDelete={(correlationId) =>
getApi().then(api => api.delete({ correlationId }))
}
/>
</div>
</main>

View File

@ -1,4 +1,5 @@
import { captureError } from "@stackframe/stack-shared/dist/utils/errors";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
import { clsx } from "clsx";
import { format, formatDistanceToNow } from "date-fns";
import { useState, useEffect } from "react";
@ -53,12 +54,27 @@ export function CallLogDetail({ row, allRows, onClose, onSaveCorrection, onMarkR
const isReviewed = optimisticReviewed ?? (row.humanReviewedAt != null);
const handleMark = () => {
const previous = optimisticReviewed;
setOptimisticReviewed(true);
Promise.resolve(onMarkReviewed?.(row.correlationId)).catch(err => captureError("call-log-mark-reviewed", err));
runAsynchronouslyWithAlert(
Promise.resolve(onMarkReviewed?.(row.correlationId)).catch(err => {
// Revert the optimistic override so the UI reflects the database's real state.
setOptimisticReviewed(previous);
captureError("call-log-mark-reviewed", err);
throw err;
})
);
};
const handleUnmark = () => {
const previous = optimisticReviewed;
setOptimisticReviewed(false);
Promise.resolve(onUnmarkReviewed?.(row.correlationId)).catch(err => captureError("call-log-unmark-reviewed", err));
runAsynchronouslyWithAlert(
Promise.resolve(onUnmarkReviewed?.(row.correlationId)).catch(err => {
setOptimisticReviewed(previous);
captureError("call-log-unmark-reviewed", err);
throw err;
})
);
};
return (

View File

@ -1,3 +1,4 @@
import { captureError } from "@stackframe/stack-shared/dist/utils/errors";
import { useState, useMemo } from "react";
import { format } from "date-fns";
import { clsx } from "clsx";
@ -8,8 +9,8 @@ type KbFilter = "all" | "published" | "draft";
export function KnowledgeBase({ rows, onSave, onDelete }: {
rows: McpCallLogRow[];
onSave: (correlationId: string, question: string, answer: string, publish: boolean) => void;
onDelete: (correlationId: string) => void;
onSave: (correlationId: string, question: string, answer: string, publish: boolean) => Promise<void> | void;
onDelete: (correlationId: string) => Promise<void> | void;
}) {
const [filter, setFilter] = useState<KbFilter>("all");
const [search, setSearch] = useState("");
@ -100,10 +101,14 @@ export function KnowledgeBase({ rows, onSave, onDelete }: {
onStartEdit={() => setEditingId(row.correlationId)}
onCancelEdit={() => setEditingId(null)}
onSave={(question, answer, publish) => {
onSave(row.correlationId, question, answer, publish);
Promise.resolve(onSave(row.correlationId, question, answer, publish))
.catch(err => captureError("knowledge-base-save", err));
setEditingId(null);
}}
onDelete={() => onDelete(row.correlationId)}
onDelete={() => {
Promise.resolve(onDelete(row.correlationId))
.catch(err => captureError("knowledge-base-delete", err));
}}
/>
))}
</div>

View File

@ -182,14 +182,27 @@ export function Usage({ rows, connectionState, onSelect, selectedId }: Props) {
const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
const p95Duration = durations.length > 0 ? durations[Math.min(Math.floor(durations.length * 0.95), durations.length - 1)] : 0;
// Time-bucketed series
const spanMs = now - rangeStart;
let seriesStart: number;
let seriesEnd: number;
if (timeRange === "all" && filtered.length > 0) {
seriesStart = Infinity;
seriesEnd = -Infinity;
for (const r of filtered) {
const ts = toDate(r.createdAt).getTime();
if (ts < seriesStart) seriesStart = ts;
if (ts > seriesEnd) seriesEnd = ts;
}
} else {
seriesStart = rangeStart;
seriesEnd = now;
}
const spanMs = Math.max(0, seriesEnd - seriesStart);
const bucketMs = spanMs <= 24 * 60 * 60 * 1000 ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
const bucketLabelFmt: Intl.DateTimeFormatOptions = bucketMs === 60 * 60 * 1000
? { hour: "numeric" }
: { month: "short", day: "numeric" };
const bucketCount = Math.min(48, Math.max(1, Math.ceil(spanMs / bucketMs)));
const bucketStart = now - bucketCount * bucketMs;
const bucketStart = seriesEnd - bucketCount * bucketMs;
const timeBuckets: Array<{ label: string, start: number, calls: number, inputTokens: number, outputTokens: number, cachedInputTokens: number }> = [];
for (let i = 0; i < bucketCount; i++) {
const start = bucketStart + i * bucketMs;
@ -557,9 +570,18 @@ export function Usage({ rows, connectionState, onSelect, selectedId }: Props) {
return (
<tr
key={String(row.id)}
role="button"
tabIndex={0}
aria-selected={selectedId === row.id}
onClick={() => onSelect(row)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelect(row);
}
}}
className={clsx(
"border-b border-gray-100 cursor-pointer hover:bg-blue-50",
"border-b border-gray-100 cursor-pointer hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
selectedId === row.id && "bg-blue-50"
)}
>

View File

@ -0,0 +1,16 @@
const IS_DEV = process.env.NODE_ENV === "development";
const PLACEHOLDER = "REPLACE_ME";
/**
* In dev, fall back to a seeded local default when an env var is missing or
* still holds the `REPLACE_ME` placeholder. In prod, missing/placeholder values
* are a deployment misconfiguration and throw immediately so requests don't
* silently go out with empty auth headers or a blank base URL.
*/
export function envOrDevDefault(value: string | undefined, devDefault: string, name: string): string {
if (!value || value === PLACEHOLDER) {
if (IS_DEV) return devDefault;
throw new Error(`${name} is not configured. Set the NEXT_PUBLIC_STACK_* vars in .env.local or the hosting platform env.`);
}
return value;
}

View File

@ -1,19 +1,12 @@
const IS_DEV = process.env.NODE_ENV === "development";
const PLACEHOLDER = "REPLACE_ME";
function envOrDevDefault(value: string | undefined, devDefault: string): string {
if (!value || value === PLACEHOLDER) {
return IS_DEV ? devDefault : "";
}
return value;
}
import { envOrDevDefault } from "./env";
const PORT_PREFIX = process.env.NEXT_PUBLIC_STACK_PORT_PREFIX ?? "81";
const API_URL = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_API_URL, `http://localhost:${PORT_PREFIX}02`);
const PROJECT_ID = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_PROJECT_ID, "internal");
const API_URL = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_API_URL, `http://localhost:${PORT_PREFIX}02`, "NEXT_PUBLIC_STACK_API_URL");
const PROJECT_ID = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_PROJECT_ID, "internal", "NEXT_PUBLIC_STACK_PROJECT_ID");
const PUBLISHABLE_CLIENT_KEY = envOrDevDefault(
process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
"this-publishable-client-key-is-for-local-development-only",
"NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY",
);
async function post(path: string, body: unknown, authHeaders: Record<string, string>): Promise<void> {

View File

@ -1,26 +1,15 @@
import { StackClientApp } from "@stackframe/stack";
const IS_DEV = process.env.NODE_ENV === "development";
const PLACEHOLDER = "REPLACE_ME";
// In dev, fall back to the seeded "internal" project if env vars are placeholders.
// In prod, the real values must be set via hosting platform env vars.
function envOrDevDefault(value: string | undefined, devDefault: string): string {
if (!value || value === PLACEHOLDER) {
if (IS_DEV) return devDefault;
throw new Error("Stack Auth env var is not configured. Set the NEXT_PUBLIC_STACK_* vars in .env.local or hosting platform env.");
}
return value;
}
import { envOrDevDefault } from "./lib/env";
const portPrefix = process.env.NEXT_PUBLIC_STACK_PORT_PREFIX ?? "81";
const projectId = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_PROJECT_ID, "internal");
const projectId = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_PROJECT_ID, "internal", "NEXT_PUBLIC_STACK_PROJECT_ID");
const publishableClientKey = envOrDevDefault(
process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
"this-publishable-client-key-is-for-local-development-only",
"NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY",
);
const apiUrl = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_API_URL, `http://localhost:${portPrefix}02`);
const apiUrl = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_API_URL, `http://localhost:${portPrefix}02`, "NEXT_PUBLIC_STACK_API_URL");
export const stackClientApp = new StackClientApp({
projectId,

View File

@ -83,10 +83,14 @@ const handler = createMcpHandler(
.join("\n\n") ??
"";
const responseConversationId = body.conversationId ?? conversationId ?? "";
const responseConversationId = body.conversationId ?? conversationId;
const bodyText = text.length > 0 ? text : "(empty response)";
const fullText = responseConversationId
? `${bodyText}\n\n[conversationId: ${responseConversationId} — pass this value as the conversationId parameter in your next ask_stack_auth call to continue this conversation]`
: bodyText;
return {
content: [{ type: "text", text: `${text.length > 0 ? text : "(empty response)"}\n\n[conversationId: ${responseConversationId} — pass this value as the conversationId parameter in your next ask_stack_auth call to continue this conversation]` }],
content: [{ type: "text", text: fullText }],
};
},
);