Skip to content

Commit

Permalink
Add Wizard AI Assistant (#306)
Browse files Browse the repository at this point in the history
Co-authored-by: Eric Lau <[email protected]>
  • Loading branch information
zackrw and ericglau authored Nov 30, 2023
1 parent ae78c41 commit bdaa219
Show file tree
Hide file tree
Showing 28 changed files with 615 additions and 19 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
dist
*.tsbuildinfo
node_modules


.env
.env.local
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Install dependencies with `yarn install`.

`packages/ui` is the interface built in Svelte. `yarn dev` spins up a local server to develop the UI.

You'll need to supply your own environment variables if you want to enable Wizard AI Assistant (OPENAI_API_KEY) and/or logging (REDIS_URL, REDIS_TOKEN).

## Embedding

To embed Contracts Wizard on your site, first include the script tag:
Expand Down
6 changes: 6 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[build]
command = "yarn --cwd packages/ui build"
publish = "packages/ui/public"

edge_functions = "packages/ui/api"

[[edge_functions]]
path = "/ai"
function = "ai"
77 changes: 77 additions & 0 deletions packages/ui/api/ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import OpenAI from 'https://esm.sh/[email protected]'
import { OpenAIStream, StreamingTextResponse } from 'https://esm.sh/[email protected]'
import { erc20Function, erc721Function, erc1155Function, governorFunction, customFunction } from '../src/wiz-functions.ts'
import { Redis } from 'https://esm.sh/@upstash/[email protected]'

export default async (req: Request) => {
try {
const data = await req.json()
const apiKey = Deno.env.get('OPENAI_API_KEY')

const redisUrl = Deno.env.get('REDIS_URL')
const redisToken = Deno.env.get('REDIS_TOKEN')

if (!redisUrl || !redisToken) { throw new Error('missing redis credentials') }

const redis = new Redis({
url: redisUrl,
token: redisToken,
})

const openai = new OpenAI({
apiKey: apiKey
})

const validatedMessages = data.messages.filter((message: { role: string, content: string }) => {
return message.content.length < 500
})

const messages = [{
role: 'system',
content: `
The current options are ${JSON.stringify(data.currentOpts)}.
Please be kind and concise. Keep responses to <100 words.
`.trim()
}, ...validatedMessages]

const response = await openai.chat.completions.create({
model: 'gpt-4-1106-preview',
messages,
functions: [
erc20Function, erc721Function, erc1155Function, governorFunction, customFunction
],
temperature: 0.7,
stream: true
})

const stream = OpenAIStream(response, {
async onCompletion(completion) {
const id = data.chatId
const updatedAt = Date.now()
const payload = {
id,
updatedAt,
messages: [
...messages,
{
content: completion,
role: 'assistant'
}
]
}
const exists = await redis.exists(`chat:${id}`)
if (!exists) {
// @ts-ignore redis types seem to require [key: string]
payload.createdAt = updatedAt
}
await redis.hset(`chat:${id}`, payload)
}
});
return new StreamingTextResponse(stream);

} catch (e) {
return Response.json({
error: 'Could not retrieve results.'
})
}
}
2 changes: 1 addition & 1 deletion packages/ui/public/cairo.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
</div>

<footer>
<p>© <a href="https://openzeppelin.com" target="_blank" rel="noopener noreferrer">OpenZeppelin</a> 2022 |&nbsp;<a href="https://openzeppelin.com/privacy" target="_blank" rel="noopener noreferrer">Privacy</a> |&nbsp;<a href="https://openzeppelin.com/tos" target="_blank" rel="noopener noreferrer">Terms of Service</a></p>
<p>© <a href="https://openzeppelin.com" target="_blank" rel="noopener noreferrer">OpenZeppelin</a> 2022-2023 |&nbsp;<a href="https://openzeppelin.com/privacy" target="_blank" rel="noopener noreferrer">Privacy</a> |&nbsp;<a href="https://openzeppelin.com/tos" target="_blank" rel="noopener noreferrer">Terms of Service</a></p>
</footer>

<!-- Start of HubSpot Embed Code -->
Expand Down
37 changes: 33 additions & 4 deletions packages/ui/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import Dropdown from './Dropdown.svelte';
import OverflowMenu from './OverflowMenu.svelte';
import Tooltip from './Tooltip.svelte';
import Wiz from './Wiz.svelte';
import type { KindedOptions, Kind, Contract, OptionsErrorMessages } from '@openzeppelin/wizard';
import { ContractBuilder, buildGeneric, printContract, sanitizeKind, OptionsError } from '@openzeppelin/wizard';
Expand All @@ -41,6 +42,8 @@
let contract: Contract = new ContractBuilder('MyToken');
$: functionCall && applyFunctionCall()
$: opts = allOpts[tab];
$: {
Expand Down Expand Up @@ -119,9 +122,36 @@
await postConfig(opts, 'download-foundry', language);
}
};
const nameMap = {
erc20: 'ERC20',
erc721: 'ERC721',
erc1155: 'ERC1155',
governor: 'Governor',
custom: 'Custom',
}
let functionCall: {
name?: string,
opts?: any
} = {}
const applyFunctionCall = () => {
if (functionCall.name) {
const name = functionCall.name as keyof typeof nameMap
tab = sanitizeKind(nameMap[name])
allOpts[tab] = {
...allOpts[tab],
...functionCall.opts
}
}
}
</script>

<div class="container flex flex-col gap-4 p-4">
<Wiz bind:functionCall={functionCall} bind:currentOpts={opts}></Wiz>

<div class="header flex flex-row justify-between">
<div class="tab overflow-hidden">
<OverflowMenu>
Expand Down Expand Up @@ -219,7 +249,7 @@
</div>

<div class="flex flex-row gap-4 grow">
<div class="controls w-64 flex flex-col shrink-0 justify-between">
<div class="controls w-64 flex flex-col shrink-0 justify-between h-[calc(100vh-84px)] overflow-auto">
<div class:hidden={tab !== 'ERC20'}>
<ERC20Controls bind:opts={allOpts.ERC20} />
</div>
Expand All @@ -237,8 +267,8 @@
</div>
</div>

<div class="output flex flex-col grow overflow-auto">
<pre class="flex flex-col grow basis-0 overflow-auto"><code class="hljs grow overflow-auto p-4">{@html highlightedCode}</code></pre>
<div class="output flex flex-col grow overflow-auto h-[calc(100vh-84px)]">
<pre class="flex flex-col grow basis-0 overflow-auto"><code class="hljs grow overflow-auto p-4">{@html highlightedCode}</code></pre>
</div>
</div>
</div>
Expand All @@ -249,7 +279,6 @@
border: 1px solid var(--gray-2);
border-radius: 10px;
min-width: 32rem;
min-height: 53rem;
}
.header {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/CustomControls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import UpgradeabilitySection from './UpgradeabilitySection.svelte';
import InfoSection from './InfoSection.svelte';
export const opts: Required<KindedOptions['Custom']> = {
export let opts: Required<KindedOptions['Custom']> = {
kind: 'Custom',
...custom.defaults,
info: { ...infoDefaults }, // create new object since Info is nested
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/ERC1155Controls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import UpgradeabilitySection from './UpgradeabilitySection.svelte';
import InfoSection from './InfoSection.svelte';
export const opts: Required<KindedOptions['ERC1155']> = {
export let opts: Required<KindedOptions['ERC1155']> = {
kind: 'ERC1155',
...erc1155.defaults,
info: { ...infoDefaults }, // create new object since Info is nested
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/ERC20Controls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import UpgradeabilitySection from './UpgradeabilitySection.svelte';
import InfoSection from './InfoSection.svelte';
export const opts: Required<KindedOptions['ERC20']> = {
export let opts: Required<KindedOptions['ERC20']> = {
kind: 'ERC20',
...erc20.defaults,
premint: '', // default to empty premint in UI instead of 0
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/ERC721Controls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import UpgradeabilitySection from './UpgradeabilitySection.svelte';
import InfoSection from './InfoSection.svelte';
export const opts: Required<KindedOptions['ERC721']> = {
export let opts: Required<KindedOptions['ERC721']> = {
kind: 'ERC721',
...erc721.defaults,
info: { ...infoDefaults }, // create new object since Info is nested
Expand Down
25 changes: 25 additions & 0 deletions packages/ui/src/ExperimentalTooltip.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import Tooltip from './Tooltip.svelte';
export let link: string | undefined = undefined;
export let align: 'right' | undefined = undefined;
export let placement: 'top' | 'bottom' | 'left' | 'right' = 'right';
</script>

<Tooltip let:trigger interactive placement={placement} theme="light-yellow border" maxWidth="15em">
<div
use:trigger
class="tooltip bg-blue-300 uppercase text-xs font-semibold px-2 py-1 rounded-md text-white hover:bg-blue-400"
class:ml-auto={align === 'right'}
>
ALPHA
</div>

<div slot="content">
<slot></slot>
{#if link}
<br>
<a target="_blank" rel="noopener noreferrer" href={link}>Read more.</a>
{/if}
</div>
</Tooltip>
2 changes: 1 addition & 1 deletion packages/ui/src/GovernorControls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
const defaults = governor.defaults;
export const opts: Required<KindedOptions['Governor']> = {
export let opts: Required<KindedOptions['Governor']> = {
kind: 'Governor',
...defaults,
proposalThreshold: '', // default to empty in UI
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/HelpTooltip.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
export let link: string | undefined = undefined;
export let align: 'right' | undefined = undefined;
export let placement: 'top' | 'bottom' | 'left' | 'right' = 'right';
</script>

<Tooltip let:trigger interactive placement="right" theme="light-yellow border" maxWidth="15em">
<Tooltip let:trigger interactive placement={placement} theme="light-yellow border" maxWidth="15em">
<svg
use:trigger
class="tooltip"
Expand Down
Loading

0 comments on commit bdaa219

Please sign in to comment.