mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Add login key system
This commit is contained in:
@@ -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) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user