Add login key system

This commit is contained in:
Brian Hackett 2025-02-21 09:00:23 -08:00
parent 38d389a42e
commit 63dcd6702e
8 changed files with 83 additions and 35 deletions

View File

@ -29,6 +29,7 @@ import { getCurrentMouseData } from '../workbench/PointSelector';
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, MaxFreeUses } from '~/utils/freeUses';
import type { FileMap } from '~/lib/stores/files';
import { shouldIncludeFile } from '~/utils/fileUtils';
import { getNutLoginKey } from '~/lib/replay/Problems';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
@ -334,11 +335,18 @@ export const ChatImpl = memo(
return;
}
const loginKey = getNutLoginKey();
if (!loginKey) {
toast.error('Please set a login key in the "User Info" settings.');
return;
}
const anthropicApiKey = Cookies.get(anthropicApiKeyCookieName);
if (!anthropicApiKey) {
if (!loginKey && !anthropicApiKey) {
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
if (numFreeUses >= MaxFreeUses) {
toast.error('All free uses consumed. Please set an Anthropic API key in the settings.');
toast.error('All free uses consumed. Please set a login key or Anthropic API key in the "User Info" settings.');
return;
}
@ -412,7 +420,7 @@ export const ChatImpl = memo(
image: imageData,
})),
] as any, // Type assertion to bypass compiler check
}, { body: { simulationEnhancedPrompt, anthropicApiKey } });
}, { body: { simulationEnhancedPrompt, anthropicApiKey, loginKey } });
if (fileModifications !== undefined) {
/**

View File

@ -2,16 +2,16 @@ import { useState } from 'react';
import { toast } from 'react-toastify';
import Cookies from 'js-cookie';
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, MaxFreeUses } from '~/utils/freeUses';
import { setNutAdminKey, setProblemsUsername, getNutAdminKey, getProblemsUsername } from '~/lib/replay/Problems';
import { saveNutLoginKey, saveProblemsUsername, getNutLoginKey, getProblemsUsername } from '~/lib/replay/Problems';
export default function ConnectionsTab() {
const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || '');
const [username, setUsername] = useState(getProblemsUsername() || '');
const [adminKey, setAdminKey] = useState(getNutAdminKey() || '');
const [loginKey, setLoginKey] = useState(getNutLoginKey() || '');
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
const handleSaveAPIKey = async (key: string) => {
if (!key || !key.startsWith('sk-ant-')) {
if (key && !key.startsWith('sk-ant-')) {
toast.error('Please provide a valid Anthropic API key');
return;
}
@ -21,13 +21,19 @@ export default function ConnectionsTab() {
};
const handleSaveUsername = async (username: string) => {
setProblemsUsername(username);
saveProblemsUsername(username);
setUsername(username);
};
const handleSaveAdminKey = async (key: string) => {
setNutAdminKey(key);
setAdminKey(key);
const handleSaveLoginKey = async (key: string) => {
setLoginKey(key);
try {
await saveNutLoginKey(key);
toast.success('Login key saved');
} catch (error) {
toast.error('Failed to save login key');
}
};
return (
@ -61,13 +67,13 @@ export default function ConnectionsTab() {
/>
</div>
</div>
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Nut Admin Key</h3>
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Nut Login Key</h3>
<div className="flex mb-4">
<div className="flex-1 mr-2">
<input
type="text"
value={adminKey}
onChange={(e) => handleSaveAdminKey(e.target.value)}
value={loginKey}
onChange={(e) => handleSaveLoginKey(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
/>
</div>

View File

@ -13,7 +13,7 @@ import { binDates } from './date-binning';
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
import { SaveProblem } from './SaveProblem';
import { SaveSolution } from './SaveSolution';
import { hasNutAdminKey } from '~/lib/replay/Problems';
import { getNutIsAdmin } from '~/lib/replay/Problems';
const menuVariants = {
closed: {
@ -140,7 +140,7 @@ export const Menu = () => {
Problems
</a>
<SaveProblem />
{hasNutAdminKey() && <SaveSolution />}
{getNutIsAdmin() && <SaveSolution />}
<a
href="/about"
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"

View File

@ -27,6 +27,7 @@ function convertContentToAnthropic(content: any): ContentBlockParam[] {
export interface AnthropicApiKey {
key: string;
isUser: boolean;
userLoginKey?: string;
}
export interface AnthropicCall {
systemPrompt: string;
@ -52,6 +53,7 @@ const callAnthropic = wrapWithSpan(
"llm.chat.calls": 1, // so we can SUM(llm.chat.calls) without doing a COUNT + filter
"llm.chat.num_messages": messages.length,
"llm.chat.is_user_api_key": apiKey.isUser,
"llm.chat.user_login_key": apiKey.userLoginKey,
});
const anthropic = new Anthropic({ apiKey: apiKey.key });

View File

@ -111,15 +111,17 @@ export async function submitProblem(problem: BoltProblemInput): Promise<string |
export async function updateProblem(problemId: string, problem: BoltProblemInput) {
try {
const adminKey = Cookies.get(nutAdminKeyCookieName);
if (!adminKey) {
toast.error("Admin key not specified");
if (!getNutIsAdmin()) {
toast.error("Admin user required");
return;
}
const loginKey = Cookies.get(nutLoginKeyCookieName);
await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand",
params: {
name: "updateBoltProblem",
params: { problemId, problem, adminKey },
params: { problemId, problem, loginKey },
},
});
} catch (error) {
@ -128,18 +130,40 @@ export async function updateProblem(problemId: string, problem: BoltProblemInput
}
}
const nutAdminKeyCookieName = 'nutAdminKey';
const nutLoginKeyCookieName = 'nutLoginKey';
const nutIsAdminCookieName = 'nutIsAdmin';
export function getNutAdminKey(): string | undefined {
return Cookies.get(nutAdminKeyCookieName);
export function getNutLoginKey(): string | undefined {
return Cookies.get(nutLoginKeyCookieName);
}
export function hasNutAdminKey(): boolean {
return !!getNutAdminKey();
export function getNutIsAdmin(): boolean {
return Cookies.get(nutIsAdminCookieName) === 'true';
}
export function setNutAdminKey(key: string) {
Cookies.set(nutAdminKeyCookieName, key);
interface UserInfo {
username: string;
loginKey: string;
details: string;
admin: boolean;
}
export async function saveNutLoginKey(key: string) {
const { rval: { userInfo } } = await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand",
params: {
name: "getUserInfo",
params: { loginKey: key },
},
}) as { rval: { userInfo: UserInfo } };
console.log("UserInfo", userInfo);
Cookies.set(nutLoginKeyCookieName, key);
Cookies.set(nutIsAdminCookieName, userInfo.admin ? 'true' : 'false');
}
export function setNutIsAdmin(isAdmin: boolean) {
Cookies.set(nutIsAdminCookieName, isAdmin ? 'true' : 'false');
}
const nutProblemsUsernameCookieName = 'nutProblemsUsername';
@ -148,7 +172,7 @@ export function getProblemsUsername(): string | undefined {
return Cookies.get(nutProblemsUsernameCookieName);
}
export function setProblemsUsername(username: string) {
export function saveProblemsUsername(username: string) {
Cookies.set(nutProblemsUsernameCookieName, username);
}

View File

@ -18,12 +18,13 @@ Focus specifically on fixing this bug. Do not guess about other problems.
async function chatAction({ context, request }: ActionFunctionArgs) {
ensureOpenTelemetryInitialized(context);
const { messages, files, promptId, simulationEnhancedPrompt, anthropicApiKey: clientAnthropicApiKey } = await request.json<{
const { messages, files, promptId, simulationEnhancedPrompt, anthropicApiKey: clientAnthropicApiKey, loginKey } = await request.json<{
messages: Messages;
files: FileMap;
promptId?: string;
simulationEnhancedPrompt?: string;
anthropicApiKey?: string;
loginKey?: string;
}>();
let finished: (v?: any) => void;
@ -49,6 +50,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
const anthropicApiKey: AnthropicApiKey = {
key: apiKey,
isUser: !!clientAnthropicApiKey,
userLoginKey: loginKey,
};
const resultStream = new ReadableStream({

View File

@ -5,11 +5,10 @@ import BackgroundRays from '~/components/ui/BackgroundRays';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { ToastContainerWrapper, Status, Keywords } from './problems';
import { toast } from 'react-toastify';
import { useCallback, useEffect } from 'react';
import { useState } from 'react';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { useParams } from '@remix-run/react';
import { getProblem, updateProblem as backendUpdateProblem, getProblemsUsername, BoltProblemStatus, hasNutAdminKey } from '~/lib/replay/Problems';
import type { BoltProblem, BoltProblemComment, BoltProblemInput } from '~/lib/replay/Problems';
import { getProblem, updateProblem as backendUpdateProblem, getProblemsUsername, BoltProblemStatus, getNutIsAdmin } from '~/lib/replay/Problems';
import type { BoltProblem, BoltProblemComment } from '~/lib/replay/Problems';
function Comments({ comments }: { comments: BoltProblemComment[] }) {
return (
@ -152,6 +151,8 @@ function UpdateProblemForms({ updateProblem }: { updateProblem: UpdateProblemCal
)
}
const Nothing = () => null;
function ViewProblemPage() {
const params = useParams();
const problemId = params.id;
@ -177,6 +178,7 @@ function ViewProblemPage() {
}, [problemId]);
return (
<Suspense fallback={<Nothing />}>
<TooltipProvider>
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
<BackgroundRays />
@ -190,12 +192,13 @@ function ViewProblemPage() {
</div>)
: <ProblemViewer problem={problemData} />}
</div>
{hasNutAdminKey() && problemData && (
{getNutIsAdmin() && problemData && (
<UpdateProblemForms updateProblem={updateProblem} />
)}
<ToastContainerWrapper />
</div>
</TooltipProvider>
</Suspense>
);
}

View File

@ -4,8 +4,7 @@ import { Menu } from '~/components/sidebar/Menu.client';
import BackgroundRays from '~/components/ui/BackgroundRays';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { cssTransition, ToastContainer } from 'react-toastify';
import { useEffect } from 'react';
import { useState } from 'react';
import { Suspense, useEffect, useState } from 'react';
import { BoltProblemStatus, listAllProblems } from '~/lib/replay/Problems';
import type { BoltProblemDescription } from '~/lib/replay/Problems';
@ -91,6 +90,8 @@ function getProblemStatus(problem: BoltProblemDescription): BoltProblemStatus {
return problem.status ?? BoltProblemStatus.Pending;
}
const Nothing = () => null;
function ProblemsPage() {
const [problems, setProblems] = useState<BoltProblemDescription[] | null>(null);
const [statusFilter, setStatusFilter] = useState<BoltProblemStatus | 'all'>(BoltProblemStatus.Solved);
@ -104,6 +105,7 @@ function ProblemsPage() {
});
return (
<Suspense fallback={<Nothing />}>
<TooltipProvider>
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
<BackgroundRays />
@ -164,6 +166,7 @@ function ProblemsPage() {
<ToastContainerWrapper />
</div>
</TooltipProvider>
</Suspense>
);
}