-
-
Notifications
You must be signed in to change notification settings - Fork 229
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #671 from tone-row/dev
v1.49.0
- Loading branch information
Showing
44 changed files
with
1,535 additions
and
365 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,11 @@ | ||
const priceId = process.env.STRIPE_PRICE_ID; | ||
const priceIdYearly = process.env.STRIPE_PRICE_ID_YEARLY; | ||
const legacyPriceId = process.env.LEGACY_STRIPE_PRICE_ID; | ||
const legacyPriceIdYearly = process.env.LEGACY_STRIPE_PRICE_ID_YEARLY; | ||
const otherValidPriceIdsString = process.env.OTHER_VALID_STRIPE_PRICE_IDS; | ||
const otherValidPriceIds = otherValidPriceIdsString | ||
? otherValidPriceIdsString.split(",") | ||
: []; | ||
export const validStripePrices = [ | ||
priceId, | ||
priceIdYearly, | ||
legacyPriceId, | ||
legacyPriceIdYearly, | ||
...otherValidPriceIds, | ||
].filter(Boolean) as string[]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { z } from "zod"; | ||
import { OpenAIStream, StreamingTextResponse } from "ai"; | ||
import OpenAI from "openai"; | ||
import { stripe } from "../_lib/_stripe"; | ||
import { kv } from "@vercel/kv"; | ||
import { Ratelimit } from "@upstash/ratelimit"; | ||
|
||
export const config = { | ||
runtime: "edge", | ||
}; | ||
|
||
const reqSchema = z.object({ | ||
prompt: z.string().min(1), | ||
}); | ||
|
||
// Create an OpenAI API client (that's edge friendly!) | ||
const openai = new OpenAI({ | ||
apiKey: process.env.OPENAI_API_KEY || "", | ||
}); | ||
|
||
export default async function handler(req: Request) { | ||
const ip = getIp(req); | ||
|
||
let isPro = false, | ||
customerId: null | string = null; | ||
|
||
// Check for auth token | ||
const token = req.headers.get("Authorization"); | ||
|
||
if (token) { | ||
// get sid from token | ||
const sid = token.split(" ")[1]; | ||
|
||
// check if subscription is active or trialing | ||
const sub = await stripe.subscriptions.retrieve(sid); | ||
if (sub.status === "active" || sub.status === "trialing") { | ||
isPro = true; | ||
customerId = sub.customer as string; | ||
} | ||
} | ||
|
||
// Implement rate-limiting based on IP for unauthorized users and customerId for authorized users | ||
// Initialize Upstash Ratelimit | ||
const ratelimit = new Ratelimit({ | ||
redis: kv, | ||
limiter: isPro | ||
? Ratelimit.slidingWindow(3, "1m") // Pro users: 3 requests per minute | ||
: Ratelimit.fixedWindow(1, "30d"), // Unauthenticated users: 1 request per month | ||
}); | ||
|
||
// Determine the key for rate limiting | ||
const rateLimitKey = isPro ? `pro_${customerId}` : `unauth_${ip}`; | ||
|
||
// Check the rate limit | ||
const { success, limit, reset, remaining } = await ratelimit.limit( | ||
rateLimitKey | ||
); | ||
|
||
if (!success) { | ||
return new Response("You have reached your request limit.", { | ||
status: 429, | ||
headers: { | ||
"X-RateLimit-Limit": limit.toString(), | ||
"X-RateLimit-Remaining": remaining.toString(), | ||
"X-RateLimit-Reset": reset.toString(), | ||
}, | ||
}); | ||
} | ||
|
||
const body = await req.json(); | ||
const parsed = reqSchema.safeParse(body); | ||
|
||
if (!parsed.success) { | ||
return new Response(JSON.stringify(parsed.error), { status: 400 }); | ||
} | ||
|
||
// Ask OpenAI for a streaming chat completion given the prompt | ||
const response = await openai.chat.completions.create({ | ||
model: "gpt-4-turbo", | ||
stream: true, | ||
messages: [ | ||
{ | ||
role: "system", | ||
content: `You are the Flowchart Fun creation assistant. When I give you a document respond with a diagram in Flowchart Fun syntax. The Flowchart Fun syntax you use indentation to express a tree shaped graph. You use text before a colon to labels to edges. You link back to earlier nodes by referring to their label in parentheses. The following characters must be escaped when used in a node or edge label: (,:,#, and .\n\nHere is a very simple graph illustrating the syntax: | ||
Node A | ||
Node B | ||
\\(Secret Node) | ||
Node C | ||
label from c to d: Node D | ||
label from d to a: (Node A) | ||
Note: Don't provide any explanation. Don't wrap your response in a code block.`, | ||
}, | ||
{ | ||
role: "user", | ||
content: | ||
`Create a flowchart using flowchart fun syntax based on the following document:\n\n` + | ||
parsed.data.prompt, | ||
}, | ||
], | ||
}); | ||
|
||
// Convert the response into a friendly text-stream | ||
const stream = OpenAIStream(response); | ||
// Respond with the stream | ||
return new StreamingTextResponse(stream); | ||
} | ||
|
||
function getIp(req: Request) { | ||
return ( | ||
req.headers.get("x-real-ip") || | ||
req.headers.get("cf-connecting-ip") || | ||
req.headers.get("x-forwarded-for") || | ||
req.headers.get("x-client-ip") || | ||
req.headers.get("x-cluster-client-ip") || | ||
req.headers.get("forwarded-for") || | ||
req.headers.get("forwarded") || | ||
req.headers.get("via") || | ||
req.headers.get("x-forwarded") || | ||
req.headers.get | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import classNames from "classnames"; | ||
import { globalZ } from "../lib/globalZ"; | ||
import { useDoc } from "../lib/useDoc"; | ||
import { useEffect, useMemo } from "react"; | ||
import { getDefaultText } from "../lib/getDefaultText"; | ||
import { useEditorStore } from "../lib/useEditorStore"; | ||
import { ConvertToFlowchart } from "./ConvertToFlowchart"; | ||
import { EditWithAI } from "./EditWithAI"; | ||
import { usePromptStore } from "../lib/usePromptStore"; | ||
|
||
/** | ||
* Watch the current state of the graph and the users actions and determine | ||
* which, if any, AI tools to display to the user. | ||
*/ | ||
export function AiToolbar() { | ||
const text = useDoc((state) => state.text); | ||
const defaultText = useMemo(() => { | ||
return getDefaultText(); | ||
}, []); | ||
const isDefaultText = text === defaultText; | ||
const selection = useEditorStore((s) => s.selection); | ||
const fullTextSelected = selection.trim() === text.trim(); | ||
const userPasted = useEditorStore((s) => s.userPasted); | ||
const enoughCharacters = text.length > 150; | ||
const lastResult = usePromptStore((s) => s.lastResult); | ||
|
||
// Set the user pasted back to false after 15 seconds, and on unmount | ||
useEffect(() => { | ||
if (userPasted) { | ||
const timeout = setTimeout(() => { | ||
useEditorStore.setState({ userPasted: false }); | ||
}, 15000); | ||
return () => clearTimeout(timeout); | ||
} | ||
}, [userPasted]); | ||
|
||
const convertIsRunning = usePromptStore((s) => s.convertIsRunning); | ||
|
||
// Qualities for displaying Convert to Flowchart button: | ||
// OR | ||
// Convert is currently running | ||
// AND | ||
// Is not the default text | ||
// There is more than 150 characters | ||
// Text is not equal to the last result | ||
// OR | ||
// Full text is selected and is more than 150 characters | ||
// Less than 15 seconds have passed since user pasted more than 150 characters | ||
const showConvertToFlowchart = | ||
convertIsRunning || | ||
(!isDefaultText && | ||
enoughCharacters && | ||
lastResult !== text && | ||
(fullTextSelected || userPasted)); | ||
|
||
return ( | ||
<div | ||
className={classNames( | ||
"drop-shadow-lg absolute top-2 right-2", | ||
globalZ.editWithAiButton | ||
)} | ||
> | ||
{showConvertToFlowchart ? <ConvertToFlowchart /> : <EditWithAI />} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { Trans, t } from "@lingui/macro"; | ||
import { Button2 } from "../ui/Shared"; | ||
import { TreeStructure } from "phosphor-react"; | ||
import { useDoc } from "../lib/useDoc"; | ||
import * as Toast from "@radix-ui/react-toast"; | ||
import { | ||
RATE_LIMIT_EXCEEDED, | ||
convertToFlowchart, | ||
} from "../lib/convertToFlowchart"; | ||
import { | ||
startConvert, | ||
stopConvert, | ||
usePromptStore, | ||
} from "../lib/usePromptStore"; | ||
import { useEditorStore } from "../lib/useEditorStore"; | ||
import { useCallback, useContext, useState } from "react"; | ||
import { AppContext } from "./AppContextProvider"; | ||
import { isError } from "../lib/helpers"; | ||
import { useIsProUser } from "../lib/hooks"; | ||
import { showPaywall } from "../lib/usePaywallModalStore"; | ||
|
||
export function ConvertToFlowchart() { | ||
const convertIsRunning = usePromptStore((s) => s.convertIsRunning); | ||
const customer = useContext(AppContext).customer; | ||
const sid = customer?.subscription?.id; | ||
const isProUser = useIsProUser(); | ||
const [errorMessage, setErrorMessage] = useState<string | null>(null); | ||
|
||
// Handle the error based on status | ||
const handleError = useCallback( | ||
(error: Error) => { | ||
if (!isProUser && error.message === RATE_LIMIT_EXCEEDED) { | ||
// Show paywall | ||
showPaywall({ | ||
title: t`Transform text into diagrams instantly`, | ||
content: t`Uh oh, you're out of free requests! Upgrade to Flowchart Fun Pro for unlimited diagram conversions, and keep transforming text into clear, visual flowcharts as easily as copy and paste.`, | ||
imgUrl: "/images/ai-convert.png", | ||
toPricingCode: "ConvertToFlowchart", | ||
}); | ||
} else { | ||
if (error.message === RATE_LIMIT_EXCEEDED) { | ||
setErrorMessage(t`Rate limit exceeded. Please try again later.`); | ||
} else { | ||
setErrorMessage(error.message); | ||
} | ||
} | ||
}, | ||
[isProUser] | ||
); | ||
|
||
return ( | ||
<> | ||
<Button2 | ||
onClick={() => { | ||
const prompt = useDoc.getState().text; | ||
startConvert(); | ||
convertToFlowchart(prompt, sid) | ||
.catch((err) => { | ||
if (isError(err)) handleError(err); | ||
}) | ||
.finally(() => { | ||
stopConvert(); | ||
useEditorStore.setState({ userPasted: false }); | ||
}); | ||
}} | ||
disabled={convertIsRunning} | ||
leftIcon={ | ||
<TreeStructure | ||
className="group-hover-tilt-shaking md:-mr-1 -mr-4" | ||
size={18} | ||
/> | ||
} | ||
color="green" | ||
size="sm" | ||
rounded | ||
className="!pt-2 !pb-[9px] !pl-3 !pr-4 disabled:!opacity-100" | ||
data-session-activity="Convert To Flowchart" | ||
isLoading={convertIsRunning} | ||
> | ||
<span className="text-[15px] hidden md:inline"> | ||
<Trans>Convert to Flowchart</Trans> | ||
</span> | ||
</Button2> | ||
<Toast.Root | ||
type="foreground" | ||
duration={4000} | ||
className="ToastRoot bg-red-300 text-red-800 font-bold border-b-2 border-r-2 border-red-500 rounded-md shadow p-4 grid [grid-template-areas:_'title_action'_'description_action'] grid-cols-[auto_max-content] gap-x-4 items-center data-[state=open]:animate-slideIn data-[state=closed]:animate-hide data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-swipeOut" | ||
open={errorMessage !== null} | ||
onOpenChange={(open) => { | ||
if (!open) setErrorMessage(null); | ||
}} | ||
> | ||
<Toast.Description> | ||
<div className="flex text-xs items-center gap-3"> | ||
<p className="leading-normal">{errorMessage}</p> | ||
</div> | ||
</Toast.Description> | ||
</Toast.Root> | ||
</> | ||
); | ||
} |
Oops, something went wrong.