mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
bot fixes
This commit is contained in:
parent
26ce83f935
commit
f9386a886e
@ -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>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
)}
|
||||
>
|
||||
|
||||
16
apps/internal-tool/src/lib/env.ts
Normal file
16
apps/internal-tool/src/lib/env.ts
Normal 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;
|
||||
}
|
||||
@ -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> {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user