Skip to content

Commit

Permalink
Edit with AI (#637)
Browse files Browse the repository at this point in the history
* Add edit button with popover

* Basic ai editing working

* Fix Overflow

* Handle AI Messages

* Move AI Button to better position

* Text-to-speech working

* Remove unused

* Show paywall

* Add darkmode

* Add translations

* Available on mobile; on hosted
  • Loading branch information
rob-gordon authored Jan 11, 2024
1 parent caed853 commit 4d82716
Show file tree
Hide file tree
Showing 41 changed files with 1,362 additions and 442 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ TODO.md
keys
ERROR.png
flowchart-fun.feature-reacher.json
.parcel-cache
.parcel-cache

speech*.mp4
70 changes: 70 additions & 0 deletions api/_lib/_llm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { z, ZodObject } from "zod";
import { openai } from "./_openai";
import zodToJsonSchema from "zod-to-json-schema";
import OpenAI from "openai";

type Schemas<T extends Record<string, ZodObject<any>>> = T;

export async function llmMany<T extends Record<string, ZodObject<any>>>(
content: string,
schemas: Schemas<T>
) {
try {
// if the user passes a key "message" in schemas, throw an error
if (schemas.message) throw new Error("Cannot use key 'message' in schemas");

const completion = await openai.chat.completions.create({
messages: [
{
role: "user",
content,
},
],
tools: Object.entries(schemas).map(([key, schema]) => ({
type: "function",
function: {
name: key,
parameters: zodToJsonSchema(schema),
},
})),
model: "gpt-3.5-turbo-1106",
// model: "gpt-4-1106-preview",
});

const choice = completion.choices[0];

if (!choice) throw new Error("No choices returned");

// Must return the full thing, message and multiple tool calls
return simplifyChoice(choice) as SimplifiedChoice<T>;
} catch (error) {
console.error(error);
const message = (error as Error)?.message || "Error with prompt";
throw new Error(message);
}
}

type SimplifiedChoice<T extends Record<string, ZodObject<any>>> = {
message: string;
toolCalls: Array<
{
[K in keyof T]: {
name: K;
args: z.infer<T[K]>;
};
}[keyof T]
>;
};

function simplifyChoice(choice: OpenAI.Chat.Completions.ChatCompletion.Choice) {
return {
message: choice.message.content || "",
toolCalls:
choice.message.tool_calls?.map((toolCall) => ({
name: toolCall.function.name,
// Wish this were type-safe!
args: JSON.parse(toolCall.function.arguments ?? "{}"),
})) || [],
};
}
12 changes: 9 additions & 3 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,27 @@
"@sendgrid/mail": "^7.4.6",
"@supabase/supabase-js": "^2.31.0",
"ajv": "^8.12.0",
"axios": "^0.27.2",
"csv-parse": "^5.3.6",
"date-fns": "^2.29.3",
"graph-selector": "^0.9.11",
"graph-selector": "^0.10.0",
"highlight.js": "^11.8.0",
"marked": "^4.1.1",
"micro": "^10.0.1",
"moniker": "^0.1.2",
"multer": "1.4.5-lts.1",
"notion-to-md": "^2.5.5",
"openai": "^4.10.0",
"openai": "^4.24.2",
"shared": "workspace:*",
"stripe": "^11.11.0"
"stripe": "^11.11.0",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.3"
},
"devDependencies": {
"@swc/jest": "^0.2.24",
"@types/jest": "^29.0.0",
"@types/marked": "^4.0.7",
"@types/multer": "^1.4.11",
"@types/node": "^18.16.17",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
Expand Down
42 changes: 42 additions & 0 deletions api/prompt/edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { VercelApiHandler } from "@vercel/node";
import { llmMany } from "../_lib/_llm";
import { z } from "zod";

const nodeSchema = z.object({
// id: z.string(),
// classes: z.string(),
label: z.string(),
});

const edgeSchema = z.object({
from: z.string(),
to: z.string(),
label: z.string().optional().default(""),
});

const graphSchema = z.object({
nodes: z.array(nodeSchema),
edges: z.array(edgeSchema),
});

const handler: VercelApiHandler = async (req, res) => {
const { graph, prompt } = req.body;
if (!graph || !prompt) {
throw new Error("Missing graph or prompt");
}

const result = await llmMany(
`You are a one-shot AI flowchart assistant. Help the user with a flowchart or diagram. Here is the current state of the flowchart:
${JSON.stringify(graph, null, 2)}
Here is the user's message:
${prompt}`,
{
updateGraph: graphSchema,
}
);

res.json(result);
};

export default handler;
27 changes: 27 additions & 0 deletions api/prompt/speech-to-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { VercelApiHandler } from "@vercel/node";
import { openai } from "../_lib/_openai";
import { toFile } from "openai";

const handler: VercelApiHandler = async (req, res) => {
try {
const { audioUrl } = req.body;

if (!audioUrl) {
res.status(400).json({ ok: false, error: "No audioUrl provided" });
return;
}

const base64Data = audioUrl.split(";base64,").pop();
const binaryData = Buffer.from(base64Data, "base64");
const transcription = await openai.audio.transcriptions.create({
file: await toFile(binaryData, "audio.mp4"),
model: "whisper-1",
});
res.send(transcription.text);
} catch (error) {
console.error(error);
res.status(500).json({ ok: false, error: "Something went wrong" });
}
};

export default handler;
3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@radix-ui/react-select": "^1.2.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-hook/throttle": "^2.2.0",
Expand Down Expand Up @@ -77,7 +78,7 @@
"file-saver": "^2.0.5",
"formulaic": "workspace:*",
"framer-motion": "^10.13.1",
"graph-selector": "^0.9.12",
"graph-selector": "^0.10.0",
"gray-matter": "^4.0.2",
"highlight.js": "^11.7.0",
"immer": "^9.0.16",
Expand Down
Binary file added app/public/images/ai-edit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion app/scripts/autotranslations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ for (const locale of locales) {
`Translating ${batch.length} phrases... (${retries} retries)`
);
const response = await openai.createCompletion({
model: "text-davinci-003",
model: "gpt-3.5-turbo-instruct",
prompt,
max_tokens: 2048,
temperature: 0.5,
Expand Down
10 changes: 7 additions & 3 deletions app/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Trans } from "@lingui/macro";
import { House, Recycle } from "phosphor-react";
import { ReactQueryDevtools } from "react-query/devtools";
import { BrowserRouter } from "react-router-dom";
import * as Toast from "@radix-ui/react-toast";

import { Button2 } from "../ui/Shared";
import Loading from "./Loading";
Expand All @@ -35,9 +36,12 @@ export default function App() {
<Sentry.ErrorBoundary fallback={ErrorFallback}>
<Elements stripe={stripePromise}>
<Suspense fallback={<Loading />}>
<TooltipProvider>
<Router />
</TooltipProvider>
<Toast.Provider swipeDirection="right">
<TooltipProvider>
<Router />
<Toast.Viewport className="[--viewport-padding:_25px] fixed bottom-0 right-0 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[2147483647] outline-none" />
</TooltipProvider>
</Toast.Provider>
<ReactQueryDevtools />
</Suspense>
</Elements>
Expand Down
Loading

0 comments on commit 4d82716

Please sign in to comment.