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

View File

@@ -2,16 +2,16 @@ import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, MaxFreeUses } from '~/utils/freeUses'; 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() { export default function ConnectionsTab() {
const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || ''); const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || '');
const [username, setUsername] = useState(getProblemsUsername() || ''); const [username, setUsername] = useState(getProblemsUsername() || '');
const [adminKey, setAdminKey] = useState(getNutAdminKey() || ''); const [loginKey, setLoginKey] = useState(getNutLoginKey() || '');
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0); const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
const handleSaveAPIKey = async (key: string) => { 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'); toast.error('Please provide a valid Anthropic API key');
return; return;
} }
@@ -21,13 +21,19 @@ export default function ConnectionsTab() {
}; };
const handleSaveUsername = async (username: string) => { const handleSaveUsername = async (username: string) => {
setProblemsUsername(username); saveProblemsUsername(username);
setUsername(username); setUsername(username);
}; };
const handleSaveAdminKey = async (key: string) => { const handleSaveLoginKey = async (key: string) => {
setNutAdminKey(key); setLoginKey(key);
setAdminKey(key);
try {
await saveNutLoginKey(key);
toast.success('Login key saved');
} catch (error) {
toast.error('Failed to save login key');
}
}; };
return ( return (
@@ -61,13 +67,13 @@ export default function ConnectionsTab() {
/> />
</div> </div>
</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 mb-4">
<div className="flex-1 mr-2"> <div className="flex-1 mr-2">
<input <input
type="text" type="text"
value={adminKey} value={loginKey}
onChange={(e) => handleSaveAdminKey(e.target.value)} 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" 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> </div>

View File

@@ -13,7 +13,7 @@ import { binDates } from './date-binning';
import { useSearchFilter } from '~/lib/hooks/useSearchFilter'; import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
import { SaveProblem } from './SaveProblem'; import { SaveProblem } from './SaveProblem';
import { SaveSolution } from './SaveSolution'; import { SaveSolution } from './SaveSolution';
import { hasNutAdminKey } from '~/lib/replay/Problems'; import { getNutIsAdmin } from '~/lib/replay/Problems';
const menuVariants = { const menuVariants = {
closed: { closed: {
@@ -140,7 +140,7 @@ export const Menu = () => {
Problems Problems
</a> </a>
<SaveProblem /> <SaveProblem />
{hasNutAdminKey() && <SaveSolution />} {getNutIsAdmin() && <SaveSolution />}
<a <a
href="/about" 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" 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 { export interface AnthropicApiKey {
key: string; key: string;
isUser: boolean; isUser: boolean;
userLoginKey?: string;
} }
export interface AnthropicCall { export interface AnthropicCall {
systemPrompt: string; 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.calls": 1, // so we can SUM(llm.chat.calls) without doing a COUNT + filter
"llm.chat.num_messages": messages.length, "llm.chat.num_messages": messages.length,
"llm.chat.is_user_api_key": apiKey.isUser, "llm.chat.is_user_api_key": apiKey.isUser,
"llm.chat.user_login_key": apiKey.userLoginKey,
}); });
const anthropic = new Anthropic({ apiKey: apiKey.key }); 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) { export async function updateProblem(problemId: string, problem: BoltProblemInput) {
try { try {
const adminKey = Cookies.get(nutAdminKeyCookieName); if (!getNutIsAdmin()) {
if (!adminKey) { toast.error("Admin user required");
toast.error("Admin key not specified"); return;
} }
const loginKey = Cookies.get(nutLoginKeyCookieName);
await sendCommandDedicatedClient({ await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand", method: "Recording.globalExperimentalCommand",
params: { params: {
name: "updateBoltProblem", name: "updateBoltProblem",
params: { problemId, problem, adminKey }, params: { problemId, problem, loginKey },
}, },
}); });
} catch (error) { } 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 { export function getNutLoginKey(): string | undefined {
return Cookies.get(nutAdminKeyCookieName); return Cookies.get(nutLoginKeyCookieName);
} }
export function hasNutAdminKey(): boolean { export function getNutIsAdmin(): boolean {
return !!getNutAdminKey(); return Cookies.get(nutIsAdminCookieName) === 'true';
} }
export function setNutAdminKey(key: string) { interface UserInfo {
Cookies.set(nutAdminKeyCookieName, key); 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'; const nutProblemsUsernameCookieName = 'nutProblemsUsername';
@@ -148,7 +172,7 @@ export function getProblemsUsername(): string | undefined {
return Cookies.get(nutProblemsUsernameCookieName); return Cookies.get(nutProblemsUsernameCookieName);
} }
export function setProblemsUsername(username: string) { export function saveProblemsUsername(username: string) {
Cookies.set(nutProblemsUsernameCookieName, username); 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) { async function chatAction({ context, request }: ActionFunctionArgs) {
ensureOpenTelemetryInitialized(context); 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; messages: Messages;
files: FileMap; files: FileMap;
promptId?: string; promptId?: string;
simulationEnhancedPrompt?: string; simulationEnhancedPrompt?: string;
anthropicApiKey?: string; anthropicApiKey?: string;
loginKey?: string;
}>(); }>();
let finished: (v?: any) => void; let finished: (v?: any) => void;
@@ -49,6 +50,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
const anthropicApiKey: AnthropicApiKey = { const anthropicApiKey: AnthropicApiKey = {
key: apiKey, key: apiKey,
isUser: !!clientAnthropicApiKey, isUser: !!clientAnthropicApiKey,
userLoginKey: loginKey,
}; };
const resultStream = new ReadableStream({ const resultStream = new ReadableStream({

View File

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

View File

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