Skip to content

Commit

Permalink
message credits integration
Browse files Browse the repository at this point in the history
  • Loading branch information
eg9y committed May 29, 2023
1 parent 8973a01 commit 306e590
Show file tree
Hide file tree
Showing 11 changed files with 393 additions and 43 deletions.
6 changes: 6 additions & 0 deletions packages/shared/src/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface Database {
profiles: {
Row: {
date_subscribed: string
edit_with_own_key: boolean
first_name: string | null
id: string
last_name: string | null
Expand All @@ -75,6 +76,7 @@ export interface Database {
}
Insert: {
date_subscribed?: string
edit_with_own_key?: boolean
first_name?: string | null
id: string
last_name?: string | null
Expand All @@ -84,6 +86,7 @@ export interface Database {
}
Update: {
date_subscribed?: string
edit_with_own_key?: boolean
first_name?: string | null
id?: string
last_name?: string | null
Expand Down Expand Up @@ -156,6 +159,7 @@ export interface Database {
Row: {
date_subscribed: string | null
decrypted_open_ai_key: string | null
edit_with_own_key: boolean | null
first_name: string | null
id: string | null
last_name: string | null
Expand All @@ -166,6 +170,7 @@ export interface Database {
Insert: {
date_subscribed?: string | null
decrypted_open_ai_key?: never
edit_with_own_key?: boolean | null
first_name?: string | null
id?: string | null
last_name?: string | null
Expand All @@ -176,6 +181,7 @@ export interface Database {
Update: {
date_subscribed?: string | null
decrypted_open_ai_key?: never
edit_with_own_key?: boolean | null
first_name?: string | null
id?: string | null
last_name?: string | null
Expand Down
7 changes: 6 additions & 1 deletion packages/supabase/functions/openai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ serve(async (req) => {

let openAiApiKey = '';
try {
openAiApiKey = await getOpenAiKey(supabase, user);
if (user.user_metadata.edit_with_api_key) {
openAiApiKey = await getOpenAiKey(supabase, user);
} else {
// user can opt out of storing their api key for their editor and use the app's api key instead
openAiApiKey = Deno.env.get('OPEN_AI_KEY') ?? '';
}
} catch (err) {
return new Response(JSON.stringify(err), {
status: 500,
Expand Down
228 changes: 228 additions & 0 deletions packages/web/src/components/ApiKeySettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { Dialog, Switch, Transition } from '@headlessui/react';
import { Fragment, useEffect, useState } from 'react';
import { shallow } from 'zustand/shallow';

import { ReactComponent as Loading } from '../assets/loading.svg';
import { useStoreSecret, selectorSecret } from '../store';
import { conditionalClassNames } from '../utils/classNames';

export default function ApiKeySettings({
open,
supabase,
setOpen,
}: {
open: boolean;
setOpen: (open: boolean) => void;
supabase: any;
}) {
const { openAiKey, setOpenAiKey, session, setSession } = useStoreSecret(
selectorSecret,
shallow,
);
const [newApiKey, setNewApiKey] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [helperMessage, setHelperMessage] = useState('');
const [isEditWithMyKey, setIsEditWithMyKey] = useState<boolean | null>(null);

useEffect(() => {
if (session && isEditWithMyKey === null) {
setIsEditWithMyKey(!!session.user.user_metadata.is_edit_with_my_key);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [session]);

async function handleChange() {
setIsLoading(true);

// update user's openai key
if (newApiKey === '') {
console.log('No key entered');
} else {
await supabase.functions.invoke('insert-api-key', {
body: {
api_key: newApiKey,
},
});
setOpenAiKey(newApiKey);
}

// edit here
setIsLoading(false);
setOpen(false);
}

return (
<Transition.Root show={open} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={() => {
return;
}}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-slate-900 bg-opacity-75 backdrop-blur-md transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 z-10 overflow-y-auto ">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-slate-50 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div>
<p className="block text-sm font-medium leading-6 text-gray-900">
Set how you want to call OpenAI API
</p>
<div className="mt-2">
<Switch.Group as="div" className="flex items-center">
<Switch
checked={!!isEditWithMyKey}
onChange={async () => {
setIsEditWithMyKey(!isEditWithMyKey);
// update user edit_with_api_key metadata. This will inform the supabase proxy to use the user's key instead of the app's key
const { data, error } =
await supabase.auth.updateUser({
data: {
edit_with_api_key: !isEditWithMyKey,
},
});

if (error) {
setHelperMessage(error.message);
return;
}
if (!data) {
setHelperMessage(
'Your settings have not been updated. ERROR',
);
return;
}

if (session) {
const newSession = { ...session };
if (newSession.user) {
newSession.user.user_metadata.edit_with_api_key =
!isEditWithMyKey;
setSession(newSession);
}
} else {
setHelperMessage(
'Your settings have not been updated. ERROR',
);
return;
}
}}
className={conditionalClassNames(
isEditWithMyKey
? 'bg-green-600'
: 'bg-gray-200',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-offset-2',
)}
>
<span
aria-hidden="true"
className={conditionalClassNames(
isEditWithMyKey
? 'translate-x-5'
: 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
)}
/>
</Switch>
<Switch.Label as="span" className="ml-1 text-sm">
<span className="font-medium text-slate-700">
Edit with my key
</span>
</Switch.Label>
</Switch.Group>
{isEditWithMyKey && (
<form
className="flex flex-col gap-1 pt-4"
onSubmit={async (e) => {
e.preventDefault();
await handleChange();
}}
>
<label
htmlFor="openAiKey"
className="block text-sm font-medium leading-6 text-gray-900"
>
OpenAI Key
</label>
<input
type="openAiKey"
name="openAiKey"
id="openAiKey"
value={openAiKey}
onChange={(e) => {
const input = e.target.value;
setNewApiKey(input);
}}
placeholder="sk-************"
className="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
aria-describedby="username-description"
/>
{isLoading && (
<Loading className="-ml-1 mr-3 h-5 w-5 animate-spin text-black" />
)}
</form>
)}
</div>
<p
className="mt-2 text-sm text-red-500"
id="username-description"
>
{helperMessage}
</p>
<div className="flex gap-2">
<button
type="button"
onClick={() => {
if (
session &&
session.user &&
session.user.user_metadata
) {
delete session.user.user_metadata
.is_edit_with_my_key;
}
setOpen(false);
}}
className="rounded-md bg-red-100 px-2.5 py-1.5 text-sm font-semibold text-red-600 shadow-sm hover:bg-red-200"
>
Cancel
</button>
<button
type="button"
onClick={async () => {
await handleChange();
}}
className="rounded-md bg-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
>
OK
</button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
12 changes: 9 additions & 3 deletions packages/web/src/components/RunFromStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ChevronDoubleRightIcon } from '@heroicons/react/20/solid';
import { shallow } from 'zustand/shallow';

import { ReactComponent as Loading } from '../assets/loading.svg';
import useSupabase from '../auth/supabaseClient';
import { useStore, useStoreSecret, selector, selectorSecret } from '../store';
import { conditionalClassNames } from '../utils/classNames';

Expand All @@ -26,10 +27,15 @@ export default function RunFromStart({
runFlow,
edges,
} = useStore(selector, shallow);
const { openAiKey } = useStoreSecret(selectorSecret, shallow);
const secret = useStoreSecret(selectorSecret, shallow);
const supabase = useSupabase();

function getSecret() {
return secret;
}

async function runFromStart() {
if (openAiKey.trim() === '') {
if (getSecret().openAiKey.trim() === '') {
setNotificationMessage('Please enter an OpenAI API key in the left panel.');
return;
}
Expand All @@ -43,7 +49,7 @@ export default function RunFromStart({
abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal;
setUnlockGraph(false);
await runFlow(nodes, edges, openAiKey, signal);
await runFlow(getSecret, nodes, edges, supabase, signal);
setUnlockGraph(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
Expand Down
11 changes: 4 additions & 7 deletions packages/web/src/nodes/templates/NodeTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ReactComponent as Loading } from '../../assets/loading.svg';
import FullScreenEditor from '../../components/FullScreenEditor';
import useStore, { selector } from '../../store/useStore';
import { conditionalClassNames } from '../../utils/classNames';
import { calculateCreditsRequired } from '../../utils/userCredits';

interface NodeTemplateInterface {
title: string;
Expand Down Expand Up @@ -124,15 +125,11 @@ const NodeTemplate: FC<
].includes(type as NodeTypesEnum)
}
>
{(data as DefaultNodeDataType & OpenAIAPIRequest).model && (
{(data as DefaultNodeDataType & OpenAIAPIRequest).model && data.text.length > 0 && (
<div className="flex gap-2">
<p className="text-slate-600">
{(data as DefaultNodeDataType & OpenAIAPIRequest).model.startsWith(
'gpt-4',
)
? '10'
: '1'}{' '}
credit
{'>'}
{calculateCreditsRequired(data, data.text)} credit
</p>
</div>
)}
Expand Down
21 changes: 19 additions & 2 deletions packages/web/src/pages/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ export default function App() {
setWorkflows,
setChatApp,
} = useStore(selector, shallow);
const { session, setSession, setOpenAiKey } = useStoreSecret(selectorSecret, shallow);
const { session, setSession, setOpenAiKey, setUserCredits } = useStoreSecret(
selectorSecret,
shallow,
);

const [settingsView, setSettingsView] = useState(true);

Expand Down Expand Up @@ -137,6 +140,21 @@ export default function App() {
supabase,
);
}
const { data: credits, error: creditsError } = await supabase
.from('profiles')
.select('plan, remaining_message_credits')
.eq('id', session?.user?.id)
.single();

if (creditsError) {
setNotificationMessage(creditsError.message);
throw creditsError;
}

setUserCredits({
plan: (credits?.plan as 'free' | 'essential' | 'premium') || 'free',
credits: credits?.remaining_message_credits || 0,
});
setIsLoading(false);
}
if (params) {
Expand Down Expand Up @@ -226,7 +244,6 @@ export default function App() {
<div className="absolute flex w-full justify-center p-4">
<SandboxExecutionPanel nodes={nodes} setNodes={setNodes} setChatApp={setChatApp} />
</div>

<div
className="grid grid-cols-2"
style={{
Expand Down
Loading

1 comment on commit 306e590

@vercel
Copy link

@vercel vercel bot commented on 306e590 May 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.