Skip to content

Commit

Permalink
feat(ui): API Keys (#729)
Browse files Browse the repository at this point in the history
Creates a /chat/api-keys route that allows the user to create and delete API keys
  • Loading branch information
andrewrisse committed Jul 11, 2024
1 parent 7abd2ed commit 1fa59ee
Show file tree
Hide file tree
Showing 58 changed files with 1,333 additions and 110 deletions.
2 changes: 1 addition & 1 deletion packages/ui/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ constants:
variables:
- name: LEAPFROGAI_API_BASE_URL #LEAPFROGAI_API_BASE_URL
description: The base URL for the LeapfrogAI API
default: http://api.leapfrogai.svc.cluster.local:8080/openai/v1
default: http://api.leapfrogai.svc.cluster.local:8080
prompt: true
sensitive: true
- name: OPENAI_API_KEY
Expand Down
2 changes: 1 addition & 1 deletion src/leapfrogai_ui/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ PUBLIC_MESSAGE_LENGTH_LIMIT=10000
DEFAULT_TEMPERATURE=0.1
DEFAULT_SYSTEM_PROMPT="You are a helpful AI assistant created by Defense Unicorns."
DEFAULT_MODEL=vllm # ex. for OpenAI would be: gpt-3.5-turbo
LEAPFROGAI_API_BASE_URL=https://leapfrogai-api.uds.dev/openai/v1
LEAPFROGAI_API_BASE_URL=https://leapfrogai-api.uds.dev
#If specified, app will use OpenAI instead of Leapfrog
OPENAI_API_KEY=

Expand Down
5 changes: 3 additions & 2 deletions src/leapfrogai_ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ If running the UI locally and utilizing LeapfrogAPI, <ins>**you must use the sam
PUBLIC_SUPABASE_URL=https://supabase-kong.uds.dev
PUBLIC_SUPABASE_ANON_KEY=<anon_key>
...
LEAPFROGAI_API_BASE_URL=https://leapfrogai-api.uds.dev/openai/v1
LEAPFROGAI_API_BASE_URL=https://leapfrogai-api.uds.dev
DEFAULT_MODEL=llama-cpp-python # or vllm
```

Expand All @@ -61,7 +61,8 @@ run the UI outside of UDS on localhost (e.g. for development work), there are so

1. Modify the "GOTRUE_URI_ALLOW_LIST" within Supabase.
The Supabase UDS package has a ConfigMap called "supabase-auth-default".
Add these variables to the "GOTRUE_URI_ALLOW_LIST" (no spaces!):
Add these values to the "GOTRUE_URI_ALLOW_LIST" (no spaces!). This variable may not exist and you will need to add it.
Restart the supabase-auth pod after updating the config:
`http://localhost:5173/auth/callback,http://localhost:5173,http://localhost:4173/auth/callback,http://localhost:4173`
Note - Port 4173 is utilized by Playwright for E2E tests. You do not need this if you are not concerned about Playwright.

Expand Down
2 changes: 2 additions & 0 deletions src/leapfrogai_ui/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { LFAssistant } from '$lib/types/assistants';
import type { Profile } from '$lib/types/profile';
import type { LFThread } from '$lib/types/threads';
import type { FileObject } from 'openai/resources/files';
import type { APIKeyRow } from '$lib/types/apiKeys';

declare global {
namespace App {
Expand All @@ -22,6 +23,7 @@ declare global {
assistants?: LFAssistant[];
assistant?: LFAssistant;
files?: FileObject[];
keys?: APIKeyRow[];
}

// interface PageState {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@

<Modal
bind:open={modalOpen}
preventCloseOnClickOutside
modalHeading="Avatar Image"
shouldSubmitOnEnter={false}
primaryButtonText="Save"
Expand Down
1 change: 1 addition & 0 deletions src/leapfrogai_ui/src/lib/components/AssistantForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
<div class="cancel-modal">
<Modal
bind:open={cancelModalOpen}
preventCloseOnClickOutside
modalHeading="Unsaved Changes"
primaryButtonText="Leave this page"
secondaryButtonText="Stay on page"
Expand Down
1 change: 1 addition & 0 deletions src/leapfrogai_ui/src/lib/components/AssistantTile.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@

<Modal
danger
preventCloseOnClickOutside
bind:open={deleteModalOpen}
modalHeading="Delete Assistant"
primaryButtonText="Delete"
Expand Down
1 change: 1 addition & 0 deletions src/leapfrogai_ui/src/lib/components/ChatSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@

<Modal
danger
preventCloseOnClickOutside
bind:open={deleteModalOpen}
modalHeading="Delete Chat"
primaryButtonText="Delete"
Expand Down
7 changes: 7 additions & 0 deletions src/leapfrogai_ui/src/lib/components/LFHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { Settings, UserAvatar } from 'carbon-icons-svelte';
import { Header, HeaderAction, HeaderUtilities } from 'carbon-components-svelte';
export let isUsingOpenAI: boolean;
let loading = false;
let signOutForm: HTMLFormElement;
Expand Down Expand Up @@ -66,6 +68,11 @@
class="header-link"
on:click={() => setActiveHeaderAction('')}>File Management</a
>
{#if !isUsingOpenAI}
<a href="/chat/api-keys" class="header-link" on:click={() => setActiveHeaderAction('')}
>API Keys</a
>
{/if}
</div>
</HeaderAction>
<HeaderAction
Expand Down
9 changes: 0 additions & 9 deletions src/leapfrogai_ui/src/lib/components/Message.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,6 @@
padding-left: layout.$spacing-05;
}
.highlight-icon :global(svg) {
cursor: pointer;
fill: themes.$icon-secondary;
transition: fill 70ms ease;
&:hover {
fill: themes.$icon-primary;
}
}
.edit-prompt :global(.lf-text-area.bx--text-area) {
background: themes.$background;
outline: 1px solid themes.$layer-02;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

<Modal
danger
preventCloseOnClickOutside
bind:open
modalHeading="Delete File"
shouldSubmitOnEnter={false}
Expand Down
65 changes: 65 additions & 0 deletions src/leapfrogai_ui/src/lib/components/modals/CopyApiKeyModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script lang="ts">
import { formatKeyLong } from '$helpers/apiKeyHelpers.js';
import { Copy } from 'carbon-icons-svelte';
import { calculateDays, formatDate } from '$helpers/dates.js';
import { Button, Modal, TextInput } from 'carbon-components-svelte';
import type { APIKeyRow } from '$lib/types/apiKeys';
export let copyKeyModalOpen: boolean;
export let handleCloseCopyKeyModal: () => void;
export let handleCopyKey: () => void;
export let createdKey: APIKeyRow | null;
let saveKeyModalRef: HTMLDivElement;
</script>

<Modal
bind:ref={saveKeyModalRef}
bind:open={copyKeyModalOpen}
preventCloseOnClickOutside
modalHeading="Save secret key"
primaryButtonText="Close"
on:close={handleCloseCopyKeyModal}
on:submit={handleCloseCopyKeyModal}
>
{#if createdKey}
<div class="centered-spaced-container" style="flex-direction: column">
<p>
Please store this secret key in a safe and accessible place. For security purposes, it
cannot be viewed again through your LeapfrogAI account. If you lose it, you'll need to
create a new one.
</p>
<div class="centered-spaced-lg-container" style="width: 100%">
<TextInput
readonly
labelText="Key"
value={formatKeyLong(createdKey.api_key, saveKeyModalRef?.offsetWidth || 200)}
/>
<Button kind="tertiary" icon={Copy} size="small" on:click={handleCopyKey}>Copy</Button>
</div>
<div style="width: 100%">
<label for="saved-expiration" class:bx--label={true}>Expiration</label>
<p id="saved-expiration">
{`${calculateDays(createdKey.created_at, createdKey.expires_at)} days - ${formatDate(new Date(createdKey.expires_at * 1000))}`}
</p>
</div>
</div>
{/if}
</Modal>

<style lang="scss">
.centered-spaced-container {
display: flex;
gap: layout.$spacing-06;
align-items: center;
}
.centered-spaced-lg-container {
display: flex;
gap: layout.$spacing-07;
align-items: center;
:global(.bx--text-input__readonly-icon) {
display: none;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script lang="ts">
import { ContentSwitcher, Modal, Switch, TextInput } from 'carbon-components-svelte';
export let modalOpen: boolean;
export let handleCancel: () => void;
export let submit: () => void;
export let name: string | undefined;
export let invalidText: string | undefined;
export let selectedExpirationIndex: number;
export let selectedExpirationDate: number;
</script>

<Modal
bind:open={modalOpen}
preventCloseOnClickOutside
modalHeading="Create new secret key"
primaryButtonText="Create"
secondaryButtonText="Cancel"
hasForm
on:click:button--secondary={handleCancel}
on:close={handleCancel}
on:submit={submit}
>
<div class="modal-inner-content">
<p style="width: 70%;">
This API key is linked to your user account and gives full access to it. Please be careful and
keep it secret.
</p>

<TextInput
id="name"
name="name"
labelText="Name"
placeholder="Test Key"
size="sm"
autocomplete="off"
bind:value={name}
invalid={!!invalidText}
{invalidText}
/>
<div>
<label for="expiration" class:bx--label={true}>Expiration</label>
<ContentSwitcher
id="expiration"
size="xl"
style="width: 60%"
bind:selectedIndex={selectedExpirationIndex}
>
<Switch text="7 Days" />
<Switch text="30 Days" />
<Switch text="60 Days" />
<Switch text="90 Days" />
</ContentSwitcher>
</div>
<input type="hidden" name="expires_at" value={selectedExpirationDate} />
</div>
</Modal>

<style lang="scss">
.modal-inner-content {
display: flex;
flex-direction: column;
gap: layout.$spacing-06;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import { Modal } from 'carbon-components-svelte';
export let confirmDeleteModalOpen: boolean;
export let keyNames: string;
export let deleting: boolean;
export let handleCancelConfirmDelete: () => void;
export let handleDelete: () => void;
</script>

<Modal
danger
preventCloseOnClickOutside
bind:open={confirmDeleteModalOpen}
modalHeading={`Delete API ${keyNames.length > 0 ? 'Keys' : 'Key'}`}
primaryButtonText="Delete"
secondaryButtonText="Cancel"
primaryButtonDisabled={deleting}
on:click:button--secondary={() => handleCancelConfirmDelete()}
on:close={() => handleCancelConfirmDelete()}
on:submit={() => handleDelete()}
>
<p>Are you sure you want to delete <span style="font-weight: bold">{keyNames}</span>?</p>
</Modal>
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
type ToastErrorText = {
type ToastData = {
title: string;
subtitle?: string;
};

export const ERROR_SAVING_MSG_TEXT: ToastErrorText = {
export const ERROR_SAVING_MSG_TEXT: ToastData = {
title: 'Error',
subtitle: 'Error saving message. Please try again.'
};

export const ERROR_GETTING_AI_RESPONSE_TEXT: ToastErrorText = {
export const ERROR_GETTING_AI_RESPONSE_TEXT: ToastData = {
title: 'Error',
subtitle: 'Error getting AI Response'
};

export const ERROR_GETTING_ASSISTANT_MSG_TEXT: ToastErrorText = {
export const ERROR_GETTING_ASSISTANT_MSG_TEXT: ToastData = {
title: 'Error',
subtitle: 'Error getting Assistant Response'
};
17 changes: 17 additions & 0 deletions src/leapfrogai_ui/src/lib/helpers/apiKeyHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Keys returned from the API list call should already be masked for security
// We use this to mask the created key before the user copies it
export const formatKeyShort = (key: string) => {
const firstTwo = key.slice(0, 5);
const lastFour = key.slice(-4);
return `${firstTwo}...${lastFour}`;
};

// Formats a key with several starts in the middle of it based on screen size
// This is used to ensure the "copy key" text input is filled with text regardless of the
// actual key length
export const formatKeyLong = (key: string, width: number) => {
const approxNumStars = (width * 0.65) / 14;
const firstTwo = key.slice(0, 5);
const lastFour = key.slice(-4);
return `${firstTwo}${'*'.repeat(approxNumStars)}${lastFour}`;
};
10 changes: 10 additions & 0 deletions src/leapfrogai_ui/src/lib/helpers/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,13 @@ export const formatDate = (date: Date) => {
// Return the formatted date string
return `${day} ${month} ${year}`;
};

// Calculate the number of days between two dates (dates are in seconds)
export const calculateDays = (beginDate: number, endDate: number) => {
const differenceInSeconds = Math.abs(endDate - beginDate);
// Convert seconds to days
const secondsPerDay = 60 * 60 * 24;
const differenceInDays = differenceInSeconds / secondsPerDay;

return Math.round(differenceInDays);
};
1 change: 1 addition & 0 deletions src/leapfrogai_ui/src/lib/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * as threads from './threads';
export * as assistants from './assistants';
export * as fileHelpers from './fileHelpers';
export * as chatHelpers from './chatHelpers';
export * as apiKeyHelpers from './apiKeyHelpers';
Loading

0 comments on commit 1fa59ee

Please sign in to comment.