Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions SELF-HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Please note the following limitations when self-hosting:

- **Billing**: Currently only supported through Stripe integration
- **Custom Domains**: Only supported when deployed on Vercel
- **AI Features**: All AI functionality is channeled through ManagePrompt and requires their service
- **AI Features**: All AI functionality is channeled through OpenRouter

## Environment Configuration

Expand All @@ -42,8 +42,6 @@ Make sure to configure the following in your environment files:
- Database connection details (Supabase)
- Authentication keys
- Stripe keys (if using billing features)
- ManagePrompt API keys (if using AI features)
- Any other third-party service credentials

For detailed environment variable setup, refer to the `.env.example` files in the respective app directories.

13 changes: 8 additions & 5 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
NEXT_PUBLIC_PAGES_DOMAIN=http://localhost:3000

# Supabase details from https://app.supabase.io
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
Expand Down Expand Up @@ -31,7 +29,12 @@ ARCJET_KEY=
# CMS
NEXT_PUBLIC_SANITY_PROJECT_ID=jeixxcw8

# ManagePrompt
MANAGEPROMPT_SECRET=
MANAGEPROMPT_CHANGEGPT_WORKFLOW_ID=
# OpenRouter AI
OPENROUTER_API_KEY=

# GitHub changelog agent
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
GITHUB_WEBHOOK_SECRET=
GITHUB_APP_SLUG=
NEXT_PUBLIC_GITHUB_APP_URL=
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useCompletion } from "@ai-sdk/react";
import { SpinnerWithSpacing } from "@changes-page/ui";
import { convertMarkdownToPlainText } from "@changes-page/utils";
import { Dialog, Transition } from "@headlessui/react";
import { LightningBoltIcon } from "@heroicons/react/solid";
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
import { getStreamingUrl } from "../../utils/useAiAssistant";
import { Fragment, useEffect, useRef } from "react";
import { Streamdown } from "streamdown";
import { PrimaryButton } from "../core/buttons.component";
import { notifyError } from "../core/toast.component";

Expand All @@ -13,61 +13,21 @@ export default function AiExpandConceptPromptDialogComponent({
content,
insertContentCallback,
}) {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<string | null>(null);
const cancelButtonRef = useRef(null);

const expandConcept = useCallback(async (text) => {
setLoading(true);

const { url } = await getStreamingUrl(
"wf_0075a2a911339f610bcfc404051cce3e"
);

const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: text,
}),
});

if (!response.ok) {
notifyError("Too many requests");
}

// This data is a ReadableStream
const data = response.body;
if (!data) {
return;
}

const reader = data.getReader();
const decoder = new TextDecoder();
let done = false;

setLoading(false);

while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value);
setResult((prev) => (prev ?? "") + chunkValue);
}
}, []);
const { completion, complete, isLoading, setCompletion } = useCompletion({
api: "/api/ai/expand-concept",
streamProtocol: "text",
onError: () => {
setOpen(false);
notifyError("Failed to process request, please contact support.");
},
});

useEffect(() => {
if (open && content) {
setLoading(true);
setResult(null);

expandConcept(convertMarkdownToPlainText(content)).catch(() => {
setLoading(false);
setOpen(false);
notifyError("Failed to process request, please contact support.");
});
setCompletion("");
complete(content);
}
}, [open, content]);

Expand Down Expand Up @@ -124,18 +84,22 @@ export default function AiExpandConceptPromptDialogComponent({
aria-hidden="true"
/>

{loading ? "Loading..." : `Check out this draft`}
{isLoading && !completion
? "Loading..."
: isLoading
? "Expanding..."
: "Check out this draft"}
</Dialog.Title>

<div className="mt-5 w-full">
<div className="mt-1 space-y-1">
<dd className="mt-1 text-sm text-gray-900">
<div className="rounded-md border border-gray-200 dark:border-gray-600 dark:divide-gray-600 p-4">
{loading && <SpinnerWithSpacing />}
{isLoading && !completion && <SpinnerWithSpacing />}

<p className="text-black dark:text-white whitespace-pre-wrap">
{result}
</p>
<div className="text-black dark:text-white prose dark:prose-invert prose-sm max-w-none">
<Streamdown>{completion}</Streamdown>
</div>
</div>
</dd>
</div>
Expand All @@ -155,10 +119,10 @@ export default function AiExpandConceptPromptDialogComponent({
</button>

<PrimaryButton
disabled={loading || !result}
disabled={isLoading || !completion}
className="mr-1 disabled:cursor-not-allowed disabled:bg-gray-400"
label="Copy to post"
onClick={() => insertContentCallback(result)}
onClick={() => insertContentCallback(completion)}
/>
</div>
</div>
Expand Down
79 changes: 22 additions & 57 deletions apps/web/components/dialogs/ai-prood-read-dialog.component.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,27 @@
import { useCompletion } from "@ai-sdk/react";
import { SpinnerWithSpacing } from "@changes-page/ui";
import { convertMarkdownToPlainText } from "@changes-page/utils";
import { Dialog, Transition } from "@headlessui/react";
import { LightningBoltIcon } from "@heroicons/react/solid";
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
import { getStreamingUrl } from "../../utils/useAiAssistant";
import { Fragment, useEffect, useRef } from "react";
import { Streamdown } from "streamdown";
import { notifyError } from "../core/toast.component";

export default function AiProofReadDialogComponent({ open, setOpen, content }) {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<string | null>(null);
const cancelButtonRef = useRef(null);

const proofRead = useCallback(async (text) => {
setLoading(true);

const { url } = await getStreamingUrl(
"wf_5a7eaceda859ee21c07771aaaecc9826"
);

const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: text,
}),
});

if (!response.ok) {
notifyError("Too many requests");
}

const data = response.body;
if (!data) {
return;
}

const reader = data.getReader();
const decoder = new TextDecoder();
let done = false;

setLoading(false);

while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value);
setResult((prev) => (prev ?? "") + chunkValue);
}
}, []);
const { completion, complete, isLoading, setCompletion } = useCompletion({
api: "/api/ai/proof-read",
streamProtocol: "text",
onError: () => {
setOpen(false);
notifyError("Failed to process request, please contact support.");
},
});

useEffect(() => {
if (open && content) {
setLoading(true);
setResult(null);

proofRead(convertMarkdownToPlainText(content)).catch(() => {
setLoading(false);
setOpen(false);
notifyError("Failed to process request, please contact support.");
});
setCompletion("");
complete(content);
}
}, [open, content]);

Expand Down Expand Up @@ -117,18 +78,22 @@ export default function AiProofReadDialogComponent({ open, setOpen, content }) {
aria-hidden="true"
/>

{loading ? "Loading..." : "Here is the proofread result!"}
{isLoading && !completion
? "Loading..."
: isLoading
? "Proofreading..."
: "Here is the proofread result!"}
</Dialog.Title>

<div className="mt-5 w-full">
<div className="mt-1 space-y-1">
<dd className="mt-1 text-sm text-gray-900">
<div className="rounded-md border border-gray-200 dark:border-gray-600 dark:divide-gray-600 p-4">
{loading && <SpinnerWithSpacing />}
{isLoading && !completion && <SpinnerWithSpacing />}

<p className="text-black dark:text-white whitespace-pre-wrap">
{result}
</p>
<div className="text-black dark:text-white prose dark:prose-invert prose-sm max-w-none">
<Streamdown>{completion}</Streamdown>
</div>
</div>
</dd>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { SpinnerWithSpacing } from "@changes-page/ui";
import { convertMarkdownToPlainText } from "@changes-page/utils";
import { Dialog, Transition } from "@headlessui/react";
import { LightningBoltIcon } from "@heroicons/react/solid";
import { Fragment, useEffect, useRef, useState } from "react";
import { promptSuggestTitle } from "../../utils/useAiAssistant";
import { notifyError } from "../core/toast.component";

export default function AiSuggestTitlePromptDialogComponent({
Expand All @@ -21,9 +19,12 @@ export default function AiSuggestTitlePromptDialogComponent({
setLoading(true);
setSuggestions([]);

const text = convertMarkdownToPlainText(content);

promptSuggestTitle(text)
fetch("/api/ai/suggest-title", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
})
.then((res) => res.json())
.then((suggestions) => {
setSuggestions(suggestions);
setLoading(false);
Expand Down
Loading