aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCodeTorso <[email protected]>2024-06-20 08:38:21 -0600
committerGitHub <[email protected]>2024-06-20 08:38:21 -0600
commitaf90b960af7a8e6debc059f8ca67af878f0f409a (patch)
tree8097644103d6f49b5b247ddada12e78132167f93
parentadd: animated query input (diff)
parentadded multi-turn conversations (diff)
downloadsupermemory-af90b960af7a8e6debc059f8ca67af878f0f409a.tar.xz
supermemory-af90b960af7a8e6debc059f8ca67af878f0f409a.zip
Merge branch 'codetorso' into kartik
-rw-r--r--SETUP-GUIDE.md15
m---------apps/browser-rendering0
-rw-r--r--apps/cf-ai-backend/package.json2
-rw-r--r--apps/cf-ai-backend/src/helper.ts105
-rw-r--r--apps/cf-ai-backend/src/index.test.ts13
-rw-r--r--apps/cf-ai-backend/src/index.ts143
-rw-r--r--apps/cf-ai-backend/src/prompts/prompt1.ts3
-rw-r--r--apps/cf-ai-backend/src/types.ts5
-rw-r--r--apps/cf-ai-backend/src/utils/chonker.ts3
-rw-r--r--apps/cf-ai-backend/src/utils/seededRandom.ts7
-rw-r--r--apps/cf-ai-backend/tsconfig.json3
-rw-r--r--apps/cf-ai-backend/wrangler.toml2
-rw-r--r--apps/web/app/(auth)/auth-buttons.tsx2
-rw-r--r--apps/web/app/(auth)/signin/page.tsx2
-rw-r--r--apps/web/app/(canvas)/canvas.tsx79
-rw-r--r--apps/web/app/(canvas)/canvas/page.tsx2
-rw-r--r--apps/web/app/(canvas)/dropComponent.tsx76
-rw-r--r--apps/web/app/(canvas)/enabledComp.tsx2
-rw-r--r--apps/web/app/(canvas)/lib/context.tsx11
-rw-r--r--apps/web/app/(canvas)/lib/createEmbeds.ts52
-rw-r--r--apps/web/app/(dash)/chat/chatWindow.tsx189
-rw-r--r--apps/web/app/(dash)/dynamicisland.tsx51
-rw-r--r--apps/web/app/(dash)/home/page.tsx39
-rw-r--r--apps/web/app/(dash)/home/queryinput.tsx96
-rw-r--r--apps/web/app/(dash)/layout.tsx2
-rw-r--r--apps/web/app/(dash)/memories/page.tsx121
-rw-r--r--apps/web/app/(editor)/components/aigenerate.tsx107
-rw-r--r--apps/web/app/(editor)/editor.tsx5
-rw-r--r--apps/web/app/(landing)/Cta.tsx2
-rw-r--r--apps/web/app/(landing)/page.tsx2
-rw-r--r--apps/web/app/actions/doers.ts231
-rw-r--r--apps/web/app/actions/fetchers.ts125
-rw-r--r--apps/web/app/actions/types.ts1
-rw-r--r--apps/web/app/api/[...nextauth]/route.ts2
-rw-r--r--apps/web/app/api/chat/route.ts2
-rw-r--r--apps/web/app/api/editorai/route.ts30
-rw-r--r--apps/web/app/api/ensureAuth.ts4
-rw-r--r--apps/web/app/api/getCount/route.ts8
-rw-r--r--apps/web/app/api/me/route.ts4
-rw-r--r--apps/web/app/api/spaces/route.ts4
-rw-r--r--apps/web/app/api/store/route.ts31
-rw-r--r--apps/web/app/api/unfirlsite/route.ts134
-rw-r--r--apps/web/app/ref/page.tsx8
-rw-r--r--apps/web/cf-env.d.ts13
-rw-r--r--apps/web/env.d.ts8
-rw-r--r--apps/web/lib/constants.ts (renamed from apps/web/app/helpers/constants.ts)6
-rw-r--r--apps/web/lib/get-metadata.ts (renamed from apps/web/app/helpers/lib/get-metadata.ts)0
-rw-r--r--apps/web/lib/get-theme-button.tsx (renamed from apps/web/app/helpers/lib/get-theme-button.tsx)0
-rw-r--r--apps/web/lib/handle-errors.ts (renamed from apps/web/app/helpers/lib/handle-errors.ts)0
-rw-r--r--apps/web/lib/searchParams.ts (renamed from apps/web/app/helpers/lib/searchParams.ts)0
-rw-r--r--apps/web/server/auth.ts (renamed from apps/web/app/helpers/server/auth.ts)0
-rw-r--r--apps/web/server/db/index.ts (renamed from apps/web/app/helpers/server/db/index.ts)0
-rw-r--r--apps/web/server/db/schema.ts (renamed from apps/web/app/helpers/server/db/schema.ts)8
-rw-r--r--package.json6
-rw-r--r--packages/shared-types/index.ts2
-rw-r--r--packages/tailwind-config/globals.css46
56 files changed, 1452 insertions, 362 deletions
diff --git a/SETUP-GUIDE.md b/SETUP-GUIDE.md
index 7d69b545..f51b1cb1 100644
--- a/SETUP-GUIDE.md
+++ b/SETUP-GUIDE.md
@@ -13,12 +13,13 @@
3. Create a `.dev.vars` file in `apps/web` with the following content:
```bash
-GOOGLE_CLIENT_ID="-"
-GOOGLE_CLIENT_SECRET="-"
+GOOGLE_CLIENT_ID="-" // required, visit https://developers.google.com/identity/protocols/oauth2
+GOOGLE_CLIENT_SECRET="-" // required
NEXTAUTH_SECRET='nextauthsecret'
DATABASE_URL='database.sqlite'
NEXTAUTH_URL='http://localhost:3000'
BACKEND_SECURITY_KEY='veryrandomsecuritykey'
+BACKEND_BASE_URL="where your backend is hosted"
```
4. Setup the database:
@@ -28,10 +29,10 @@ First, edit the `wrangler.toml` file in `apps/web` to point the d1 database to y
You can create a d1 database by running this command
```
-wrangler d1 create DATABASE_NAME
+bunx wrangler d1 create <YOUR_DATABASE_NAME>
```
-And then replace these values
+And then replace database_name and database_id with the values
```
[[d1_databases]]
@@ -43,10 +44,12 @@ database_id = "YOUR_DB_ID"
Simply run this command in `apps/web`
```
-wrangler d1 execute dev-d1-anycontext --local --file=db/prepare.sql
+bunx wrangler d1 migrations apply <YOUR_DATABASE_NAME>
```
-If it runs, you can set up the cloud database as well by removing the `--local` flag.
+If it runs, you can set up the cloud database as well by removing the `--local` flag,
+
+if you just want to contribute to frontend then just run `bun run dev` in the root of the project and done! (you won't be able to try ai stuff), otherwise continue...
5. You need to host your own worker for the `apps/cf-ai-backend` module.
diff --git a/apps/browser-rendering b/apps/browser-rendering
-Subproject b37c962365a36cf342a31a196f4908f4f134355
+Subproject 4d21045a45fdbf56b7483d3704ae0474ebf044f
diff --git a/apps/cf-ai-backend/package.json b/apps/cf-ai-backend/package.json
index 480f9601..78353e08 100644
--- a/apps/cf-ai-backend/package.json
+++ b/apps/cf-ai-backend/package.json
@@ -6,7 +6,7 @@
"scripts": {
"test": "jest --verbose",
"deploy": "wrangler deploy",
- "dev": "wrangler dev",
+ "dev": "wrangler dev --remote --port 8686",
"start": "wrangler dev",
"unsafe-reset-vector-db": "wrangler vectorize delete supermem-vector && wrangler vectorize create --dimensions=1536 supermem-vector-1 --metric=cosine"
},
diff --git a/apps/cf-ai-backend/src/helper.ts b/apps/cf-ai-backend/src/helper.ts
index 87495c59..cef781be 100644
--- a/apps/cf-ai-backend/src/helper.ts
+++ b/apps/cf-ai-backend/src/helper.ts
@@ -21,8 +21,6 @@ export async function initQuery(
index: c.env.VECTORIZE_INDEX,
});
- const DEFAULT_MODEL = "gpt-4o";
-
let selectedModel:
| ReturnType<ReturnType<typeof createOpenAI>>
| ReturnType<ReturnType<typeof createGoogleGenerativeAI>>
@@ -52,12 +50,6 @@ export async function initQuery(
break;
}
- if (!selectedModel) {
- throw new Error(
- `Model ${model} not found and default model ${DEFAULT_MODEL} is also not available.`,
- );
- }
-
return { store, model: selectedModel };
}
@@ -72,19 +64,46 @@ export async function deleteDocument({
c: Context<{ Bindings: Env }>;
store: CloudflareVectorizeStore;
}) {
- const toBeDeleted = `${url}-${user}`;
+ const toBeDeleted = `${url}#supermemory-web`;
const random = seededRandom(toBeDeleted);
const uuid =
random().toString(36).substring(2, 15) +
random().toString(36).substring(2, 15);
- await c.env.KV.list({ prefix: uuid }).then(async (keys) => {
- for (const key of keys.keys) {
- await c.env.KV.delete(key.name);
- await store.delete({ ids: [key.name] });
+ const allIds = await c.env.KV.list({ prefix: uuid });
+
+ if (allIds.keys.length > 0) {
+ const savedVectorIds = allIds.keys.map((key) => key.name);
+ const vectors = await c.env.VECTORIZE_INDEX.getByIds(savedVectorIds);
+ // We don't actually delete document directly, we just remove the user from the metadata.
+ // If there's no user left, we can delete the document.
+ const newVectors = vectors.map((vector) => {
+ delete vector.metadata[`user-${user}`];
+
+ // Get count of how many users are left
+ const userCount = Object.keys(vector.metadata).filter((key) =>
+ key.startsWith("user-"),
+ ).length;
+
+ // If there's no user left, we can delete the document.
+ // need to make sure that every chunk is deleted otherwise it would be problematic.
+ if (userCount === 0) {
+ store.delete({ ids: savedVectorIds });
+ void Promise.all(savedVectorIds.map((id) => c.env.KV.delete(id)));
+ return null;
+ }
+
+ return vector;
+ });
+
+ // If all vectors are null (deleted), we can delete the KV too. Otherwise, we update (upsert) the vectors.
+ if (newVectors.every((v) => v === null)) {
+ await c.env.KV.delete(uuid);
+ } else {
+ await c.env.VECTORIZE_INDEX.upsert(newVectors.filter((v) => v !== null));
}
- });
+ }
}
export async function batchCreateChunksAndEmbeddings({
@@ -98,19 +117,47 @@ export async function batchCreateChunksAndEmbeddings({
chunks: string[];
context: Context<{ Bindings: Env }>;
}) {
- const ourID = `${body.url}-${body.user}`;
+ //! NOTE that we use #supermemory-web to ensure that
+ //! If a user saves it through the extension, we don't want other users to be able to see it.
+ // Requests from the extension should ALWAYS have a unique ID with the USERiD in it.
+ // I cannot stress this enough, important for security.
+ const ourID = `${body.url}#supermemory-web`;
+ const random = seededRandom(ourID);
+ const uuid =
+ random().toString(36).substring(2, 15) +
+ random().toString(36).substring(2, 15);
- await deleteDocument({ url: body.url, user: body.user, c: context, store });
+ const allIds = await context.env.KV.list({ prefix: uuid });
- const random = seededRandom(ourID);
+ // If some chunks for that content already exist, we'll just update the metadata to include
+ // the user.
+ if (allIds.keys.length > 0) {
+ const savedVectorIds = allIds.keys.map((key) => key.name);
+ const vectors = await context.env.VECTORIZE_INDEX.getByIds(savedVectorIds);
+
+ // Now, we'll update all vector metadatas with one more userId and all spaceIds
+ const newVectors = vectors.map((vector) => {
+ vector.metadata = {
+ ...vector.metadata,
+ [`user-${body.user}`]: 1,
+
+ // For each space in body, add the spaceId to the vector metadata
+ ...(body.spaces ?? [])?.reduce((acc, space) => {
+ acc[`space-${body.user}-${space}`] = 1;
+ return acc;
+ }, {}),
+ };
+
+ return vector;
+ });
+
+ await context.env.VECTORIZE_INDEX.upsert(newVectors);
+ return;
+ }
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
- const uuid =
- random().toString(36).substring(2, 15) +
- random().toString(36).substring(2, 15) +
- "-" +
- i;
+ const chunkId = `${uuid}-${i}`;
const newPageContent = `Title: ${body.title}\nDescription: ${body.description}\nURL: ${body.url}\nContent: ${chunk}`;
@@ -121,19 +168,25 @@ export async function batchCreateChunksAndEmbeddings({
metadata: {
title: body.title?.slice(0, 50) ?? "",
description: body.description ?? "",
- space: body.space ?? "",
url: body.url,
- user: body.user,
+ type: body.type ?? "page",
+ content: newPageContent,
+
+ [`user-${body.user}`]: 1,
+ ...body.spaces?.reduce((acc, space) => {
+ acc[`space-${body.user}-${space}`] = 1;
+ return acc;
+ }, {}),
},
},
],
{
- ids: [uuid],
+ ids: [chunkId],
},
);
console.log("Docs added: ", docs);
- await context.env.KV.put(uuid, ourID);
+ await context.env.KV.put(chunkId, ourID);
}
}
diff --git a/apps/cf-ai-backend/src/index.test.ts b/apps/cf-ai-backend/src/index.test.ts
deleted file mode 100644
index bbf66fb5..00000000
--- a/apps/cf-ai-backend/src/index.test.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import app from ".";
-
-// TODO: write more tests
-describe("Test the application", () => {
- it("Should return 200 response", async () => {
- const res = await app.request("http://localhost/");
- expect(res.status).toBe(200);
- }),
- it("Should return 404 response", async () => {
- const res = await app.request("http://localhost/404");
- expect(res.status).toBe(404);
- });
-});
diff --git a/apps/cf-ai-backend/src/index.ts b/apps/cf-ai-backend/src/index.ts
index 2dbb2d0c..e89d170c 100644
--- a/apps/cf-ai-backend/src/index.ts
+++ b/apps/cf-ai-backend/src/index.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
import { Hono } from "hono";
-import { CoreMessage, streamText } from "ai";
+import { CoreMessage, generateText, streamText } from "ai";
import { chatObj, Env, vectorObj } from "./types";
import {
batchCreateChunksAndEmbeddings,
@@ -18,7 +18,12 @@ import { swaggerUI } from "@hono/swagger-ui";
const app = new Hono<{ Bindings: Env }>();
-app.get("/doc", swaggerUI({ url: "/doc" }));
+app.get(
+ "/ui",
+ swaggerUI({
+ url: "/doc",
+ }),
+);
// ------- MIDDLEWARES -------
app.use("*", poweredBy());
@@ -34,6 +39,17 @@ app.use("/api/", async (c, next) => {
});
// ------- MIDDLEWARES END -------
+const fileSchema = z
+ .instanceof(File)
+ .refine(
+ (file) => file.size <= 10 * 1024 * 1024,
+ "File size should be less than 10MB",
+ ) // Validate file size
+ .refine(
+ (file) => ["image/jpeg", "image/png", "image/gif"].includes(file.type),
+ "Invalid file type",
+ ); // Validate file type
+
app.get("/", (c) => {
return c.text("Supermemory backend API is running!");
});
@@ -57,6 +73,82 @@ app.post("/api/add", zValidator("json", vectorObj), async (c) => {
return c.json({ status: "ok" });
});
+app.post(
+ "/api/add-with-image",
+ zValidator(
+ "form",
+ z.object({
+ images: z
+ .array(fileSchema)
+ .min(1, "At least one image is required")
+ .optional(),
+ "images[]": z
+ .array(fileSchema)
+ .min(1, "At least one image is required")
+ .optional(),
+ text: z.string().optional(),
+ spaces: z.array(z.string()).optional(),
+ url: z.string(),
+ user: z.string(),
+ }),
+ (c) => {
+ console.log(c);
+ },
+ ),
+ async (c) => {
+ const body = c.req.valid("form");
+
+ const { store } = await initQuery(c);
+
+ if (!(body.images || body["images[]"])) {
+ return c.json({ status: "error", message: "No images found" }, 400);
+ }
+
+ const imagePromises = (body.images ?? body["images[]"]).map(
+ async (image) => {
+ const buffer = await image.arrayBuffer();
+ const input = {
+ image: [...new Uint8Array(buffer)],
+ prompt:
+ "What's in this image? caption everything you see in great detail. If it has text, do an OCR and extract all of it.",
+ max_tokens: 1024,
+ };
+ const response = await c.env.AI.run(
+ "@cf/llava-hf/llava-1.5-7b-hf",
+ input,
+ );
+ console.log(response.description);
+ return response.description;
+ },
+ );
+
+ const imageDescriptions = await Promise.all(imagePromises);
+
+ await batchCreateChunksAndEmbeddings({
+ store,
+ body: {
+ url: body.url,
+ user: body.user,
+ type: "image",
+ description:
+ imageDescriptions.length > 1
+ ? `A group of ${imageDescriptions.length} images on ${body.url}`
+ : imageDescriptions[0],
+ spaces: body.spaces,
+ pageContent: imageDescriptions.join("\n"),
+ title: "Image content from the web",
+ },
+ chunks: [
+ imageDescriptions,
+ ...(body.text ? chunkText(body.text, 1536) : []),
+ ].flat(),
+ context: c,
+ });
+
+ return c.json({ status: "ok" });
+ },
+);
+
app.get(
"/api/ask",
zValidator(
@@ -101,12 +193,14 @@ app.post(
const body = c.req.valid("json");
const sourcesOnly = query.sourcesOnly === "true";
- const spaces = query.spaces?.split(",") ?? [""];
+ const spaces = query.spaces?.split(",") ?? [undefined];
// Get the AI model maker and vector store
const { model, store } = await initQuery(c, query.model);
- const filter: VectorizeVectorMetadataFilter = { user: query.user };
+ const filter: VectorizeVectorMetadataFilter = {
+ [`user-${query.user}`]: 1,
+ };
console.log("Spaces", spaces);
// Converting the query to a vector so that we can search for similar vectors
@@ -118,9 +212,9 @@ app.post(
// SLICED to 5 to avoid too many queries
for (const space of spaces.slice(0, 5)) {
console.log("space", space);
- if (space !== "") {
+ if (!space && spaces.length > 1) {
// it's possible for space list to be [undefined] so we only add space filter conditionally
- filter.space = space;
+ filter[`space-${query.user}-${space}`] = 1;
}
// Because there's no OR operator in the filter, we have to make multiple queries
@@ -173,29 +267,20 @@ app.post(
dataPoint.id.toString(),
);
- // We are getting the content ID back, so that the frontend can show the actual sources properly.
- // it IS a lot of DB calls, i completely agree.
- // TODO: return metadata value here, so that the frontend doesn't have to re-fetch anything.
const storedContent = await Promise.all(
idsAsStrings.map(async (id) => await c.env.KV.get(id)),
);
- return c.json({ ids: storedContent });
- }
-
- const vec = responses.matches.map((data) => ({ metadata: data.metadata }));
+ const metadata = normalizedData.map((datapoint) => datapoint.metadata);
- const vecWithScores = vec.map((v, i) => ({
- ...v,
- score: sortedHighScoreData[i].score,
- normalisedScore: sortedHighScoreData[i].normalizedScore,
- }));
+ return c.json({ ids: storedContent, metadata });
+ }
- const preparedContext = vecWithScores.map(
- ({ metadata, score, normalisedScore }) => ({
+ const preparedContext = normalizedData.map(
+ ({ metadata, score, normalizedScore }) => ({
context: `Website title: ${metadata!.title}\nDescription: ${metadata!.description}\nURL: ${metadata!.url}\nContent: ${metadata!.text}`,
score,
- normalisedScore,
+ normalizedScore,
}),
);
@@ -245,4 +330,20 @@ app.delete(
},
);
+// ERROR #1 - this is the api that the editor uses, it is just a scrape off of /api/chat so you may check that out
+app.get('/api/editorai', zValidator(
+ "query",
+ z.object({
+ context: z.string(),
+ request: z.string(),
+ }),
+), async (c)=> {
+ const { context, request } = c.req.valid("query");
+ const { model } = await initQuery(c);
+
+ const response = await streamText({ model, prompt: `${request}-${context}`, maxTokens: 224 });
+
+ return response.toTextStreamResponse();
+})
+
export default app;
diff --git a/apps/cf-ai-backend/src/prompts/prompt1.ts b/apps/cf-ai-backend/src/prompts/prompt1.ts
index d2ee988c..289495b6 100644
--- a/apps/cf-ai-backend/src/prompts/prompt1.ts
+++ b/apps/cf-ai-backend/src/prompts/prompt1.ts
@@ -18,13 +18,12 @@ export const template = ({ contexts, question }) => {
// Map over contexts to generate the context and score parts
const contextParts = contexts
.map(
- ({ context, score, normalisedScore }) => `
+ ({ context, normalisedScore }) => `
<context>
${context}
</context>
<context_score>
- score: ${score}
normalisedScore: ${normalisedScore}
</context_score>`,
)
diff --git a/apps/cf-ai-backend/src/types.ts b/apps/cf-ai-backend/src/types.ts
index bea4bf80..417d6320 100644
--- a/apps/cf-ai-backend/src/types.ts
+++ b/apps/cf-ai-backend/src/types.ts
@@ -2,7 +2,7 @@ import { z } from "zod";
export type Env = {
VECTORIZE_INDEX: VectorizeIndex;
- AI: Fetcher;
+ AI: Ai;
SECURITY_KEY: string;
OPENAI_API_KEY: string;
GOOGLE_AI_API_KEY: string;
@@ -43,7 +43,8 @@ export const vectorObj = z.object({
pageContent: z.string(),
title: z.string().optional(),
description: z.string().optional(),
- space: z.string().optional(),
+ spaces: z.array(z.string()).optional(),
url: z.string(),
user: z.string(),
+ type: z.string().optional().default("page"),
});
diff --git a/apps/cf-ai-backend/src/utils/chonker.ts b/apps/cf-ai-backend/src/utils/chonker.ts
index 39d4b458..c63020be 100644
--- a/apps/cf-ai-backend/src/utils/chonker.ts
+++ b/apps/cf-ai-backend/src/utils/chonker.ts
@@ -1,5 +1,8 @@
import nlp from "compromise";
+/**
+ * Split text into chunks of specified max size with some overlap for continuity.
+ */
export default function chunkText(
text: string,
maxChunkSize: number,
diff --git a/apps/cf-ai-backend/src/utils/seededRandom.ts b/apps/cf-ai-backend/src/utils/seededRandom.ts
index 36a1e4f9..9e315ee8 100644
--- a/apps/cf-ai-backend/src/utils/seededRandom.ts
+++ b/apps/cf-ai-backend/src/utils/seededRandom.ts
@@ -1,5 +1,9 @@
import { MersenneTwister19937, integer } from "random-js";
+/**
+ * Hashes a string to a 32-bit integer.
+ * @param {string} seed - The input string to hash.
+ */
function hashString(seed: string) {
let hash = 0;
for (let i = 0; i < seed.length; i++) {
@@ -10,6 +14,9 @@ function hashString(seed: string) {
return hash;
}
+/**
+ * returns a funtion that generates same sequence of random numbers for a given seed between 0 and 1.
+ */
export function seededRandom(seed: string) {
const seedHash = hashString(seed);
const engine = MersenneTwister19937.seed(seedHash);
diff --git a/apps/cf-ai-backend/tsconfig.json b/apps/cf-ai-backend/tsconfig.json
index 2b75d5a0..fcdf6914 100644
--- a/apps/cf-ai-backend/tsconfig.json
+++ b/apps/cf-ai-backend/tsconfig.json
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"lib": ["ES2020"],
- "types": ["@cloudflare/workers-types"]
+ "types": ["@cloudflare/workers-types"],
+ "downlevelIteration": true
}
}
diff --git a/apps/cf-ai-backend/wrangler.toml b/apps/cf-ai-backend/wrangler.toml
index db0ae945..fa883195 100644
--- a/apps/cf-ai-backend/wrangler.toml
+++ b/apps/cf-ai-backend/wrangler.toml
@@ -5,7 +5,7 @@ node_compat = true
[[vectorize]]
binding = "VECTORIZE_INDEX"
-index_name = "supermem-vector"
+index_name = "supermem-vector-dev"
[ai]
binding = "AI"
diff --git a/apps/web/app/(auth)/auth-buttons.tsx b/apps/web/app/(auth)/auth-buttons.tsx
index 0e99213e..5b0ad06e 100644
--- a/apps/web/app/(auth)/auth-buttons.tsx
+++ b/apps/web/app/(auth)/auth-buttons.tsx
@@ -2,7 +2,7 @@
import { Button } from "@repo/ui/shadcn/button";
import React from "react";
-import { signIn } from "../helpers/server/auth";
+import { signIn } from "../../server/auth";
function SignIn() {
return (
diff --git a/apps/web/app/(auth)/signin/page.tsx b/apps/web/app/(auth)/signin/page.tsx
index ba84a94a..d7bad8da 100644
--- a/apps/web/app/(auth)/signin/page.tsx
+++ b/apps/web/app/(auth)/signin/page.tsx
@@ -1,7 +1,7 @@
import Image from "next/image";
import Link from "next/link";
import Logo from "@/public/logo.svg";
-import { signIn } from "@/app/helpers/server/auth";
+import { signIn } from "@/server/auth";
import { Google } from "@repo/ui/components/icons";
export const runtime = "edge";
diff --git a/apps/web/app/(canvas)/canvas.tsx b/apps/web/app/(canvas)/canvas.tsx
index 9ec57d6d..498ab1eb 100644
--- a/apps/web/app/(canvas)/canvas.tsx
+++ b/apps/web/app/(canvas)/canvas.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Editor, Tldraw, setUserPreferences, TLStoreWithStatus } from "tldraw";
import { createAssetFromUrl } from "./lib/createAssetUrl";
import "tldraw/tldraw.css";
@@ -7,10 +7,53 @@ import { twitterCardUtil } from "./twitterCard";
import createEmbedsFromUrl from "./lib/createEmbeds";
import { loadRemoteSnapshot } from "./lib/loadSnap";
import { SaveStatus } from "./savesnap";
-import { getAssetUrls } from '@tldraw/assets/selfHosted'
-import { memo } from 'react';
+import { getAssetUrls } from "@tldraw/assets/selfHosted";
+import { memo } from "react";
+import DragContext from "./lib/context";
+import DropZone from "./dropComponent";
-export const Canvas = memo(()=>{
+export const Canvas = memo(() => {
+ const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false);
+ const Dragref = useRef<HTMLDivElement | null>(null)
+
+ const handleDragOver = (event: any) => {
+ event.preventDefault();
+ setIsDraggingOver(true);
+ console.log("entere")
+ };
+
+ const handleDragLeave = () => {
+ setIsDraggingOver(false);
+ console.log("leaver")
+ };
+
+ useEffect(() => {
+ const divElement = Dragref.current;
+ if (divElement) {
+ divElement.addEventListener('dragover', handleDragOver);
+ divElement.addEventListener('dragleave', handleDragLeave);
+ }
+ return () => {
+ if (divElement) {
+ divElement.removeEventListener('dragover', handleDragOver);
+ divElement.removeEventListener('dragleave', handleDragLeave);
+ }
+ };
+ }, []);
+
+ return (
+ <DragContext.Provider value={{ isDraggingOver, setIsDraggingOver }}>
+ <div
+ ref={Dragref}
+ className="w-full h-full"
+ >
+ <TldrawComponent />
+ </div>
+ </DragContext.Provider>
+ );
+});
+
+const TldrawComponent =memo(() => {
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
status: "loading",
});
@@ -38,18 +81,22 @@ export const Canvas = memo(()=>{
setUserPreferences({ id: "supermemory", isDarkMode: true });
- const assetUrls = getAssetUrls()
+ const assetUrls = getAssetUrls();
return (
- <Tldraw
- assetUrls={assetUrls}
- components={components}
- store={storeWithStatus}
- shapeUtils={[twitterCardUtil]}
- onMount={handleMount}
- >
- <div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
- <SaveStatus />
- </div>
- </Tldraw>
+ <div className="w-full h-full">
+ <Tldraw
+ className="relative"
+ assetUrls={assetUrls}
+ components={components}
+ store={storeWithStatus}
+ shapeUtils={[twitterCardUtil]}
+ onMount={handleMount}
+ >
+ <div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
+ <SaveStatus />
+ </div>
+ <DropZone />
+ </Tldraw>
+ </div>
);
})
diff --git a/apps/web/app/(canvas)/canvas/page.tsx b/apps/web/app/(canvas)/canvas/page.tsx
index 7abfa583..366a4481 100644
--- a/apps/web/app/(canvas)/canvas/page.tsx
+++ b/apps/web/app/(canvas)/canvas/page.tsx
@@ -18,7 +18,7 @@ function page() {
const [fullScreen, setFullScreen] = useState(false);
return (
- <div className={`h-screen w-full ${ !fullScreen && "px-4 py-6"} transition-all`}>
+ <div className={`h-screen w-full ${ !fullScreen ? "px-4 py-6": "bg-[#1F2428]"} transition-all`}>
<div>
<PanelGroup className={` ${fullScreen ? "w-[calc(100vw-2rem)]" : "w-screen"} transition-all`} direction="horizontal">
<Panel onExpand={()=> {setTimeout(()=> setFullScreen(false), 50)}} onCollapse={()=> {setTimeout(()=> setFullScreen(true), 50)}} defaultSize={30} collapsible={true} minSize={22}>
diff --git a/apps/web/app/(canvas)/dropComponent.tsx b/apps/web/app/(canvas)/dropComponent.tsx
new file mode 100644
index 00000000..03a32358
--- /dev/null
+++ b/apps/web/app/(canvas)/dropComponent.tsx
@@ -0,0 +1,76 @@
+import React, { useRef, useCallback, useEffect, useContext } from "react";
+import { useEditor } from "tldraw";
+import DragContext, { DragContextType } from "./lib/context";
+import { handleExternalDroppedContent } from "./lib/createEmbeds";
+
+const stripHtmlTags = (html: string): string => {
+ const div = document.createElement("div");
+ div.innerHTML = html;
+ return div.textContent || div.innerText || "";
+};
+
+const useDrag = (): DragContextType => {
+ const context = useContext(DragContext);
+ if (!context) {
+ throw new Error('useCounter must be used within a CounterProvider');
+ }
+ return context;
+};
+
+
+function DropZone() {
+ const dropRef = useRef<HTMLDivElement | null>(null);
+ const {isDraggingOver, setIsDraggingOver} = useDrag();
+
+ const editor = useEditor();
+
+ const handleDrop = useCallback((event: React.DragEvent<HTMLDivElement>) => {
+ event.preventDefault();
+ setIsDraggingOver(false);
+ const dt = event.dataTransfer;
+ const items = dt.items;
+
+ for (let i = 0; i < items.length; i++) {
+ if (items[i]!.kind === "file" && items[i]!.type.startsWith("image/")) {
+ const file = items[i]!.getAsFile();
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ if (e.target) {
+ // setDroppedImage(e.target.result as string);
+ }
+ };
+ reader.readAsDataURL(file);
+ }
+ } else if (items[i]!.kind === "string") {
+ items[i]!.getAsString((data) => {
+ const cleanText = stripHtmlTags(data);
+ handleExternalDroppedContent({editor,text:cleanText})
+ });
+ }
+ }
+ }, []);
+
+ useEffect(() => {
+ const divElement = dropRef.current;
+ if (divElement) {
+ // @ts-ignore
+ divElement.addEventListener("drop", handleDrop);
+ }
+ return () => {
+ if (divElement) {
+ // @ts-ignore
+ divElement.removeEventListener("drop", handleDrop);
+ }
+ };
+ }, []);
+
+ return (
+ <div
+ className={`h-full w-full absolute top-0 left-0 z-[100000] pointer-events-none ${isDraggingOver && "bg-[#2C3439] pointer-events-auto"}`}
+ ref={dropRef}
+ ></div>
+ );
+}
+
+export default DropZone;
diff --git a/apps/web/app/(canvas)/enabledComp.tsx b/apps/web/app/(canvas)/enabledComp.tsx
index 5dbe6ee7..85811b82 100644
--- a/apps/web/app/(canvas)/enabledComp.tsx
+++ b/apps/web/app/(canvas)/enabledComp.tsx
@@ -7,12 +7,12 @@ export const components: Partial<TLUiComponents> = {
TopPanel: null,
DebugPanel: null,
DebugMenu: null,
+ PageMenu: null,
// Minimap: null,
// ContextMenu: null,
// HelpMenu: null,
// ZoomMenu: null,
// StylePanel: null,
- // PageMenu: null,
// NavigationPanel: null,
// Toolbar: null,
// KeyboardShortcutsDialog: null,
diff --git a/apps/web/app/(canvas)/lib/context.tsx b/apps/web/app/(canvas)/lib/context.tsx
new file mode 100644
index 00000000..36a106cf
--- /dev/null
+++ b/apps/web/app/(canvas)/lib/context.tsx
@@ -0,0 +1,11 @@
+import { createContext } from 'react';
+
+export interface DragContextType {
+ isDraggingOver: boolean;
+ setIsDraggingOver: React.Dispatch<React.SetStateAction<boolean>>;
+}
+
+
+const DragContext = createContext<DragContextType | undefined>(undefined);
+
+export default DragContext; \ No newline at end of file
diff --git a/apps/web/app/(canvas)/lib/createEmbeds.ts b/apps/web/app/(canvas)/lib/createEmbeds.ts
index 322e697e..0db3c71b 100644
--- a/apps/web/app/(canvas)/lib/createEmbeds.ts
+++ b/apps/web/app/(canvas)/lib/createEmbeds.ts
@@ -2,8 +2,8 @@ import { AssetRecordType, Editor, TLAsset, TLAssetId, TLBookmarkShape, TLExterna
export default async function createEmbedsFromUrl({url, point, sources, editor}: {
url: string
- point: VecLike | undefined
- sources: TLExternalContentSource[] | undefined
+ point?: VecLike | undefined
+ sources?: TLExternalContentSource[] | undefined
editor: Editor
}){
@@ -50,10 +50,18 @@ export default async function createEmbedsFromUrl({url, point, sources, editor}:
type: "url",
url,
});
- const fetchWebsite = await (await fetch(`https://unfurl-bookmark.pruthvirajthinks.workers.dev/?url=${url}`)).json()
- if (fetchWebsite.title) bookmarkAsset.props.title = fetchWebsite.title;
- if (fetchWebsite.image) bookmarkAsset.props.image = fetchWebsite.image;
- if (fetchWebsite.description) bookmarkAsset.props.description = fetchWebsite.description;
+ const fetchWebsite: {
+ title?: string;
+ image?: string;
+ description?: string;
+ } = await (await fetch(`/api/unfirlsite?website=${url}`, {
+ method: "POST"
+ })).json()
+ if (bookmarkAsset){
+ if (fetchWebsite.title) bookmarkAsset.props.title = fetchWebsite.title;
+ if (fetchWebsite.image) bookmarkAsset.props.image = fetchWebsite.image;
+ if (fetchWebsite.description) bookmarkAsset.props.description = fetchWebsite.description;
+ }
if (!bookmarkAsset) throw Error("Could not create an asset");
asset = bookmarkAsset;
} catch (e) {
@@ -79,6 +87,38 @@ export default async function createEmbedsFromUrl({url, point, sources, editor}:
});
}
+function isURL(str: string) {
+ try {
+ new URL(str);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+
+export function handleExternalDroppedContent({text, editor}: {text:string, editor: Editor}){
+ const position = editor.inputs.shiftKey
+ ? editor.inputs.currentPagePoint
+ : editor.getViewportPageBounds().center;
+
+ if (isURL(text)){
+ createEmbedsFromUrl({editor, url: text})
+ } else{
+ editor.createShape({
+ type: "text",
+ x: position.x - 75,
+ y: position.y - 75,
+ props: {
+ text: text,
+ size: "s",
+ textAlign: "start",
+ },
+ });
+
+ }
+}
+
function centerSelectionAroundPoint(editor: Editor, position: VecLike) {
// Re-position shapes so that the center of the group is at the provided point
const viewportPageBounds = editor.getViewportPageBounds()
diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx
index bb6a0be1..32fd1fce 100644
--- a/apps/web/app/(dash)/chat/chatWindow.tsx
+++ b/apps/web/app/(dash)/chat/chatWindow.tsx
@@ -1,7 +1,7 @@
"use client";
import { AnimatePresence } from "framer-motion";
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
import QueryInput from "../home/queryinput";
import { cn } from "@repo/ui/lib/utils";
import { motion } from "framer-motion";
@@ -19,7 +19,10 @@ import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import rehypeHighlight from "rehype-highlight";
import { code, p } from "./markdownRenderHelpers";
-import { codeLanguageSubset } from "@/app/helpers/constants";
+import { codeLanguageSubset } from "@/lib/constants";
+import { z } from "zod";
+import { toast } from "sonner";
+import Link from "next/link";
function ChatWindow({
q,
@@ -33,19 +36,85 @@ function ChatWindow({
{
question: q,
answer: {
- parts: [
- // {
- // text: `It seems like there might be a typo in your question. Could you please clarify or provide more context? If you meant "interesting," please let me know what specific information or topic you find interesting, and I can help you with that.`,
- // },
- ],
+ parts: [],
sources: [],
},
},
]);
+ const [isAutoScroll, setIsAutoScroll] = useState(true);
+
+ const removeJustificationFromText = (text: string) => {
+ // remove everything after the first "<justification>" word
+ const justificationLine = text.indexOf("<justification>");
+ if (justificationLine !== -1) {
+ // Add that justification to the last chat message
+ const lastChatMessage = chatHistory[chatHistory.length - 1];
+ if (lastChatMessage) {
+ lastChatMessage.answer.justification = text.slice(justificationLine);
+ }
+ return text.slice(0, justificationLine);
+ }
+ return text;
+ };
const router = useRouter();
const getAnswer = async (query: string, spaces: string[]) => {
+ const sourcesFetch = await fetch(
+ `/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true`,
+ {
+ method: "POST",
+ body: JSON.stringify({ chatHistory }),
+ },
+ );
+
+ // TODO: handle this properly
+ const sources = await sourcesFetch.json();
+
+ const sourcesZod = z.object({
+ ids: z.array(z.string()),
+ metadata: z.array(z.any()),
+ });
+
+ const sourcesParsed = sourcesZod.safeParse(sources);
+
+ if (!sourcesParsed.success) {
+ console.log(sources);
+ console.error(sourcesParsed.error);
+ toast.error("Something went wrong while getting the sources");
+ return;
+ }
+
+ setChatHistory((prevChatHistory) => {
+ window.scrollTo({
+ top: document.documentElement.scrollHeight,
+ behavior: "smooth",
+ });
+ const newChatHistory = [...prevChatHistory];
+ const lastAnswer = newChatHistory[newChatHistory.length - 1];
+ if (!lastAnswer) return prevChatHistory;
+ const filteredSourceUrls = new Set(
+ sourcesParsed.data.metadata.map((source) => source.url),
+ );
+ const uniqueSources = sourcesParsed.data.metadata.filter((source) => {
+ if (filteredSourceUrls.has(source.url)) {
+ filteredSourceUrls.delete(source.url);
+ return true;
+ }
+ return false;
+ });
+ lastAnswer.answer.sources = uniqueSources.map((source) => ({
+ title: source.title ?? "Untitled",
+ type: source.type ?? "page",
+ source: source.url ?? "https://supermemory.ai",
+ content: source.description ?? "No content available",
+ numChunks: sourcesParsed.data.metadata.filter(
+ (f) => f.url === source.url,
+ ).length,
+ }));
+ return newChatHistory;
+ });
+
const resp = await fetch(`/api/chat?q=${query}&spaces=${spaces}`, {
method: "POST",
body: JSON.stringify({ chatHistory }),
@@ -53,7 +122,6 @@ function ChatWindow({
const reader = resp.body?.getReader();
let done = false;
- let result = "";
while (!done && reader) {
const { value, done: d } = await reader.read();
done = d;
@@ -62,23 +130,28 @@ function ChatWindow({
const newChatHistory = [...prevChatHistory];
const lastAnswer = newChatHistory[newChatHistory.length - 1];
if (!lastAnswer) return prevChatHistory;
- lastAnswer.answer.parts.push({ text: new TextDecoder().decode(value) });
+ const txt = new TextDecoder().decode(value);
+
+ if (isAutoScroll) {
+ window.scrollTo({
+ top: document.documentElement.scrollHeight,
+ behavior: "smooth",
+ });
+ }
+
+ lastAnswer.answer.parts.push({ text: txt });
return newChatHistory;
});
}
-
- console.log(result);
};
useEffect(() => {
if (q.trim().length > 0) {
+ setLayout("chat");
getAnswer(
q,
spaces.map((s) => s.id),
);
- setTimeout(() => {
- setLayout("chat");
- }, 300);
} else {
router.push("/home");
}
@@ -94,18 +167,23 @@ function ChatWindow({
className="max-w-3xl h-full justify-center items-center flex mx-auto w-full flex-col"
>
<div className="w-full h-96">
- <QueryInput initialQuery={q} initialSpaces={[]} disabled />
+ <QueryInput
+ handleSubmit={() => {}}
+ initialQuery={q}
+ initialSpaces={[]}
+ disabled
+ />
</div>
</motion.div>
) : (
<div
- className="max-w-3xl flex mx-auto w-full flex-col mt-24"
+ className="max-w-3xl relative flex mx-auto w-full flex-col mt-24 pb-32"
key="chat"
>
{chatHistory.map((chat, idx) => (
<div
key={idx}
- className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b" : ""}`}
+ className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b border-b-gray-400" : ""}`}
>
<h2
className={cn(
@@ -151,15 +229,25 @@ function ChatWindow({
</>
))}
{chat.answer.sources.map((source, idx) => (
- <div
+ <Link
+ href={source.source}
key={idx}
className="rounded-xl bg-secondary p-4 flex flex-col gap-2 min-w-72"
>
- <div className="text-foreground-menu">
- {source.type}
+ <div className="flex justify-between text-foreground-menu text-sm">
+ <span>{source.type}</span>
+
+ {source.numChunks > 1 && (
+ <span>{source.numChunks} chunks</span>
+ )}
+ </div>
+ <div className="text-base">{source.title}</div>
+ <div className="text-xs">
+ {source.content.length > 100
+ ? source.content.slice(0, 100) + "..."
+ : source.content}
</div>
- <div>{source.title}</div>
- </div>
+ </Link>
))}
</AccordionContent>
</AccordionItem>
@@ -197,14 +285,67 @@ function ChatWindow({
}}
className="flex flex-col gap-2"
>
- {chat.answer.parts.map((part) => part.text).join("")}
+ {removeJustificationFromText(
+ chat.answer.parts.map((part) => part.text).join(""),
+ )}
</Markdown>
</div>
</div>
-
+ {/* Justification */}
+ {chat.answer.justification &&
+ chat.answer.justification.length && (
+ <div
+ className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
+ >
+ <Accordion defaultValue={""} type="single" collapsible>
+ <AccordionItem value="justification">
+ <AccordionTrigger className="text-foreground-menu">
+ Justification
+ </AccordionTrigger>
+ <AccordionContent
+ className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
+ defaultChecked
+ >
+ {chat.answer.justification.length > 0
+ ? chat.answer.justification
+ .replaceAll("<justification>", "")
+ .replaceAll("</justification>", "")
+ : "No justification provided."}
+ </AccordionContent>
+ </AccordionItem>
+ </Accordion>
+ </div>
+ )}
</div>
</div>
))}
+
+ <div className="fixed bottom-0 w-full max-w-3xl pb-4">
+ <QueryInput
+ mini
+ className="w-full shadow-md"
+ initialQuery={""}
+ initialSpaces={[]}
+ handleSubmit={async (q, spaces) => {
+ setChatHistory((prevChatHistory) => {
+ return [
+ ...prevChatHistory,
+ {
+ question: q,
+ answer: {
+ parts: [],
+ sources: [],
+ },
+ },
+ ];
+ });
+ await getAnswer(
+ q,
+ spaces.map((s) => `${s.id}`),
+ );
+ }}
+ />
+ </div>
</div>
)}
</AnimatePresence>
diff --git a/apps/web/app/(dash)/dynamicisland.tsx b/apps/web/app/(dash)/dynamicisland.tsx
index c08f883a..98fafc7a 100644
--- a/apps/web/app/(dash)/dynamicisland.tsx
+++ b/apps/web/app/(dash)/dynamicisland.tsx
@@ -4,12 +4,12 @@ import { AddIcon } from "@repo/ui/icons";
import Image from "next/image";
import { AnimatePresence, useMotionValueEvent, useScroll } from "framer-motion";
-import { useEffect, useRef, useState } from "react";
+import { useActionState, useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";
import { Label } from "@repo/ui/shadcn/label";
import { Input } from "@repo/ui/shadcn/input";
import { Textarea } from "@repo/ui/shadcn/textarea";
-import { createSpace } from "../actions/doers";
+import { createMemory, createSpace } from "../actions/doers";
import {
Select,
SelectContent,
@@ -20,6 +20,7 @@ import {
import { Space } from "../actions/types";
import { getSpaces } from "../actions/fetchers";
import { toast } from "sonner";
+import { useFormStatus } from "react-dom";
export function DynamicIsland() {
const { scrollYProgress } = useScroll();
@@ -253,13 +254,39 @@ function PageForm({
cancelfn: () => void;
spaces: Space[];
}) {
+ const [loading, setLoading] = useState(false);
+
+ const { pending } = useFormStatus();
return (
- <div className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
+ <form
+ action={async (e: FormData) => {
+ const content = e.get("content")?.toString();
+ const space = e.get("space")?.toString();
+ if (!content) {
+ toast.error("Content is required");
+ return;
+ }
+ setLoading(true);
+ const cont = await createMemory({
+ content: content,
+ spaces: space ? [space] : undefined,
+ });
+
+ console.log(cont);
+ setLoading(false);
+ if (cont.success) {
+ toast.success("Memory created");
+ } else {
+ toast.error("Memory creation failed");
+ }
+ }}
+ className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"
+ >
<div>
<Label className="text-[#858B92]" htmlFor="space">
Space
</Label>
- <Select>
+ <Select name="space">
<SelectTrigger>
<SelectValue placeholder="Space" />
</SelectTrigger>
@@ -272,24 +299,28 @@ function PageForm({
</SelectContent>
</Select>
</div>
+ <div key={`${loading}-${pending}`}>
+ {loading ? <div>Loading...</div> : "not loading"}
+ </div>
<div>
<Label className="text-[#858B92]" htmlFor="name">
Page Url
</Label>
<Input
className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0"
- id="name"
+ id="input"
+ name="content"
/>
</div>
<div className="flex justify-end">
- <div
- onClick={cancelfn}
+ <button
+ type="submit"
className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer"
>
- cancel
- </div>
+ Submit
+ </button>
</div>
- </div>
+ </form>
);
}
diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx
index c539673d..bdf6a61e 100644
--- a/apps/web/app/(dash)/home/page.tsx
+++ b/apps/web/app/(dash)/home/page.tsx
@@ -1,11 +1,12 @@
-import React from "react";
-import Menu from "../menu";
-import Header from "../header";
+"use client";
+
+import React, { useEffect, useState } from "react";
import QueryInput from "./queryinput";
-import { homeSearchParamsCache } from "@/app/helpers/lib/searchParams";
+import { homeSearchParamsCache } from "@/lib/searchParams";
import { getSpaces } from "@/app/actions/fetchers";
+import { useRouter } from "next/navigation";
-async function Page({
+function Page({
searchParams,
}: {
searchParams: Record<string, string | string[] | undefined>;
@@ -13,12 +14,18 @@ async function Page({
// TODO: use this to show a welcome page/modal
const { firstTime } = homeSearchParamsCache.parse(searchParams);
- let spaces = await getSpaces();
+ const [spaces, setSpaces] = useState<{ id: number; name: string }[]>([]);
+
+ useEffect(() => {
+ getSpaces().then((res) => {
+ if (res.success && res.data) {
+ setSpaces(res.data);
+ }
+ // TODO: HANDLE ERROR
+ });
+ }, []);
- if (!spaces.success) {
- // TODO: handle this error properly.
- spaces.data = [];
- }
+ const { push } = useRouter();
return (
<div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col">
@@ -26,7 +33,17 @@ async function Page({
{/* <div className="">hi {firstTime ? 'first time' : ''}</div> */}
<div className="w-full pb-20">
- <QueryInput initialSpaces={spaces.data} />
+ <QueryInput
+ handleSubmit={(q, spaces) => {
+ const newQ =
+ "/chat?q=" +
+ encodeURI(q) +
+ (spaces ? "&spaces=" + JSON.stringify(spaces) : "");
+
+ push(newQ);
+ }}
+ initialSpaces={spaces}
+ />
</div>
</div>
);
diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx
index fbd537e3..4fadfb6f 100644
--- a/apps/web/app/(dash)/home/queryinput.tsx
+++ b/apps/web/app/(dash)/home/queryinput.tsx
@@ -12,6 +12,9 @@ function QueryInput({
initialQuery = "",
initialSpaces = [],
disabled = false,
+ className,
+ mini = false,
+ handleSubmit,
}: {
initialQuery?: string;
initialSpaces?: {
@@ -19,32 +22,14 @@ function QueryInput({
name: string;
}[];
disabled?: boolean;
+ className?: string;
+ mini?: boolean;
+ handleSubmit: (q: string, spaces: { id: number; name: string }[]) => void;
}) {
const [q, setQ] = useState(initialQuery);
const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
- const { push } = useRouter();
-
- const parseQ = () => {
- // preparedSpaces is list of spaces selected by user, with id and name
- const preparedSpaces = initialSpaces
- .filter((x) => selectedSpaces.includes(x.id))
- .map((x) => {
- return {
- id: x.id,
- name: x.name,
- };
- });
-
- const newQ =
- "/chat?q=" +
- encodeURI(q) +
- (selectedSpaces ? "&spaces=" + JSON.stringify(preparedSpaces) : "");
-
- return newQ;
- };
-
const options = useMemo(
() =>
initialSpaces.map((x) => ({
@@ -54,21 +39,43 @@ function QueryInput({
[initialSpaces],
);
+ const preparedSpaces = useMemo(
+ () =>
+ initialSpaces
+ .filter((x) => selectedSpaces.includes(x.id))
+ .map((x) => {
+ return {
+ id: x.id,
+ name: x.name,
+ };
+ }),
+ [selectedSpaces, initialSpaces],
+ );
+
return (
- <div>
- <div className="bg-secondary rounded-t-[24px]">
+ <div className={className}>
+ <div
+ className={`bg-secondary ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
+ >
{/* input and action button */}
- <form action={async () => push(parseQ())} className="flex gap-4 p-3">
+ <form
+ action={async () => {
+ handleSubmit(q, preparedSpaces);
+ setQ("");
+ }}
+ className="flex gap-4 p-3"
+ >
<textarea
name="q"
cols={30}
- rows={4}
+ rows={mini ? 2 : 4}
className="bg-transparent pt-2.5 text-base placeholder:text-[#5D6165] text-[#9DA0A4] focus:text-white duration-200 tracking-[3%] outline-none resize-none w-full p-4"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
- if (!e.shiftKey) push(parseQ());
+ handleSubmit(q, preparedSpaces);
+ setQ("");
}
}}
onChange={(e) => setQ(e.target.value)}
@@ -85,24 +92,29 @@ function QueryInput({
<Image src={ArrowRightIcon} alt="Right arrow icon" />
</button>
</form>
-
- <Divider />
</div>
{/* selected sources */}
- <div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-[24px]">
- <MultipleSelector
- key={options.length}
- disabled={disabled}
- defaultOptions={options}
- onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))}
- placeholder="Focus on specific spaces..."
- emptyIndicator={
- <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
- no results found.
- </p>
- }
- />
- </div>
+ {!mini && (
+ <>
+ <Divider />
+ <div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-3xl">
+ <MultipleSelector
+ key={options.length}
+ disabled={disabled}
+ defaultOptions={options}
+ onChange={(e) =>
+ setSelectedSpaces(e.map((x) => parseInt(x.value)))
+ }
+ placeholder="Focus on specific spaces..."
+ emptyIndicator={
+ <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
+ no results found.
+ </p>
+ }
+ />
+ </div>
+ </>
+ )}
</div>
);
}
diff --git a/apps/web/app/(dash)/layout.tsx b/apps/web/app/(dash)/layout.tsx
index 4c787c9c..4e1f6989 100644
--- a/apps/web/app/(dash)/layout.tsx
+++ b/apps/web/app/(dash)/layout.tsx
@@ -1,7 +1,7 @@
import Header from "./header";
import Menu from "./menu";
import { redirect } from "next/navigation";
-import { auth } from "../helpers/server/auth";
+import { auth } from "../../server/auth";
import { Toaster } from "@repo/ui/shadcn/sonner";
async function Layout({ children }: { children: React.ReactNode }) {
diff --git a/apps/web/app/(dash)/memories/page.tsx b/apps/web/app/(dash)/memories/page.tsx
index bc2fcd53..ff746d1d 100644
--- a/apps/web/app/(dash)/memories/page.tsx
+++ b/apps/web/app/(dash)/memories/page.tsx
@@ -1,14 +1,31 @@
"use client";
+import { getAllUserMemoriesAndSpaces } from "@/app/actions/fetchers";
+import { Space } from "@/app/actions/types";
+import { Content } from "@/server/db/schema";
import { NextIcon, SearchIcon, UrlIcon } from "@repo/ui/icons";
import Image from "next/image";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
-function page() {
- const [filter, setFilter] = useState("All")
- const setFilterfn = (i:string) => setFilter(i)
+function Page() {
+ const [filter, setFilter] = useState("All");
+ const setFilterfn = (i: string) => setFilter(i);
+
+ const [search, setSearch] = useState("");
+
+ const [memoriesAndSpaces, setMemoriesAndSpaces] = useState<{
+ memories: Content[];
+ spaces: Space[];
+ }>({ memories: [], spaces: [] });
+
+ useEffect(() => {
+ (async () => {
+ const { success, data } = await getAllUserMemoriesAndSpaces();
+ if (!success ?? !data) return;
+ setMemoriesAndSpaces({ memories: data.memories, spaces: data.spaces });
+ })();
+ }, []);
- const [search, setSearch] = useState("")
return (
<div className="max-w-3xl min-w-3xl py-36 h-full flex mx-auto w-full flex-col gap-12">
<h2 className="text-white w-full font-medium text-2xl text-left">
@@ -16,41 +33,50 @@ function page() {
</h2>
<div className="flex flex-col gap-4">
- <div className="w-full relative">
- <input
- type="text"
- className=" w-full py-3 rounded-md text-lg pl-8 bg-[#1F2428] outline-none"
- placeholder="search here..."
- />
- <Image className="absolute top-1/2 -translate-y-1/2 left-2" src={SearchIcon} alt="Search icon" />
- </div>
-
- <Filters filter={filter} setFilter={setFilterfn} />
+ <div className="w-full relative">
+ <input
+ type="text"
+ className=" w-full py-3 rounded-md text-lg pl-8 bg-[#1F2428] outline-none"
+ placeholder="search here..."
+ />
+ <Image
+ className="absolute top-1/2 -translate-y-1/2 left-2"
+ src={SearchIcon}
+ alt="Search icon"
+ />
+ </div>
+ <Filters filter={filter} setFilter={setFilterfn} />
</div>
<div>
<div className="text-[#B3BCC5]">Spaces</div>
- <TabComponent title="AI Technologies" description="Resources 12" />
- <TabComponent title="Python Tricks" description="Resources 120" />
- <TabComponent title="JavaScript Hacks" description="Resources 14" />
+ {memoriesAndSpaces.spaces.map((space) => (
+ <TabComponent title={space.name} description={space.id.toString()} />
+ ))}
</div>
<div>
<div className="text-[#B3BCC5]">Pages</div>
- <LinkComponent title="How to make a custom AI model?" url="https://google.com" />
- <LinkComponent title="GPT 5 Release Date" url="https://wth.com" />
- <LinkComponent title="Why @sama never use uppercase" url="https://tom.com" />
+ {memoriesAndSpaces.memories.map((memory) => (
+ <LinkComponent title={memory.title ?? "No title"} url={memory.url} />
+ ))}
</div>
</div>
);
}
-function TabComponent({title, description}: {title:string, description:string}){
+function TabComponent({
+ title,
+ description,
+}: {
+ title: string;
+ description: string;
+}) {
return (
<div className="flex items-center my-6">
<div>
<div className="h-12 w-12 bg-[#1F2428] flex justify-center items-center rounded-md">
- {title.slice(0,2).toUpperCase()}
+ {title.slice(0, 2).toUpperCase()}
</div>
</div>
<div className="grow px-4">
@@ -58,37 +84,50 @@ function TabComponent({title, description}: {title:string, description:string}){
<div>{description}</div>
</div>
<div>
- <Image src={NextIcon} alt="Search icon" />
+ <Image src={NextIcon} alt="Search icon" />
</div>
</div>
- )
+ );
}
-function LinkComponent({title, url}: {title:string, url:string}){
+function LinkComponent({ title, url }: { title: string; url: string }) {
return (
<div className="flex items-center my-6">
- <div>
- <div className="h-12 w-12 bg-[#1F2428] flex justify-center items-center rounded-md">
- <Image src={UrlIcon} alt="Url icon" />
+ <div>
+ <div className="h-12 w-12 bg-[#1F2428] flex justify-center items-center rounded-md">
+ <Image src={UrlIcon} alt="Url icon" />
+ </div>
+ </div>
+ <div className="grow px-4">
+ <div className="text-lg text-[#fff]">{title}</div>
+ <div>{url}</div>
</div>
</div>
- <div className="grow px-4">
- <div className="text-lg text-[#fff]">{title}</div>
- <div>{url}</div>
- </div>
- </div>
- )
+ );
}
-const FilterMethods = ["All", "Spaces", "Pages", "Notes"]
-function Filters({setFilter, filter}:{setFilter: (i:string)=> void, filter: string}){
+const FilterMethods = ["All", "Spaces", "Pages", "Notes"];
+function Filters({
+ setFilter,
+ filter,
+}: {
+ setFilter: (i: string) => void;
+ filter: string;
+}) {
return (
<div className="flex gap-4">
- {FilterMethods.map((i)=> {
- return <div onClick={()=> setFilter(i)} className={`transition px-6 py-2 rounded-xl ${i === filter ? "bg-[#21303D] text-[#369DFD]" : "text-[#B3BCC5] bg-[#1F2428] hover:bg-[#1f262d] hover:text-[#76a3cc]"}`}>{i}</div>
+ {FilterMethods.map((i) => {
+ return (
+ <div
+ onClick={() => setFilter(i)}
+ className={`transition px-6 py-2 rounded-xl ${i === filter ? "bg-[#21303D] text-[#369DFD]" : "text-[#B3BCC5] bg-[#1F2428] hover:bg-[#1f262d] hover:text-[#76a3cc]"}`}
+ >
+ {i}
+ </div>
+ );
})}
</div>
- )
+ );
}
-export default page;
+export default Page;
diff --git a/apps/web/app/(editor)/components/aigenerate.tsx b/apps/web/app/(editor)/components/aigenerate.tsx
index b1c4ccd4..de9b2a3f 100644
--- a/apps/web/app/(editor)/components/aigenerate.tsx
+++ b/apps/web/app/(editor)/components/aigenerate.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
import Magic from "./ui/magic";
import CrazySpinner from "./ui/crazy-spinner";
import Asksvg from "./ui/asksvg";
@@ -8,14 +8,11 @@ import Autocompletesvg from "./ui/autocompletesvg";
import { motion, AnimatePresence } from "framer-motion";
import type { Editor } from "@tiptap/core";
import { useEditor } from "novel";
-
+import { NodeSelection } from "prosemirror-state";
function Aigenerate() {
const [visible, setVisible] = useState(false);
const [generating, setGenerating] = useState(false);
-
- // generating -> can be converted to false, so we need to make sure the generation gets cancelled
- // visible
const { editor } = useEditor();
const setGeneratingfn = (v: boolean) => setGenerating(v);
@@ -58,8 +55,9 @@ function Aigenerate() {
}}
className="absolute z-50 top-0"
>
- <ToolBar setGeneratingfn={setGeneratingfn} editor={editor} />
- <div className="h-8 w-18rem bg-blue-600 blur-[16rem]" />
+ {/* TODO: handle Editor not initalised, maybe with a loading state. */}
+ <ToolBar setGeneratingfn={setGeneratingfn} editor={editor} />
+ <div className="h-8 w-18rem bg-blue-600 blur-[16rem]" />
</motion.div>
</div>
);
@@ -68,10 +66,22 @@ function Aigenerate() {
export default Aigenerate;
const options = [
- <><Translatesvg />Translate</>,
- <><Rewritesvg />Change Tone</>,
- <><Asksvg />Ask Gemini</>,
- <><Autocompletesvg />Auto Complete</>
+ <>
+ <Translatesvg />
+ Translate
+ </>,
+ <>
+ <Rewritesvg />
+ Change Tone
+ </>,
+ <>
+ <Asksvg />
+ Ask Gemini
+ </>,
+ <>
+ <Autocompletesvg />
+ Auto Complete
+ </>,
];
function ToolBar({
@@ -116,7 +126,7 @@ function ToolBar({
)}
</AnimatePresence>
<div className="select-none flex items-center whitespace-nowrap gap-3 relative z-[60] pointer-events-none">
- {item}
+ {item}
</div>
</div>
))}
@@ -135,43 +145,42 @@ async function AigenerateContent({
}) {
setGeneratingfn(true);
- const {from, to} = editor.view.state.selection;
- const content = editor.view.state.selection.content();
- content.content.forEach((v, i)=> {
- v.forEach((v, i)=> {
- console.log(v.text)
- })
- })
-
- const transaction = editor.state.tr
- transaction.replaceRange(from, to, content)
-
- editor.view.dispatch(transaction)
-
- // console.log(content)
- // content.map((v, i)=> console.log(v.content))
-
- // const fragment = Fragment.fromArray(content);
-
- // console.log(fragment)
-
- // editor.view.state.selection.content().content.append(content)
+ const { from, to } = editor.view.state.selection;
+
+ const slice = editor.state.selection.content();
+ const text = editor.storage.markdown.serializer.serialize(slice.content);
+
+ const request = [
+ "Translate to hindi written in english, do not write anything else",
+ "change tone, improve the way be more formal",
+ "ask, answer the question",
+ "continue this, minimum 80 characters, do not repeat just continue don't use ... to denote start",
+ ];
+
+ const resp = await fetch("/api/editorai", {
+ method: "POST",
+ body: JSON.stringify({
+ context: text,
+ request: request[idx],
+ }),
+ });
+
+ const reader = resp.body?.getReader();
+ let done = false;
+ let position = to;
+ while (!done && reader) {
+ const { value, done: d } = await reader.read();
+ done = d;
+
+ const decoded = new TextDecoder().decode(value);
+ console.log(decoded);
+ editor
+ .chain()
+ .focus()
+ .insertContentAt(position + 1, decoded)
+ .run();
+ position += decoded.length;
+ }
setGeneratingfn(false);
-
-
-
- // const genAI = new GoogleGenerativeAI("AIzaSyDGwJCP9SH5gryyvh65LJ6xTZ0SOdNvzyY");
- // const model = genAI.getGenerativeModel({ model: "gemini-pro"});
-
- // const result = (await model.generateContent(`${ty}, ${query}`)).response.text();
-
- // .insertContentAt(
- // {
- // from: from,
- // to: to,
- // },
- // result,
- // )
- // .run();
}
diff --git a/apps/web/app/(editor)/editor.tsx b/apps/web/app/(editor)/editor.tsx
index 5b4a60ce..f7f9a098 100644
--- a/apps/web/app/(editor)/editor.tsx
+++ b/apps/web/app/(editor)/editor.tsx
@@ -15,19 +15,20 @@ import Topbar from "./components/topbar";
const Editor = () => {
const [initialContent, setInitialContent] = useState<null | JSONContent>(
- null
+ null,
);
const [saveStatus, setSaveStatus] = useState("Saved");
const [charsCount, setCharsCount] = useState();
const [visible, setVisible] = useState(true);
useEffect(() => {
+ if (typeof window === "undefined") return;
const content = window.localStorage.getItem("novel-content");
if (content) setInitialContent(JSON.parse(content));
else setInitialContent(defaultEditorContent);
}, []);
- if (!initialContent) return null;
+ if (!initialContent) return <>Loading...</>;
return (
<div className="relative w-full max-w-screen-xl">
diff --git a/apps/web/app/(landing)/Cta.tsx b/apps/web/app/(landing)/Cta.tsx
index be99bf99..f0f471c2 100644
--- a/apps/web/app/(landing)/Cta.tsx
+++ b/apps/web/app/(landing)/Cta.tsx
@@ -24,7 +24,7 @@ function Cta() {
height={1405}
priority
draggable="false"
- className="absolute z-[-2] hidden select-none rounded-3xl bg-black md:block lg:w-[80%]"
+ className="absolute z-[-2] hidden select-none rounded-3xl bg-background md:block lg:w-[80%]"
/>
<h1 className="z-20 mt-4 text-center text-5xl font-medium tracking-tight text-white">
Your bookmarks are collecting dust.
diff --git a/apps/web/app/(landing)/page.tsx b/apps/web/app/(landing)/page.tsx
index 09f94d92..5f8b28b4 100644
--- a/apps/web/app/(landing)/page.tsx
+++ b/apps/web/app/(landing)/page.tsx
@@ -5,7 +5,7 @@ import Cta from "./Cta";
import { Toaster } from "@repo/ui/shadcn/toaster";
import Features from "./Features";
import Footer from "./footer";
-import { auth } from "../helpers/server/auth";
+import { auth } from "../../server/auth";
import { redirect } from "next/navigation";
export const runtime = "edge";
diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts
index c8a1f3b4..6c7180d9 100644
--- a/apps/web/app/actions/doers.ts
+++ b/apps/web/app/actions/doers.ts
@@ -1,10 +1,15 @@
"use server";
import { revalidatePath } from "next/cache";
-import { db } from "../helpers/server/db";
-import { space } from "../helpers/server/db/schema";
+import { db } from "../../server/db";
+import { contentToSpace, space, storedContent } from "../../server/db/schema";
import { ServerActionReturnType } from "./types";
-import { auth } from "../helpers/server/auth";
+import { auth } from "../../server/auth";
+import { Tweet } from "react-tweet/api";
+import { getMetaData } from "@/lib/get-metadata";
+import { and, eq, inArray, sql } from "drizzle-orm";
+import { LIMITS } from "@/lib/constants";
+import { z } from "zod";
export const createSpace = async (
input: string | FormData,
@@ -41,3 +46,223 @@ export const createSpace = async (
}
}
};
+
+const typeDecider = (content: string) => {
+ // if the content is a URL, then it's a page. if its a URL with https://x.com/user/status/123, then it's a tweet. else, it's a note.
+ // do strict checking with regex
+ if (content.match(/https?:\/\/[\w\.]+\/[\w]+\/[\w]+\/[\d]+/)) {
+ return "tweet";
+ } else if (content.match(/https?:\/\/[\w\.]+/)) {
+ return "page";
+ } else {
+ return "note";
+ }
+};
+
+export const limit = async (userId: string, type = "page") => {
+ const count = await db
+ .select({
+ count: sql<number>`count(*)`.mapWith(Number),
+ })
+ .from(storedContent)
+ .where(and(eq(storedContent.userId, userId), eq(storedContent.type, type)));
+
+ if (count[0]!.count > LIMITS[type as keyof typeof LIMITS]) {
+ return false;
+ }
+
+ return true;
+};
+
+const getTweetData = async (tweetID: string) => {
+ const url = `https://cdn.syndication.twimg.com/tweet-result?id=${tweetID}&lang=en&features=tfw_timeline_list%3A%3Btfw_follower_count_sunset%3Atrue%3Btfw_tweet_edit_backend%3Aon%3Btfw_refsrc_session%3Aon%3Btfw_fosnr_soft_interventions_enabled%3Aon%3Btfw_show_birdwatch_pivots_enabled%3Aon%3Btfw_show_business_verified_badge%3Aon%3Btfw_duplicate_scribes_to_settings%3Aon%3Btfw_use_profile_image_shape_enabled%3Aon%3Btfw_show_blue_verified_badge%3Aon%3Btfw_legacy_timeline_sunset%3Atrue%3Btfw_show_gov_verified_badge%3Aon%3Btfw_show_business_affiliate_badge%3Aon%3Btfw_tweet_edit_frontend%3Aon&token=4c2mmul6mnh`;
+
+ const resp = await fetch(url, {
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
+ Accept: "application/json",
+ "Accept-Language": "en-US,en;q=0.5",
+ "Accept-Encoding": "gzip, deflate, br",
+ Connection: "keep-alive",
+ "Upgrade-Insecure-Requests": "1",
+ "Cache-Control": "max-age=0",
+ TE: "Trailers",
+ },
+ });
+ console.log(resp.status);
+ const data = (await resp.json()) as Tweet;
+
+ return data;
+};
+
+export const createMemory = async (input: {
+ content: string;
+ spaces?: string[];
+}): ServerActionReturnType<number> => {
+ const data = await auth();
+
+ if (!data || !data.user || !data.user.id) {
+ return { error: "Not authenticated", success: false };
+ }
+
+ const type = typeDecider(input.content);
+
+ let pageContent = input.content;
+ let metadata: Awaited<ReturnType<typeof getMetaData>>;
+
+ if (!(await limit(data.user.id, type))) {
+ return {
+ success: false,
+ data: 0,
+ error: `You have exceeded the limit of ${LIMITS[type as keyof typeof LIMITS]} ${type}s.`,
+ };
+ }
+
+ if (type === "page") {
+ const response = await fetch("https://md.dhr.wtf/?url=" + input.content, {
+ headers: {
+ Authorization: "Bearer " + process.env.BACKEND_SECURITY_KEY,
+ },
+ });
+ pageContent = await response.text();
+ metadata = await getMetaData(input.content);
+ } else if (type === "tweet") {
+ const tweet = await getTweetData(input.content.split("/").pop() as string);
+ pageContent = JSON.stringify(tweet);
+ metadata = {
+ baseUrl: input.content,
+ description: tweet.text,
+ image: tweet.user.profile_image_url_https,
+ title: `Tweet by ${tweet.user.name}`,
+ };
+ } else if (type === "note") {
+ pageContent = input.content;
+ const noteId = new Date().getTime();
+ metadata = {
+ baseUrl: `https://supermemory.ai/note/${noteId}`,
+ description: `Note created at ${new Date().toLocaleString()}`,
+ image: "https://supermemory.ai/logo.png",
+ title: `${pageContent.slice(0, 20)} ${pageContent.length > 20 ? "..." : ""}`,
+ };
+ } else {
+ return {
+ success: false,
+ data: 0,
+ error: "Invalid type",
+ };
+ }
+
+ let storeToSpaces = input.spaces;
+
+ if (!storeToSpaces) {
+ storeToSpaces = [];
+ }
+
+ const vectorSaveResponse = await fetch(
+ `${process.env.BACKEND_BASE_URL}/api/add`,
+ {
+ method: "POST",
+ body: JSON.stringify({
+ pageContent,
+ title: metadata.title,
+ description: metadata.description,
+ url: metadata.baseUrl,
+ spaces: storeToSpaces,
+ user: data.user.id,
+ type,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + process.env.BACKEND_SECURITY_KEY,
+ },
+ },
+ );
+
+ if (!vectorSaveResponse.ok) {
+ const errorData = await vectorSaveResponse.text();
+ console.log(errorData);
+ return {
+ success: false,
+ data: 0,
+ error: `Failed to save to vector store. Backend returned error: ${errorData}`,
+ };
+ }
+
+ // Insert into database
+ const insertResponse = await db
+ .insert(storedContent)
+ .values({
+ content: pageContent,
+ title: metadata.title,
+ description: metadata.description,
+ url: input.content,
+ baseUrl: metadata.baseUrl,
+ image: metadata.image,
+ savedAt: new Date(),
+ userId: data.user.id,
+ type,
+ })
+ .returning({ id: storedContent.id });
+
+ const contentId = insertResponse[0]?.id;
+ if (!contentId) {
+ return {
+ success: false,
+ data: 0,
+ error: "Something went wrong while saving the document to the database",
+ };
+ }
+
+ if (storeToSpaces.length > 0) {
+ // Adding the many-to-many relationship between content and spaces
+ const spaceData = await db
+ .select()
+ .from(space)
+ .where(
+ and(
+ inArray(
+ space.id,
+ storeToSpaces.map((s) => parseInt(s)),
+ ),
+ eq(space.user, data.user.id),
+ ),
+ )
+ .all();
+
+ await Promise.all(
+ spaceData.map(async (space) => {
+ await db
+ .insert(contentToSpace)
+ .values({ contentId: contentId, spaceId: space.id });
+ }),
+ );
+ }
+
+ try {
+ const response = await vectorSaveResponse.json();
+
+ const expectedResponse = z.object({ status: z.literal("ok") });
+
+ const parsedResponse = expectedResponse.safeParse(response);
+
+ if (!parsedResponse.success) {
+ return {
+ success: false,
+ data: 0,
+ error: `Failed to save to vector store. Backend returned error: ${parsedResponse.error.message}`,
+ };
+ }
+
+ return {
+ success: true,
+ data: 1,
+ };
+ } catch (e) {
+ return {
+ success: false,
+ data: 0,
+ error: `Failed to save to vector store. Backend returned error: ${e}`,
+ };
+ }
+};
diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts
index 9c2527f0..dc71252e 100644
--- a/apps/web/app/actions/fetchers.ts
+++ b/apps/web/app/actions/fetchers.ts
@@ -1,10 +1,15 @@
"use server";
-import { eq } from "drizzle-orm";
-import { db } from "../helpers/server/db";
-import { users } from "../helpers/server/db/schema";
+import { eq, inArray, not, sql } from "drizzle-orm";
+import { db } from "../../server/db";
+import {
+ Content,
+ contentToSpace,
+ storedContent,
+ users,
+} from "../../server/db/schema";
import { ServerActionReturnType, Space } from "./types";
-import { auth } from "../helpers/server/auth";
+import { auth } from "../../server/auth";
export const getSpaces = async (): ServerActionReturnType<Space[]> => {
const data = await auth();
@@ -23,3 +28,115 @@ export const getSpaces = async (): ServerActionReturnType<Space[]> => {
return { success: true, data: spacesWithoutUser };
};
+
+export const getAllMemories = async (
+ freeMemoriesOnly: boolean = false,
+): ServerActionReturnType<Content[]> => {
+ const data = await auth();
+
+ if (!data || !data.user) {
+ return { error: "Not authenticated", success: false };
+ }
+
+ if (!freeMemoriesOnly) {
+ // Returns all memories, no matter the space.
+ const memories = await db.query.storedContent.findMany({
+ where: eq(users, data.user.id),
+ });
+
+ return { success: true, data: memories };
+ }
+
+ // This only returns memories that are not a part of any space.
+ // This is useful for home page where we want to show a list of spaces and memories.
+ const contentNotInAnySpace = await db
+ .select()
+ .from(storedContent)
+ .where(
+ not(
+ eq(
+ storedContent.id,
+ db
+ .select({ contentId: contentToSpace.contentId })
+ .from(contentToSpace),
+ ),
+ ),
+ )
+ .execute();
+
+ return { success: true, data: contentNotInAnySpace };
+};
+
+export const getAllUserMemoriesAndSpaces = async (): ServerActionReturnType<{
+ spaces: Space[];
+ memories: Content[];
+}> => {
+ const data = await auth();
+
+ if (!data || !data.user) {
+ return { error: "Not authenticated", success: false };
+ }
+
+ const spaces = await db.query.space.findMany({
+ where: eq(users, data.user.id),
+ });
+
+ const spacesWithoutUser = spaces.map((space) => {
+ return { ...space, user: undefined };
+ });
+
+ // const contentCountBySpace = await db
+ // .select({
+ // spaceId: contentToSpace.spaceId,
+ // count: sql<number>`count(*)`.mapWith(Number),
+ // })
+ // .from(contentToSpace)
+ // .where(
+ // inArray(
+ // contentToSpace.spaceId,
+ // spacesWithoutUser.map((space) => space.id),
+ // ),
+ // )
+ // .groupBy(contentToSpace.spaceId)
+ // .execute();
+
+ // console.log(contentCountBySpace);
+
+ // get a count with space mappings like spaceID: count (number of memories in that space)
+ const contentCountBySpace = await db
+ .select({
+ spaceId: contentToSpace.spaceId,
+ count: sql<number>`count(*)`.mapWith(Number),
+ })
+ .from(contentToSpace)
+ .where(
+ inArray(
+ contentToSpace.spaceId,
+ spacesWithoutUser.map((space) => space.id),
+ ),
+ )
+ .groupBy(contentToSpace.spaceId)
+ .execute();
+
+ console.log(contentCountBySpace);
+
+ const contentNotInAnySpace = await db
+ .select()
+ .from(storedContent)
+ .where(
+ not(
+ eq(
+ storedContent.id,
+ db
+ .select({ contentId: contentToSpace.contentId })
+ .from(contentToSpace),
+ ),
+ ),
+ )
+ .execute();
+
+ return {
+ success: true,
+ data: { spaces: spacesWithoutUser, memories: contentNotInAnySpace },
+ };
+};
diff --git a/apps/web/app/actions/types.ts b/apps/web/app/actions/types.ts
index fbf669e2..5c5afc5c 100644
--- a/apps/web/app/actions/types.ts
+++ b/apps/web/app/actions/types.ts
@@ -1,6 +1,7 @@
export type Space = {
id: number;
name: string;
+ numberOfMemories?: number;
};
export type ServerActionReturnType<T> = Promise<{
diff --git a/apps/web/app/api/[...nextauth]/route.ts b/apps/web/app/api/[...nextauth]/route.ts
index 50807ab1..e19cc16e 100644
--- a/apps/web/app/api/[...nextauth]/route.ts
+++ b/apps/web/app/api/[...nextauth]/route.ts
@@ -1,2 +1,2 @@
-export { GET, POST } from "../../helpers/server/auth";
+export { GET, POST } from "../../../server/auth";
export const runtime = "edge";
diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts
index aba8784c..c19ce92b 100644
--- a/apps/web/app/api/chat/route.ts
+++ b/apps/web/app/api/chat/route.ts
@@ -54,7 +54,7 @@ export async function POST(req: NextRequest) {
);
const resp = await fetch(
- `https://new-cf-ai-backend.dhravya.workers.dev/api/chat?query=${query}&user=${session.user.email}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
+ `${process.env.BACKEND_BASE_URL}/api/chat?query=${query}&user=${session.user.id}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_SECURITY_KEY}`,
diff --git a/apps/web/app/api/editorai/route.ts b/apps/web/app/api/editorai/route.ts
new file mode 100644
index 00000000..5e1fbf0c
--- /dev/null
+++ b/apps/web/app/api/editorai/route.ts
@@ -0,0 +1,30 @@
+import type { NextRequest } from "next/server";
+import { ensureAuth } from "../ensureAuth";
+
+export const runtime = "edge";
+
+// ERROR #2 - This the the next function that calls the backend, I sometimes think this is redundency, but whatever
+// I have commented the auth code, It should not work in development, but it still does sometimes
+export async function POST(request: NextRequest) {
+ // const d = await ensureAuth(request);
+ // if (!d) {
+ // return new Response("Unauthorized", { status: 401 });
+ // }
+ const res : {context: string, request: string} = await request.json()
+
+ try {
+ const resp = await fetch(`${process.env.BACKEND_BASE_URL}/api/editorai?context=${res.context}&request=${res.request}`);
+ // this just checks if there are erros I am keeping it commented for you to better understand the important pieces
+ // if (resp.status !== 200 || !resp.ok) {
+ // const errorData = await resp.text();
+ // console.log(errorData);
+ // return new Response(
+ // JSON.stringify({ message: "Error in CF function", error: errorData }),
+ // { status: resp.status },
+ // );
+ // }
+ return new Response(resp.body, { status: 200 });
+ } catch (error) {
+ return new Response(`Error, ${error}`)
+ }
+} \ No newline at end of file
diff --git a/apps/web/app/api/ensureAuth.ts b/apps/web/app/api/ensureAuth.ts
index a1401a07..d2fbac0b 100644
--- a/apps/web/app/api/ensureAuth.ts
+++ b/apps/web/app/api/ensureAuth.ts
@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
-import { db } from "../helpers/server/db";
-import { sessions, users } from "../helpers/server/db/schema";
+import { db } from "../../server/db";
+import { sessions, users } from "../../server/db/schema";
import { eq } from "drizzle-orm";
export async function ensureAuth(req: NextRequest) {
diff --git a/apps/web/app/api/getCount/route.ts b/apps/web/app/api/getCount/route.ts
index f760c145..7cd2a2d3 100644
--- a/apps/web/app/api/getCount/route.ts
+++ b/apps/web/app/api/getCount/route.ts
@@ -1,6 +1,6 @@
-import { db } from "@/app/helpers/server/db";
+import { db } from "@/server/db";
import { and, eq, ne, sql } from "drizzle-orm";
-import { sessions, storedContent, users } from "@/app/helpers/server/db/schema";
+import { sessions, storedContent, users } from "@/server/db/schema";
import { type NextRequest, NextResponse } from "next/server";
import { ensureAuth } from "../ensureAuth";
@@ -20,7 +20,7 @@ export async function GET(req: NextRequest) {
.from(storedContent)
.where(
and(
- eq(storedContent.user, session.user.id),
+ eq(storedContent.userId, session.user.id),
eq(storedContent.type, "twitter-bookmark"),
),
);
@@ -32,7 +32,7 @@ export async function GET(req: NextRequest) {
.from(storedContent)
.where(
and(
- eq(storedContent.user, session.user.id),
+ eq(storedContent.userId, session.user.id),
ne(storedContent.type, "twitter-bookmark"),
),
);
diff --git a/apps/web/app/api/me/route.ts b/apps/web/app/api/me/route.ts
index 20b6aece..621dcbfe 100644
--- a/apps/web/app/api/me/route.ts
+++ b/apps/web/app/api/me/route.ts
@@ -1,6 +1,6 @@
-import { db } from "@/app/helpers/server/db";
+import { db } from "@/server/db";
import { eq } from "drizzle-orm";
-import { sessions, users } from "@/app/helpers/server/db/schema";
+import { sessions, users } from "@/server/db/schema";
import { type NextRequest, NextResponse } from "next/server";
export const runtime = "edge";
diff --git a/apps/web/app/api/spaces/route.ts b/apps/web/app/api/spaces/route.ts
index c46b02fc..cbed547d 100644
--- a/apps/web/app/api/spaces/route.ts
+++ b/apps/web/app/api/spaces/route.ts
@@ -1,5 +1,5 @@
-import { db } from "@/app/helpers/server/db";
-import { sessions, space, users } from "@/app/helpers/server/db/schema";
+import { db } from "@/server/db";
+import { sessions, space, users } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";
import { ensureAuth } from "../ensureAuth";
diff --git a/apps/web/app/api/store/route.ts b/apps/web/app/api/store/route.ts
index f96f90cf..cb10db24 100644
--- a/apps/web/app/api/store/route.ts
+++ b/apps/web/app/api/store/route.ts
@@ -1,4 +1,4 @@
-import { db } from "@/app/helpers/server/db";
+import { db } from "@/server/db";
import { and, eq, sql, inArray } from "drizzle-orm";
import {
contentToSpace,
@@ -6,10 +6,12 @@ import {
storedContent,
users,
space,
-} from "@/app/helpers/server/db/schema";
+} from "@/server/db/schema";
import { type NextRequest, NextResponse } from "next/server";
-import { getMetaData } from "@/app/helpers/lib/get-metadata";
+import { getMetaData } from "@/lib/get-metadata";
import { ensureAuth } from "../ensureAuth";
+import { limit } from "@/app/actions/doers";
+import { LIMITS } from "@/lib/constants";
export const runtime = "edge";
@@ -33,22 +35,13 @@ export async function POST(req: NextRequest) {
storeToSpaces = [];
}
- const count = await db
- .select({
- count: sql<number>`count(*)`.mapWith(Number),
- })
- .from(storedContent)
- .where(
- and(
- eq(storedContent.user, session.user.id),
- eq(storedContent.type, "page"),
- ),
- );
-
- if (count[0]!.count > 100) {
+ if (!(await limit(session.user.id))) {
return NextResponse.json(
- { message: "Error", error: "Limit exceeded" },
- { status: 499 },
+ {
+ message: "Error: Ratelimit exceeded",
+ error: `You have exceeded the limit of ${LIMITS["page"]} pages.`,
+ },
+ { status: 429 },
);
}
@@ -62,7 +55,7 @@ export async function POST(req: NextRequest) {
baseUrl: metadata.baseUrl,
image: metadata.image,
savedAt: new Date(),
- user: session.user.id,
+ userId: session.user.id,
})
.returning({ id: storedContent.id });
diff --git a/apps/web/app/api/unfirlsite/route.ts b/apps/web/app/api/unfirlsite/route.ts
new file mode 100644
index 00000000..4b8b4858
--- /dev/null
+++ b/apps/web/app/api/unfirlsite/route.ts
@@ -0,0 +1,134 @@
+import { load } from 'cheerio'
+import { AwsClient } from "aws4fetch";
+
+import type { NextRequest } from "next/server";
+import { ensureAuth } from "../ensureAuth";
+
+export const runtime = "edge";
+
+const r2 = new AwsClient({
+ accessKeyId: process.env.R2_ACCESS_KEY_ID,
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
+});
+
+
+export async function POST(request: NextRequest) {
+
+ const d = await ensureAuth(request);
+ if (!d) {
+ return new Response("Unauthorized", { status: 401 });
+ }
+
+ if (
+ !process.env.R2_ACCESS_KEY_ID ||
+ !process.env.R2_ACCOUNT_ID ||
+ !process.env.R2_SECRET_ACCESS_KEY ||
+ !process.env.R2_BUCKET_NAME
+ ) {
+ return new Response(
+ "Missing one or more R2 env variables: R2_ENDPOINT, R2_ACCESS_ID, R2_SECRET_KEY, R2_BUCKET_NAME. To get them, go to the R2 console, create and paste keys in a `.dev.vars` file in the root of this project.",
+ { status: 500 },
+ );
+ }
+
+ const website = new URL(request.url).searchParams.get("website");
+
+ if (!website) {
+ return new Response("Missing website", { status: 400 });
+ }
+
+ const salt = () => Math.floor(Math.random() * 11);
+ const encodeWebsite = `${encodeURIComponent(website)}${salt()}`;
+
+ try {
+ // this returns the og image, description and title of website
+ const response = await unfurl(website);
+
+ if (!response.image){
+ return new Response(JSON.stringify(response))
+ }
+
+ const imageUrl = await process.env.DEV_IMAGES.get(encodeWebsite)
+ if (imageUrl){
+ return new Response(JSON.stringify({
+ image: imageUrl,
+ title: response.title,
+ description: response.description,
+ }))
+ }
+
+ const res = await fetch(`${response.image}`)
+ const image = await res.blob();
+
+ const url = new URL(
+ `https://${process.env.R2_BUCKET_NAME}.${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`
+ );
+
+ url.pathname = encodeWebsite;
+ url.searchParams.set("X-Amz-Expires", "3600");
+
+ const signedPuturl = await r2.sign(
+ new Request(url, {
+ method: "PUT",
+ }),
+ {
+ aws: { signQuery: true },
+ }
+ );
+ await fetch(signedPuturl.url, {
+ method: 'PUT',
+ body: image,
+ });
+
+ await process.env.DEV_IMAGES.put(encodeWebsite, `${process.env.R2_PUBLIC_BUCKET_ADDRESS}/${encodeWebsite}`)
+
+ return new Response(JSON.stringify({
+ image: `${process.env.R2_PUBLIC_BUCKET_ADDRESS}/${encodeWebsite}`,
+ title: response.title,
+ description: response.description,
+ }));
+
+ } catch (error) {
+ console.log(error)
+ return new Response(JSON.stringify({
+ status: 500,
+ error: error,
+ }))
+ }
+ }
+
+export async function unfurl(url: string) {
+ const response = await fetch(url)
+ if (response.status >= 400) {
+ throw new Error(`Error fetching url: ${response.status}`)
+ }
+ const contentType = response.headers.get('content-type')
+ if (!contentType?.includes('text/html')) {
+ throw new Error(`Content-type not right: ${contentType}`)
+ }
+
+ const content = await response.text()
+ const $ = load(content)
+
+ const og: { [key: string]: string | undefined } = {}
+ const twitter: { [key: string]: string | undefined } = {}
+
+ // @ts-ignore, it just works so why care of type safety if someone has better way go ahead
+ $('meta[property^=og:]').each((_, el) => (og[$(el).attr('property')!] = $(el).attr('content')))
+ // @ts-ignore
+ $('meta[name^=twitter:]').each((_, el) => (twitter[$(el).attr('name')!] = $(el).attr('content')))
+
+ const title = og['og:title'] ?? twitter['twitter:title'] ?? $('title').text() ?? undefined
+ const description =
+ og['og:description'] ??
+ twitter['twitter:description'] ??
+ $('meta[name="description"]').attr('content') ??
+ undefined
+ const image = og['og:image:secure_url'] ?? og['og:image'] ?? twitter['twitter:image'] ?? undefined
+
+ return {
+ title,
+ description,
+ image,
+ }
+}
diff --git a/apps/web/app/ref/page.tsx b/apps/web/app/ref/page.tsx
index 9ace733a..b51a16bb 100644
--- a/apps/web/app/ref/page.tsx
+++ b/apps/web/app/ref/page.tsx
@@ -1,9 +1,9 @@
import { Button } from "@repo/ui/shadcn/button";
-import { auth, signIn, signOut } from "../helpers/server/auth";
-import { db } from "../helpers/server/db";
+import { auth, signIn, signOut } from "../../server/auth";
+import { db } from "../../server/db";
import { sql } from "drizzle-orm";
-import { users } from "../helpers/server/db/schema";
-import { getThemeToggler } from "../helpers/lib/get-theme-button";
+import { users } from "../../server/db/schema";
+import { getThemeToggler } from "../../lib/get-theme-button";
export const runtime = "edge";
diff --git a/apps/web/cf-env.d.ts b/apps/web/cf-env.d.ts
index 98303f35..be5c991a 100644
--- a/apps/web/cf-env.d.ts
+++ b/apps/web/cf-env.d.ts
@@ -1,6 +1,17 @@
declare global {
namespace NodeJS {
- interface ProcessEnv extends CloudflareEnv {}
+ interface ProcessEnv extends CloudflareEnv {
+ GOOGLE_CLIENT_ID: string;
+ GOOGLE_CLIENT_SECRET: string;
+ AUTH_SECRET: string;
+ R2_ENDPOINT: string;
+ R2_ACCESS_KEY_ID: string;
+ R2_SECRET_ACCESS_KEY: string;
+ R2_PUBLIC_BUCKET_ADDRESS: string;
+ R2_BUCKET_NAME: string;
+ BACKEND_SECURITY_KEY: string;
+ BACKEND_BASE_URL: string;
+ }
}
}
diff --git a/apps/web/env.d.ts b/apps/web/env.d.ts
index 2755280c..4f11ba55 100644
--- a/apps/web/env.d.ts
+++ b/apps/web/env.d.ts
@@ -2,14 +2,6 @@
// by running `wrangler types --env-interface CloudflareEnv env.d.ts`
interface CloudflareEnv {
- GOOGLE_CLIENT_ID: string;
- GOOGLE_CLIENT_SECRET: string;
- AUTH_SECRET: string;
- R2_ENDPOINT: string;
- R2_ACCESS_ID: string;
- R2_SECRET_KEY: string;
- R2_BUCKET_NAME: string;
- BACKEND_SECURITY_KEY: string;
STORAGE: R2Bucket;
DATABASE: D1Database;
}
diff --git a/apps/web/app/helpers/constants.ts b/apps/web/lib/constants.ts
index c3fc640a..7a9485cf 100644
--- a/apps/web/app/helpers/constants.ts
+++ b/apps/web/lib/constants.ts
@@ -1,3 +1,9 @@
+export const LIMITS = {
+ page: 100,
+ tweet: 1000,
+ note: 1000,
+};
+
export const codeLanguageSubset = [
"python",
"javascript",
diff --git a/apps/web/app/helpers/lib/get-metadata.ts b/apps/web/lib/get-metadata.ts
index 4609e49b..4609e49b 100644
--- a/apps/web/app/helpers/lib/get-metadata.ts
+++ b/apps/web/lib/get-metadata.ts
diff --git a/apps/web/app/helpers/lib/get-theme-button.tsx b/apps/web/lib/get-theme-button.tsx
index 020cc976..020cc976 100644
--- a/apps/web/app/helpers/lib/get-theme-button.tsx
+++ b/apps/web/lib/get-theme-button.tsx
diff --git a/apps/web/app/helpers/lib/handle-errors.ts b/apps/web/lib/handle-errors.ts
index 42cae589..42cae589 100644
--- a/apps/web/app/helpers/lib/handle-errors.ts
+++ b/apps/web/lib/handle-errors.ts
diff --git a/apps/web/app/helpers/lib/searchParams.ts b/apps/web/lib/searchParams.ts
index 9899eaf7..9899eaf7 100644
--- a/apps/web/app/helpers/lib/searchParams.ts
+++ b/apps/web/lib/searchParams.ts
diff --git a/apps/web/app/helpers/server/auth.ts b/apps/web/server/auth.ts
index c4e426d4..c4e426d4 100644
--- a/apps/web/app/helpers/server/auth.ts
+++ b/apps/web/server/auth.ts
diff --git a/apps/web/app/helpers/server/db/index.ts b/apps/web/server/db/index.ts
index 4d671bea..4d671bea 100644
--- a/apps/web/app/helpers/server/db/index.ts
+++ b/apps/web/server/db/index.ts
diff --git a/apps/web/app/helpers/server/db/schema.ts b/apps/web/server/db/schema.ts
index e3e789c6..1ff23c82 100644
--- a/apps/web/app/helpers/server/db/schema.ts
+++ b/apps/web/server/db/schema.ts
@@ -103,11 +103,9 @@ export const storedContent = createTable(
savedAt: int("savedAt", { mode: "timestamp" }).notNull(),
baseUrl: text("baseUrl", { length: 255 }),
ogImage: text("ogImage", { length: 255 }),
- type: text("type", { enum: ["note", "page", "twitter-bookmark"] }).default(
- "page",
- ),
+ type: text("type").default("page"),
image: text("image", { length: 255 }),
- userId: int("user").references(() => users.id, {
+ userId: text("user").references(() => users.id, {
onDelete: "cascade",
}),
},
@@ -119,6 +117,8 @@ export const storedContent = createTable(
}),
);
+export type Content = typeof storedContent.$inferSelect;
+
export const contentToSpace = createTable(
"contentToSpace",
{
diff --git a/package.json b/package.json
index 63d008d9..b6597fd7 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"devDependencies": {
"@clack/prompts": "^0.7.0",
"@cloudflare/next-on-pages": "1",
- "@cloudflare/workers-types": "^4.20240512.0",
+ "@cloudflare/workers-types": "^4.20240614.0",
"@repo/eslint-config": "*",
"@repo/tailwind-config": "*",
"@repo/typescript-config": "*",
@@ -45,7 +45,7 @@
"@auth/drizzle-adapter": "^1.1.0",
"@aws-sdk/client-s3": "^3.577.0",
"@aws-sdk/s3-request-presigner": "^3.577.0",
- "@cloudflare/puppeteer": "^0.0.8",
+ "@cloudflare/puppeteer": "^0.0.11",
"@headlessui/react": "^2.0.4",
"@heroicons/react": "^2.1.3",
"@hono/swagger-ui": "^0.2.2",
@@ -66,6 +66,7 @@
"@tldraw/assets": "^2.2.0",
"@types/readline-sync": "^1.4.8",
"ai": "^3.1.14",
+ "aws4fetch": "^1.0.18",
"cheerio": "^1.0.0-rc.12",
"compromise": "^14.13.0",
"drizzle-orm": "^0.30.10",
@@ -79,6 +80,7 @@
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.51.5",
"react-markdown": "^9.0.1",
+ "react-tweet": "^3.2.1",
"rehype-highlight": "^7.0.0",
"rehype-katex": "^7.0.0",
"remark-gfm": "^4.0.0",
diff --git a/packages/shared-types/index.ts b/packages/shared-types/index.ts
index 46e3edba..d3f466e1 100644
--- a/packages/shared-types/index.ts
+++ b/packages/shared-types/index.ts
@@ -10,8 +10,10 @@ export const ChatHistoryZod = z.object({
source: z.string(),
title: z.string(),
content: z.string(),
+ numChunks: z.number().optional().default(1),
}),
),
+ justification: z.string().optional(),
}),
});
diff --git a/packages/tailwind-config/globals.css b/packages/tailwind-config/globals.css
index 2587fe25..93099e8d 100644
--- a/packages/tailwind-config/globals.css
+++ b/packages/tailwind-config/globals.css
@@ -2,39 +2,37 @@
@tailwind components;
@tailwind utilities;
-@media (prefers-color-scheme: dark) {
- :root {
- --foreground: rgba(179, 188, 197, 1);
- --foreground-menu: rgba(106, 115, 125, 1);
- --background: rgba(23, 27, 31, 1);
- --secondary: rgba(31, 36, 40, 1);
- --primary: rgba(54, 157, 253, 1);
- --border: rgba(51, 57, 67, 1);
+:root {
+ --foreground: rgba(179, 188, 197, 1);
+ --foreground-menu: rgba(106, 115, 125, 1);
+ --background: rgba(23, 27, 31, 1);
+ --secondary: rgba(31, 36, 40, 1);
+ --primary: rgba(54, 157, 253, 1);
+ --border: rgba(51, 57, 67, 1);
- --card: 0 0% 100%;
- --card-foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
- --primary-foreground: 0 0% 98%;
+ --primary-foreground: 0 0% 98%;
- --secondary-foreground: 0 0% 9%;
+ --secondary-foreground: 0 0% 9%;
- --muted: 0 0% 96.1%;
- --muted-foreground: 0 0% 45.1%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
- --accent: 0 0% 96.1%;
- --accent-foreground: 0 0% 9%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 0 0% 98%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
- --input: 0 0% 89.8%;
- --ring: 0 0% 3.9%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
- --radius: 0.5rem;
- }
+ --radius: 0.5rem;
}
body {