Skip to content
Draft
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
14 changes: 12 additions & 2 deletions apps/app/src/components/prDrafts/DraftingTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DocumentPrDraftRecord, PrDraftDocumentStatus } from "@cloudy/utils/common";
import { DocumentPrDraftRecord, PrDraftDocumentModificationType, PrDraftDocumentStatus } from "@cloudy/utils/common";
import { BookDashedIcon, FileCheckIcon, XIcon } from "lucide-react";

export const DraftingTitle = ({ documentDraft }: { documentDraft: DocumentPrDraftRecord }) => {
Expand All @@ -11,10 +11,20 @@ export const DraftingTitle = ({ documentDraft }: { documentDraft: DocumentPrDraf
[PrDraftDocumentStatus.SKIPPED]: <XIcon className="size-4 text-secondary" />,
};

const prefixStates = {
[PrDraftDocumentModificationType.CREATE]: "Creating",
[PrDraftDocumentModificationType.EDIT]: "Editing",
};

return (
<div className="flex flex-1 flex-row items-start gap-1">
<div className="shrink-0 pt-0.5">{iconStates[status]}</div>
<span className="flex-1 truncate text-sm font-medium text-secondary">{documentDraft?.path}</span>
<div className="flex-1 truncate text-sm font-medium text-secondary">
<span className="font-normal text-tertiary">
{prefixStates[documentDraft.modification_type as PrDraftDocumentModificationType] + " "}
</span>
<span>{documentDraft?.path}</span>
</div>
</div>
);
};
2 changes: 1 addition & 1 deletion apps/app/src/views/navigation/PendingPullRequests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const useOpenPrs = () => {
await supabase
.from("pull_request_metadata")
.select(
"id, pr_number, pr_status, repo:repository_connections(owner,name), document_pr_drafts(*, document:thoughts(id, title, content_md))",
"id, pr_number, pr_status, repo:repository_connections(owner,name), document_pr_drafts(*, document:thoughts!document_id(id, title, content_md))",
)
.eq("project_id", project!.id),
);
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/views/prDetail/PrDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const PrDetailView = () => {
};

return (
<div className="w-full px-16 pb-8 pt-24">
<div className="mx-auto w-full max-w-screen-lg px-16 pb-8 pt-24">
<div className="mb-6 flex flex-col gap-2">
<div className="flex flex-row justify-between gap-2">
<div className="flex flex-col gap-1">
Expand Down
14 changes: 11 additions & 3 deletions apps/app/src/views/prDetail/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrDocsStatus, PrDraftDocumentStatus, handleSupabaseError } from "@cloudy/utils/common";
import { PrDocsStatus, PrDraftDocumentStatus, fixOneToOne, handleSupabaseError } from "@cloudy/utils/common";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";

Expand Down Expand Up @@ -83,15 +83,23 @@ export const usePrDetail = () => {
return useQuery({
queryKey: prQueryKeys.prDetail(prMetadataId!),
queryFn: async () => {
return handleSupabaseError(
const data = handleSupabaseError(
await supabase
.from("pull_request_metadata")
.select(
"*, repo:repository_connections(owner,name), document_pr_drafts(*, document:thoughts(id, title, content_md))",
"*, repo:repository_connections(owner,name), document_pr_drafts(*, document:thoughts!document_id(id, title, content_md))",
)
.eq("id", prMetadataId!)
.single(),
);

return {
...data,
document_pr_drafts: data.document_pr_drafts.map(draft => ({
...draft,
document: fixOneToOne(draft.document),
})),
};
},
});
};
Expand Down
119 changes: 91 additions & 28 deletions apps/web/app/api/ai/create-draft-for-pr/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
LibraryItem,
PrDraftDocumentModificationType,
PrStatus,
RepositoryConnectionRecord,
getLibraryItems,
Expand All @@ -8,6 +10,7 @@ import {
zip,
} from "@cloudy/utils/common";
import { Database } from "@repo/db";
import * as Sentry from "@sentry/nextjs";
import { SupabaseClient } from "@supabase/supabase-js";
import { generateObject, generateText, tool } from "ai";
import { z } from "zod";
Expand All @@ -28,14 +31,28 @@ const documentSchema = z.object({
.string()
.describe('The path to the document within the library, for example "/folder1/Folder 2/Document title here"'),
title: z.string().describe("The title of the document"),
content: z.string().describe("The markdown contents of the document"),
content: z.string().describe("The entire markdown contents of the document"),
type: z.enum(["create", "edit"]).describe("Whether to create or edit an existing document"),
});

type Document = z.infer<typeof documentSchema>;
type DocumentEdit = z.infer<typeof documentSchema>;

const getLibraryAsPrompt = async (workspaceId: string, projectId: string, supabase: SupabaseClient<Database>) => {
const libraryItems = await getLibraryItems({ workspaceId, projectId }, supabase);
const getDocumentAsPrompt = async (documentId: string, supabase: SupabaseClient<Database>) => {
const document = handleSupabaseError(
await supabase.from("thoughts").select("title, content_md").eq("id", documentId).single(),
);

return `<document>
<title>
${document.title}
</title>
<content>
${document.content_md}
</content>
</document>`;
};

const getLibraryAsPrompt = async (libraryItems: LibraryItem[]) => {
if (libraryItems.length === 0) {
return `There currently isn't any existing documentation for this project.`;
}
Expand All @@ -50,16 +67,14 @@ ${libraryItems.map(item => `- ${item.type} ID "${item.id.slice(0, 6)}": "${item.
const makeSystemPrompt = () => {
return `You are an expert at creating documentation for projects. You are able to create folders/paths and markdown documents.
- You can create documents in the root of the library ("/Document name") or in any subfolder: ("/path/to/folder/Document title here").
- You do not need to create a /docs folder, the root folder is already for docs.`;
- You do not need to create a /docs folder, the root folder is already for docs.
- You have tools to view existing documents and to create/edit documents.

Create effective documentation for a whole project's codebase, not just for the pull request.`;
};

const makePrDocsGenerationPrompt = async (
payload: PullRequestDocsGenerationDetails,
workspaceId: string,
projectId: string,
supabase: SupabaseClient<Database>,
) => {
return `${await getLibraryAsPrompt(workspaceId, projectId, supabase)}
const makePrDocsGenerationPrompt = async (payload: PullRequestDocsGenerationDetails, libraryItems: LibraryItem[]) => {
return `${await getLibraryAsPrompt(libraryItems)}

- You don't need to provide a h1 title again in the document contents, place the title in the title field.

Expand Down Expand Up @@ -126,6 +141,27 @@ export const createDraftForPr = async (

const diffText = comparison.data.files?.map(file => file.patch).join("\n\n") ?? "";

if (diffText.length > 60000) {
await octokit.rest.issues.createComment({
owner: repositoryConnection.owner,
repo: repositoryConnection.name,
issue_number: pullRequestNumber,
body: `👋 Looks like your changes are too big to generate docs for, I'm skipping this PR. We're working on supporting larger diffs soon!`,
});

console.log("Diff text is too long, currently not supported");

Sentry.captureMessage("Diff text is too long, currently not supported", {
extra: {
workspace,
repositoryConnectionId: repositoryConnection.id,
pullRequestNumber,
},
});

return;
}

const {
object: { needsDocs },
} = await generateObject({
Expand All @@ -145,33 +181,58 @@ export const createDraftForPr = async (
body: `👋 Looks like your changes don't need any docs, you're all clear!`,
});

console.log("No docs needed");

return;
}

// 1. Determine whether the pr needs any docs
// 2. Generate the docs, we'll need to support as many pages as needed
// 2.1. Also, ideally we get a folder structure for the docs
// 3. Somehow we also make these docs only draft documents, that get deleted when the PR is closed and published when the PR is merged
const libraryItems = await getLibraryItems({ workspaceId: workspace.id, projectId: project.id }, supabase);

const libraryPathsToItems = Object.fromEntries(libraryItems.map(item => [item.path, item]));

const documents: Document[] = [];
const documents: DocumentEdit[] = [];

const { text } = await generateText({
model: heliconeAnthropic.languageModel("claude-3-5-haiku-20241022"),
system: makeSystemPrompt(),
prompt: await makePrDocsGenerationPrompt(
{ title, description: description ?? "", diffText },
workspace.id,
project.id,
supabase,
),
prompt: await makePrDocsGenerationPrompt({ title, description: description ?? "", diffText }, libraryItems),
tools: {
createDocument: tool({
description: "Create a document",
viewDocuments: tool({
description: "View documents",
parameters: z.object({
paths: z.array(z.string()).describe("The paths to the documents within the library"),
}),
execute: async ({ paths }) => {
return await Promise.all(
paths.map(async path => {
const item = libraryPathsToItems[path];

if (!item) {
return `Error: ${path} is not a valid path`;
}

if (item.type === "folder") {
return `Error: ${item.path} is a folder, not a document`;
}

return await getDocumentAsPrompt(item.id, supabase);
}),
);
},
}),
modifyDocument: tool({
description: "Edit or create a document",
parameters: documentSchema,
execute: async ({ path, title, content }) => {
documents.push({ path, title, content });
execute: async ({ path, title, content, type }) => {
if (type === "edit" && !libraryPathsToItems[path]) {
return `Error: ${path} is not a valid path, cannot edit`;
} else if (type === "create" && libraryPathsToItems[path]) {
return `Error: ${path} is already a document, cannot create`;
}

documents.push({ path, title, content, type });

return "Document created";
return "Done";
},
}),
},
Expand Down Expand Up @@ -213,6 +274,8 @@ export const createDraftForPr = async (
pr_metadata_id: prMetadata.id,
document_id: documentId,
path: document.path,
type: document.type as PrDraftDocumentModificationType,
original_document_id: document.type === "create" ? null : libraryPathsToItems[document.path]!.id,
})),
)
.select("id"),
Expand Down
27 changes: 27 additions & 0 deletions apps/web/app/api/ai/create-draft-for-pr/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PrStatus, handleSupabaseError } from "@cloudy/utils/common";
import { NextRequest, NextResponse } from "next/server";

import { getSupabase } from "app/api/utils/supabase";

import { createDraftForPr } from ".";

//** Use only for testing */
export const POST = async (req: NextRequest) => {
const supabase = await getSupabase({ mode: "service", bypassAuth: true });
const repositoryConnection = handleSupabaseError(
await supabase.from("repository_connections").select("*").eq("id", "ca076ea0-4d17-47ff-a4a3-851027c839b6").single(),
);
const diffText = await createDraftForPr(
repositoryConnection,
36,
"feat(pages): Introduce public library v0",
"Allows you to partially make any document/folder of your library public on cloudy pages.",
PrStatus.OPEN,
"325946f67d701b42a9fbb1a09efb79c02b5c0364",
"abf4bd2759bc0789b9ec7e475387ccb8cda2216f",
);

return NextResponse.json({
diffText,
});
};
3 changes: 3 additions & 0 deletions packages/db/src/database.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,7 @@ export type Database = {
created_at: string
document_id: string
id: string
modification_type: string
path: string | null
pr_metadata_id: string
status: string
Expand All @@ -961,6 +962,7 @@ export type Database = {
created_at?: string
document_id: string
id?: string
modification_type?: string
path?: string | null
pr_metadata_id: string
status?: string
Expand All @@ -969,6 +971,7 @@ export type Database = {
created_at?: string
document_id?: string
id?: string
modification_type?: string
path?: string | null
pr_metadata_id?: string
status?: string
Expand Down
5 changes: 5 additions & 0 deletions packages/utils/src/common/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export enum PrDraftDocumentStatus {
SKIPPED = "skipped",
}

export enum PrDraftDocumentModificationType {
CREATE = "create",
EDIT = "edit",
}

export const makeGithubPrUrl = (
owner: string,
repo: string,
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/common/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export const getLibraryItems = async (
title,
created_at
),
document_pr_drafts(
document_pr_drafts!document_id(
id
)
`
Expand Down
Loading