feat: Rewrite backend to hono

This commit is contained in:
Dhravya 2024-05-18 17:32:14 -05:00
parent 54cc2687f3
commit c68433d9d8
48 changed files with 1268 additions and 1656 deletions

View file

@ -1,13 +0,0 @@
# http://editorconfig.org
root = true
[*]
indent_style = tab
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space

View file

@ -1,173 +1,13 @@
# Logs
wrangler.toml
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
dist
node_modules
worker
package-lock.json
yarn.lock
.cargo-ok
# Output of 'npm pack'
*.tgz
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# wrangler project
# wrangler files
.wrangler
.dev.vars
.wrangler/

View file

@ -1,6 +0,0 @@
{
"printWidth": 140,
"singleQuote": true,
"semi": true,
"useTabs": true
}

View file

@ -0,0 +1,58 @@
# Hono minimal project
This is a minimal project with [Hono](https://github.com/honojs/hono/) for Cloudflare Workers.
## Features
- Minimal
- TypeScript
- Wrangler to develop and deploy.
- [Jest](https://jestjs.io/ja/) for testing.
## Usage
Initialize
```
npx create-cloudflare my-app https://github.com/honojs/hono-minimal
```
Install
```
yarn install
```
Develop
```
yarn dev
```
Test
```
yarn test
```
Deploy
```
yarn deploy
```
## Examples
See: <https://github.com/honojs/examples>
## For more information
See: <https://honojs.dev>
## Author
Yusuke Wada <https://github.com/yusukebe>
## License
MIT

View file

@ -0,0 +1,7 @@
module.exports = {
testEnvironment: "miniflare",
testMatch: ["**/test/**/*.+(ts|tsx)", "**/src/**/(*.)+(spec|test).+(ts|tsx)"],
transform: {
"^.+\\.(ts|tsx)$": "esbuild-jest",
},
};

View file

@ -1,16 +1,17 @@
{
"name": "cf-ai-backend",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"unsafe-reset-vector-db": "wrangler vectorize delete supermem-vector && wrangler vectorize create --dimensions=1536 supermem-vector-1 --metric=cosine"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240222.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
}
"name": "new-cf-ai-backend",
"private": true,
"version": "0.0.1",
"main": "src/index.ts",
"scripts": {
"test": "jest --verbose",
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"unsafe-reset-vector-db": "wrangler vectorize delete supermem-vector && wrangler vectorize create --dimensions=1536 supermem-vector-1 --metric=cosine"
},
"license": "MIT",
"dependencies": {
"@hono/zod-validator": "^0.2.1"
}
}

View file

@ -1,43 +0,0 @@
import { AiTextGenerationOutput } from '@cloudflare/ai/dist/ai/tasks/text-generation';
interface OpenAIEmbeddingsParams {
apiKey: string;
modelName: string;
}
export class OpenAIEmbeddings {
private apiKey: string;
private modelName: string;
constructor({ apiKey, modelName }: OpenAIEmbeddingsParams) {
this.apiKey = apiKey;
this.modelName = modelName;
}
async embedDocuments(texts: string[]): Promise<number[][]> {
const responses = await Promise.all(texts.map((text) => this.embedQuery(text)));
return responses;
}
async embedQuery(text: string): Promise<number[]> {
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
input: text,
model: this.modelName,
}),
});
const data = (await response.json()) as {
data: {
embedding: number[];
}[];
};
return data.data[0].embedding;
}
}

View file

@ -1,19 +0,0 @@
interface Env {
VECTORIZE_INDEX: VectorizeIndex;
AI: Fetcher;
SECURITY_KEY: string;
OPENAI_API_KEY: string;
GOOGLE_AI_API_KEY: string;
MY_QUEUE: Queue<TweetData[]>;
KV: KVNamespace;
MYBROWSER: BrowserWorker;
}
interface TweetData {
tweetText: string;
postUrl: string;
authorName: string;
handle: string;
time: string;
saveToUser: string;
}

View file

@ -0,0 +1,139 @@
import { Context } from "hono";
import { Env, vectorObj } from "./types";
import { CloudflareVectorizeStore } from "@langchain/cloudflare";
import { OpenAIEmbeddings } from "./utils/OpenAIEmbedder";
import { createOpenAI } from "@ai-sdk/openai";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createAnthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import { seededRandom } from "./utils/seededRandom";
export async function initQuery(
c: Context<{ Bindings: Env }>,
model: string = "gpt-4o",
) {
const embeddings = new OpenAIEmbeddings({
apiKey: c.env.OPENAI_API_KEY,
modelName: "text-embedding-3-small",
});
const store = new CloudflareVectorizeStore(embeddings, {
index: c.env.VECTORIZE_INDEX,
});
const DEFAULT_MODEL = "gpt-4o";
let selectedModel:
| ReturnType<ReturnType<typeof createOpenAI>>
| ReturnType<ReturnType<typeof createGoogleGenerativeAI>>
| ReturnType<ReturnType<typeof createAnthropic>>;
switch (model) {
case "claude-3-opus":
const anthropic = createAnthropic({
apiKey: c.env.ANTHROPIC_API_KEY,
});
selectedModel = anthropic.chat("claude-3-opus-20240229");
console.log("Selected model: ", selectedModel);
break;
case "gemini-1.5-pro":
const googleai = createGoogleGenerativeAI({
apiKey: c.env.GOOGLE_AI_API_KEY,
});
selectedModel = googleai.chat("models/gemini-1.5-pro-latest");
console.log("Selected model: ", selectedModel);
break;
case "gpt-4o":
default:
const openai = createOpenAI({
apiKey: c.env.OPENAI_API_KEY,
});
selectedModel = openai.chat("gpt-4o");
break;
}
if (!selectedModel) {
throw new Error(
`Model ${model} not found and default model ${DEFAULT_MODEL} is also not available.`,
);
}
return { store, model: selectedModel };
}
export async function deleteDocument({
url,
user,
c,
store,
}: {
url: string;
user: string;
c: Context<{ Bindings: Env }>;
store: CloudflareVectorizeStore;
}) {
const toBeDeleted = `${url}-${user}`;
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] });
}
});
}
export async function batchCreateChunksAndEmbeddings({
store,
body,
chunks,
context,
}: {
store: CloudflareVectorizeStore;
body: z.infer<typeof vectorObj>;
chunks: string[];
context: Context<{ Bindings: Env }>;
}) {
const ourID = `${body.url}-${body.user}`;
await deleteDocument({ url: body.url, user: body.user, c: context, store });
const random = seededRandom(ourID);
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 newPageContent = `Title: ${body.title}\nDescription: ${body.description}\nURL: ${body.url}\nContent: ${chunk}`;
const docs = await store.addDocuments(
[
{
pageContent: newPageContent,
metadata: {
title: body.title?.slice(0, 50) ?? "",
description: body.description ?? "",
space: body.space ?? "",
url: body.url,
user: body.user,
},
},
],
{
ids: [uuid],
},
);
console.log("Docs added: ", docs);
await context.env.KV.put(uuid, ourID);
}
}

View file

@ -0,0 +1,13 @@
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);
});
});

View file

@ -1,50 +1,223 @@
import type { VectorizeIndex, Fetcher, Request } from '@cloudflare/workers-types';
import { z } from "zod";
import { Hono } from "hono";
import { CoreMessage, streamText } from "ai";
import { chatObj, Env, vectorObj } from "./types";
import {
batchCreateChunksAndEmbeddings,
deleteDocument,
initQuery,
} from "./helper";
import { timing } from "hono/timing";
import { logger } from "hono/logger";
import { poweredBy } from "hono/powered-by";
import { bearerAuth } from "hono/bearer-auth";
import { zValidator } from "@hono/zod-validator";
import chunkText from "./utils/chonker";
import { systemPrompt, template } from "./prompts/prompt1";
import { CloudflareVectorizeStore } from '@langchain/cloudflare';
import { OpenAIEmbeddings } from './OpenAIEmbedder';
import { GoogleGenerativeAI } from '@google/generative-ai';
import routeMap from './routes';
import { queue } from './routes/queue';
const app = new Hono<{ Bindings: Env }>();
function isAuthorized(request: Request, env: Env): boolean {
return request.headers.get('X-Custom-Auth-Key') === env.SECURITY_KEY;
}
// ------- MIDDLEWARES -------
app.use("*", poweredBy());
app.use("*", timing());
app.use("*", logger());
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
if (!isAuthorized(request, env)) {
return new Response('Unauthorized', { status: 401 });
}
app.use("/api/", async (c, next) => {
const auth = bearerAuth({ token: c.env.SECURITY_KEY });
return auth(c, next);
});
// ------- MIDDLEWARES END -------
const embeddings = new OpenAIEmbeddings({
apiKey: env.OPENAI_API_KEY,
modelName: 'text-embedding-3-small',
});
app.get("/", (c) => {
return c.text("Supermemory backend API is running!");
});
const store = new CloudflareVectorizeStore(embeddings, {
index: env.VECTORIZE_INDEX,
});
app.get("/api/health", (c) => {
return c.json({ status: "ok" });
});
const genAI = new GoogleGenerativeAI(env.GOOGLE_AI_API_KEY);
app.post("/api/add", zValidator("json", vectorObj), async (c) => {
const body = c.req.valid("json");
const model = genAI.getGenerativeModel({ model: 'gemini-pro' });
const { store } = await initQuery(c);
const url = new URL(request.url);
const path = url.pathname;
const method = request.method.toUpperCase();
await batchCreateChunksAndEmbeddings({
store,
body,
chunks: chunkText(body.pageContent, 1536),
context: c,
});
const routeHandlers = routeMap.get(path);
return c.json({ status: "ok" });
});
if (!routeHandlers) {
return new Response('Not Found', { status: 404 });
}
app.get(
"/api/ask",
zValidator(
"query",
z.object({
query: z.string(),
}),
),
async (c) => {
const query = c.req.valid("query");
const handler = routeHandlers[method];
const { model } = await initQuery(c);
if (!handler) {
return new Response('Method Not Allowed', { status: 405 });
}
return await handler(request, store, embeddings, model, env, ctx);
},
queue,
};
const response = await streamText({ model, prompt: query.query });
const r = response.toTextStreamResponse();
return r;
},
);
app.post(
"/api/chat",
zValidator(
"query",
z.object({
query: z.string(),
topK: z.number().optional().default(10),
user: z.string(),
spaces: z.string().optional(),
sourcesOnly: z.string().optional().default("false"),
model: z.string().optional().default("gpt-4o"),
}),
),
zValidator("json", chatObj),
async (c) => {
const query = c.req.valid("query");
const body = c.req.valid("json");
if (body.chatHistory) {
body.chatHistory = body.chatHistory.map((i) => ({
...i,
content: i.parts.length > 0 ? i.parts.join(" ") : i.content,
}));
}
const sourcesOnly = query.sourcesOnly === "true";
const spaces = query.spaces?.split(",") || [undefined];
const { model, store } = await initQuery(c, query.model);
const filter: VectorizeVectorMetadataFilter = { user: query.user };
const queryAsVector = await store.embeddings.embedQuery(query.query);
const responses: VectorizeMatches = { matches: [], count: 0 };
for (const space of spaces) {
if (space !== undefined) {
filter.space = space;
}
const resp = await c.env.VECTORIZE_INDEX.query(queryAsVector, {
topK: query.topK,
filter,
});
if (resp.count > 0) {
responses.matches.push(...resp.matches);
responses.count += resp.count;
}
}
const minScore = Math.min(...responses.matches.map(({ score }) => score));
const maxScore = Math.max(...responses.matches.map(({ score }) => score));
const normalizedData = responses.matches.map((data) => ({
...data,
normalizedScore:
maxScore !== minScore
? 1 + ((data.score - minScore) / (maxScore - minScore)) * 98
: 50, // If all scores are the same, set them to the middle of the scale
}));
let highScoreData = normalizedData.filter(
({ normalizedScore }) => normalizedScore > 50,
);
if (highScoreData.length === 0) {
highScoreData = normalizedData
.sort((a, b) => b.score - a.score)
.slice(0, 3);
}
const sortedHighScoreData = highScoreData.sort(
(a, b) => b.normalizedScore - a.normalizedScore,
);
console.log(JSON.stringify(sortedHighScoreData));
if (sourcesOnly) {
const idsAsStrings = sortedHighScoreData.map((dataPoint) =>
dataPoint.id.toString(),
);
const storedContent = await Promise.all(
idsAsStrings.map(async (id) => await c.env.KV.get(id)),
);
return c.json({ ids: storedContent });
}
const vec = await c.env.VECTORIZE_INDEX.getByIds(
sortedHighScoreData.map(({ id }) => id),
);
const vecWithScores = vec.map((v, i) => ({
...v,
score: sortedHighScoreData[i].score,
}));
const preparedContext = vecWithScores.map(({ metadata, score }) => ({
context: `Website title: ${metadata!.title}\nDescription: ${metadata!.description}\nURL: ${metadata!.url}\nContent: ${metadata!.text}`,
score,
}));
const initialMessages: CoreMessage[] = [
{ role: "user", content: systemPrompt },
{ role: "assistant", content: "Hello, how can I help?" },
];
const prompt = template({
contexts: preparedContext,
question: query.query,
});
const userMessage: CoreMessage = { role: "user", content: prompt };
const response = await streamText({
model,
messages: [
...initialMessages,
...((body.chatHistory || []) as CoreMessage[]),
userMessage,
],
temperature: 0.4,
});
return response.toTextStreamResponse();
},
);
app.delete(
"/api/delete",
zValidator(
"query",
z.object({
websiteUrl: z.string(),
user: z.string(),
}),
),
async (c) => {
const { websiteUrl, user } = c.req.valid("query");
const { store } = await initQuery(c);
await deleteDocument({ url: websiteUrl, user, c, store });
return c.json({ message: "Document deleted" });
},
);
export default app;

View file

@ -0,0 +1,47 @@
export const systemPrompt = `You are an AI assistant called Supermemory that acts as a "Second Brain" by answering questions based on provided context. Your goal is to directly address the question concisely and to the point, without excessive elaboration.
Multiple pieces of context, each with an associated relevance score, will be provided. Each context piece and its score will be enclosed within the following tags: <context> and <context_score>. The question you need to answer will be enclosed within the <question> tags.
To generate your answer:
- Carefully analyze the question and identify the key information needed to address it
- Locate the specific parts of each context that contain this key information
- Compare the relevance scores of the provided contexts
- In the <justification> tags, provide a brief justification for which context(s) are more relevant to answering the question based on the scores
- Concisely summarize the relevant information from the higher-scoring context(s) in your own words
- Provide a direct answer to the question
- Use markdown formatting in your answer, including bold, italics, and bullet points as appropriate to improve readability and highlight key points
- Give detailed and accurate responses for things like 'write a blog' or long-form questions.
Provide your justification between <justification> tags and your final answer between <answer> tags, formatting both in markdown.
If no context is provided, introduce yourself and explain that the user can save content which will allow you to answer questions about that content in the future. Do not provide an answer if no context is provided.`;
export const template = ({ contexts, question }) => {
// Map over contexts to generate the context and score parts
const contextParts = contexts
.map(
({ context, score }) => `
<context>
${context}
</context>
<context_score>
${score}
</context_score>`,
)
.join("\n");
// Construct the final prompt using a template literal
const finalPrompt = `
Here's the given context and question for the task:
${contextParts}
The question is provided in the prompt below:
<question>
${question}
</question>
`;
return finalPrompt.trim();
};

View file

@ -1,43 +0,0 @@
import { CloudflareVectorizeStore } from '@langchain/cloudflare';
import * as apiAdd from './routes/add';
import * as apiQuery from './routes/query';
import * as apiAsk from './routes/ask';
import * as apiChat from './routes/chat';
import * as apiBatchUploadTweets from './routes/batchUploadTweets';
import * as apiGetPageContent from './routes/getPageContent';
import * as apiDelete from './routes/delete';
import * as apiEdit from './routes/edit';
import * as apiWipeData from './routes/wipedata';
import { OpenAIEmbeddings } from './OpenAIEmbedder';
import { GenerativeModel } from '@google/generative-ai';
import { Request } from '@cloudflare/workers-types';
type RouteHandler = (
request: Request,
store: CloudflareVectorizeStore,
embeddings: OpenAIEmbeddings,
model: GenerativeModel,
env: Env,
ctx?: ExecutionContext,
) => Promise<Response>;
const routeMap = new Map<string, Record<string, RouteHandler>>();
routeMap.set('/add', apiAdd);
routeMap.set('/query', apiQuery);
routeMap.set('/ask', apiAsk);
routeMap.set('/chat', apiChat);
routeMap.set('/batchUploadTweets', apiBatchUploadTweets);
routeMap.set('/getPageContent', apiGetPageContent);
routeMap.set('/delete', apiDelete);
routeMap.set('/edit', apiEdit);
routeMap.set('/wipedata', apiWipeData);
// Add more route mappings as needed
// routeMap.set('/api/otherRoute', { ... });
export default routeMap;

View file

@ -1,52 +0,0 @@
import { Request } from '@cloudflare/workers-types';
import { type CloudflareVectorizeStore } from '@langchain/cloudflare';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { GenerativeModel } from '@google/generative-ai';
import { seededRandom } from '../util';
export async function POST(request: Request, store: CloudflareVectorizeStore, _: OpenAIEmbeddings, m: GenerativeModel, env: Env) {
const body = (await request.json()) as {
pageContent: string;
title?: string;
description?: string;
space?: string;
url: string;
user: string;
};
if (!body.pageContent || !body.url) {
return new Response(JSON.stringify({ message: 'Invalid Page Content' }), { status: 400 });
}
// TODO: FIX THIS,BUT TEMPERORILY TRIM page content to 1000 words
body.pageContent = body.pageContent.split(' ').slice(0, 1000).join(' ');
const newPageContent = `Title: ${body.title}\nDescription: ${body.description}\nURL: ${body.url}\nContent: ${body.pageContent}`;
const ourID = `${body.url}-${body.user}`;
const random = seededRandom(ourID);
const uuid = random().toString(36).substring(2, 15) + random().toString(36).substring(2, 15);
await env.KV.put(uuid, ourID);
await store.addDocuments(
[
{
pageContent: newPageContent,
metadata: {
title: body.title?.slice(0, 50) ?? '',
description: body.description ?? '',
space: body.space ?? '',
url: body.url,
user: body.user,
},
},
],
{
ids: [uuid],
},
);
return new Response(JSON.stringify({ message: 'Document Added' }), { status: 200 });
}

View file

@ -1,35 +0,0 @@
import { GenerativeModel } from '@google/generative-ai';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { CloudflareVectorizeStore } from '@langchain/cloudflare';
import { Request } from '@cloudflare/workers-types';
export async function POST(request: Request, _: CloudflareVectorizeStore, embeddings: OpenAIEmbeddings, model: GenerativeModel, env?: Env) {
const body = (await request.json()) as {
query: string;
};
if (!body.query) {
return new Response(JSON.stringify({ message: 'Invalid Page Content' }), { status: 400 });
}
const prompt = `You are an agent that answers a question based on the query. don't say 'based on the context'.\n\n Context:\n${body.query} \nAnswer this question based on the context. Question: ${body.query}\nAnswer:`;
const output = await model.generateContentStream(prompt);
const response = new Response(
new ReadableStream({
async start(controller) {
const converter = new TextEncoder();
for await (const chunk of output.stream) {
const chunkText = await chunk.text();
console.log(chunkText);
const encodedChunk = converter.encode('data: ' + JSON.stringify({ response: chunkText }) + '\n\n');
controller.enqueue(encodedChunk);
}
const doneChunk = converter.encode('data: [DONE]');
controller.enqueue(doneChunk);
controller.close();
},
}),
);
return response;
}

View file

@ -1,38 +0,0 @@
import { Request } from '@cloudflare/workers-types';
import { type CloudflareVectorizeStore } from '@langchain/cloudflare';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { GenerativeModel } from '@google/generative-ai';
export async function POST(request: Request, store: CloudflareVectorizeStore, _: OpenAIEmbeddings, m: GenerativeModel, env: Env) {
const body = (await request.json()) as TweetData[] | undefined;
if (!body) {
return new Response(JSON.stringify({ message: 'Body is missing' }), { status: 400 });
}
const bytes = new TextEncoder().encode(JSON.stringify(body)).length;
if (bytes < 128000) {
await env.MY_QUEUE.send(body);
} else {
let bytesTillNow = 0;
let batches: TweetData[] = [];
const getByteLength = (data: string) => new TextEncoder().encode(data).length;
for (let i = 0; i < body.length; i++) {
const byteLength = getByteLength(JSON.stringify(body[i]));
if (bytesTillNow + byteLength < 100000) {
bytesTillNow += byteLength;
batches.push(body[i]);
} else {
await env.MY_QUEUE.send(batches);
batches = [body[i]];
bytesTillNow = byteLength;
}
}
}
return new Response(JSON.stringify({ message: 'Document Added' }), { status: 200 });
}

View file

@ -1,141 +0,0 @@
import { Content, GenerativeModel } from '@google/generative-ai';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { CloudflareVectorizeStore } from '@langchain/cloudflare';
import { Request } from '@cloudflare/workers-types';
export async function POST(request: Request, _: CloudflareVectorizeStore, embeddings: OpenAIEmbeddings, model: GenerativeModel, env?: Env) {
const queryparams = new URL(request.url).searchParams;
const query = queryparams.get('q');
const topK = parseInt(queryparams.get('topK') ?? '5');
const user = queryparams.get('user');
const spaces = queryparams.get('spaces') ?? undefined;
const sp = spaces === 'null' ? undefined : spaces;
const spacesArray = sp ? sp.split(',') : undefined;
const sourcesOnly = queryparams.get('sourcesOnly') ?? 'false';
if (!user) {
return new Response(JSON.stringify({ message: 'Invalid User' }), { status: 400 });
}
if (!query) {
return new Response(JSON.stringify({ message: 'Invalid Query' }), { status: 400 });
}
const filter: VectorizeVectorMetadataFilter = {
user,
};
const responses: VectorizeMatches = { matches: [], count: 0 };
if (spacesArray) {
for (const space of spacesArray) {
filter.space = space;
const queryAsVector = await embeddings.embedQuery(query);
const resp = await env!.VECTORIZE_INDEX.query(queryAsVector, {
topK,
filter,
});
if (resp.count > 0) {
responses.matches.push(...resp.matches);
responses.count += resp.count;
}
}
} else {
const queryAsVector = await embeddings.embedQuery(query);
const resp = await env!.VECTORIZE_INDEX.query(queryAsVector, {
topK,
filter: {
user,
},
});
if (resp.count > 0) {
responses.matches.push(...resp.matches);
responses.count += resp.count;
}
}
// if (responses.count === 0) {
// return new Response(JSON.stringify({ message: "No Results Found" }), { status: 404 });
// }
const highScoreIds = responses.matches.filter(({ score }) => score > 0.3).map(({ id }) => id);
console.log('highscoreIds', highScoreIds);
if (sourcesOnly === 'true') {
// Try await env.KV.get(id) for each id in a Promise.all
const idsAsStrings = highScoreIds.map(String);
const storedContent = await Promise.all(
idsAsStrings.map(async (id) => {
const stored = await env!.KV.get(id);
if (stored) {
return stored;
}
return id;
}),
);
console.log(storedContent);
return new Response(JSON.stringify({ ids: storedContent }), { status: 200 });
}
const vec = await env!.VECTORIZE_INDEX.getByIds(highScoreIds);
const preparedContext = vec
.map(
({ metadata }) =>
`Website title: ${metadata!.title}\nDescription: ${metadata!.description}\nURL: ${metadata!.url}\nContent: ${metadata!.text}`,
)
.join('\n\n');
const body = (await request.json()) as {
chatHistory?: Content[];
};
const defaultHistory = [
{
role: 'user',
parts: [
{
text: `You are an agent that summarizes a page based on the query. don't say 'based on the context'. I expect you to be like a 'Second Brain'. you will be provided with the context (old saved posts) and questions. Answer accordingly. Answer in markdown format`,
},
],
},
{
role: 'model',
parts: [{ text: "Ok, I am a personal assistant, and will act as a second brain to help with user's queries." }],
},
] as Content[];
const chat = model.startChat({
history: [...defaultHistory, ...(body.chatHistory ?? [])],
});
const prompt =
`You are supermemory - an agent that answers a question based on the context provided. don't say 'based on the context'. Be concise and to the point, make sure that you are addressing the question properly but don't yap too much. I expect you to be like a 'Second Brain'. you will be provided with the context (old saved posts) and questions. Answer accordingly. Answer in markdown format. Use bold, italics, bullet points` +
`Context:\n${preparedContext == '' ? "No context, just introduce yourself and say something like 'I don't know, but you can save things from the sidebar on the right and then query me'" : preparedContext + `Question: ${query}\nAnswer:`}\n\n`;
const output = await chat.sendMessageStream(prompt);
const response = new Response(
new ReadableStream({
async start(controller) {
const converter = new TextEncoder();
for await (const chunk of output.stream) {
const chunkText = await chunk.text();
const encodedChunk = converter.encode('data: ' + JSON.stringify({ response: chunkText }) + '\n\n');
controller.enqueue(encodedChunk);
}
const doneChunk = converter.encode('data: [DONE]');
controller.enqueue(doneChunk);
controller.close();
},
}),
);
return response;
}

View file

@ -1,27 +0,0 @@
import { Request } from '@cloudflare/workers-types';
import { type CloudflareVectorizeStore } from '@langchain/cloudflare';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { GenerativeModel } from '@google/generative-ai';
import { seededRandom } from '../util';
export async function DELETE(request: Request, store: CloudflareVectorizeStore, _: OpenAIEmbeddings, m: GenerativeModel, env: Env) {
const { searchParams } = new URL(request.url);
const websiteUrl = searchParams.get('websiteUrl');
const user = searchParams.get('user');
if (!websiteUrl || !user) {
return new Response(JSON.stringify({ message: 'Invalid Request, need websiteUrl and user' }), { status: 400 });
}
const ourID = `${websiteUrl}-${user}`;
const uuid = await env.KV.get(ourID);
if (!uuid) {
return new Response(JSON.stringify({ message: 'Document not found' }), { status: 404 });
}
await store.delete({ ids: [uuid] });
return new Response(JSON.stringify({ message: 'Document deleted' }), { status: 200 });
}

View file

@ -1,61 +0,0 @@
import { Request } from '@cloudflare/workers-types';
import { type CloudflareVectorizeStore } from '@langchain/cloudflare';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { GenerativeModel } from '@google/generative-ai';
import { seededRandom } from '../util';
export async function POST(request: Request, store: CloudflareVectorizeStore, _: OpenAIEmbeddings, m: GenerativeModel, env: Env) {
const body = (await request.json()) as {
pageContent: string;
title?: string;
description?: string;
space?: string;
url: string;
user: string;
};
if (!body.pageContent || !body.url) {
return new Response(JSON.stringify({ message: 'Invalid Page Content' }), { status: 400 });
}
const { searchParams } = new URL(request.url);
const uniqueUrl = searchParams.get('uniqueUrl');
const toBeDeleted = `${uniqueUrl}-${body.user}`;
const tbduuid = await env.KV.get(toBeDeleted);
if (tbduuid) {
await store.delete({ ids: [tbduuid] });
}
// TODO: FIX THIS,BUT TEMPERORILY TRIM page content to 1000 words
body.pageContent = body.pageContent.split(' ').slice(0, 1000).join(' ');
const newPageContent = `Title: ${body.title}\nDescription: ${body.description}\nURL: ${body.url}\nContent: ${body.pageContent}`;
const ourID = `${body.url}-${body.user}`;
const random = seededRandom(ourID);
const uuid = random().toString(36).substring(2, 15) + random().toString(36).substring(2, 15);
await env.KV.put(uuid, ourID);
await store.addDocuments(
[
{
pageContent: newPageContent,
metadata: {
title: body.title?.slice(0, 50) ?? '',
description: body.description ?? '',
space: body.space ?? '',
url: body.url,
user: body.user,
},
},
],
{
ids: [uuid],
},
);
return new Response(JSON.stringify({ message: 'Document Added' }), { status: 200 });
}

View file

@ -1,33 +0,0 @@
import { GenerativeModel } from '@google/generative-ai';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { CloudflareVectorizeStore } from '@langchain/cloudflare';
import { Request } from '@cloudflare/workers-types';
import puppeteer from '@cloudflare/puppeteer';
// TODO: THIS DOESN'T WORK PROPERLY. FOR EG, FOR THIS URL https://dev.to/challenges/cloudflare, IT DOESN'T RETURN FULL CONTENT
export async function GET(request: Request, _: CloudflareVectorizeStore, embeddings: OpenAIEmbeddings, model: GenerativeModel, env?: Env) {
const { searchParams } = new URL(request.url);
let url = searchParams.get('url');
let img: Buffer;
if (url) {
url = new URL(url).toString(); // normalize
const browser = await puppeteer.launch(env?.MYBROWSER);
const page = await browser.newPage();
await page.goto(url);
await page.waitForSelector('body');
const contentElement = await page.$('body');
const content = await page.evaluate((element) => element.innerText, contentElement);
await browser.close();
return new Response(content, {
headers: {
'content-type': 'text/html',
},
});
} else {
return new Response('Please add an ?url=https://example.com/ parameter');
}
}

View file

@ -1,94 +0,0 @@
import { GenerativeModel } from '@google/generative-ai';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { CloudflareVectorizeStore } from '@langchain/cloudflare';
import { Request } from '@cloudflare/workers-types';
export async function GET(request: Request, _: CloudflareVectorizeStore, embeddings: OpenAIEmbeddings, model: GenerativeModel, env?: Env) {
const queryparams = new URL(request.url).searchParams;
const query = queryparams.get('q');
const topK = parseInt(queryparams.get('topK') ?? '5');
const user = queryparams.get('user');
const space = queryparams.get('space');
const sourcesOnly = queryparams.get('sourcesOnly') ?? 'false';
if (!user) {
return new Response(JSON.stringify({ message: 'Invalid User' }), { status: 400 });
}
if (!query) {
return new Response(JSON.stringify({ message: 'Invalid Query' }), { status: 400 });
}
const filter: VectorizeVectorMetadataFilter = {
user,
};
if (space) {
filter.space;
}
const queryAsVector = await embeddings.embedQuery(query);
const resp = await env!.VECTORIZE_INDEX.query(queryAsVector, {
topK,
filter,
});
if (resp.count === 0) {
return new Response(JSON.stringify({ message: 'No Results Found' }), { status: 404 });
}
const highScoreIds = resp.matches.filter(({ score }) => score > 0.3).map(({ id }) => id);
if (sourcesOnly === 'true') {
const idsAsStrings = highScoreIds.map(String);
const storedContent = await Promise.all(
idsAsStrings.map(async (id) => {
const stored = await env!.KV.get(id);
if (stored) {
return stored;
}
return id;
}),
);
console.log(storedContent);
return new Response(JSON.stringify({ ids: storedContent }), { status: 200 });
}
const vec = await env!.VECTORIZE_INDEX.getByIds(highScoreIds);
if (vec.length === 0 || !vec[0].metadata) {
return new Response(JSON.stringify({ message: 'No Results Found' }), { status: 400 });
}
const preparedContext = vec
.slice(0, 3)
.map(
({ metadata }) =>
`Website title: ${metadata!.title}\nDescription: ${metadata!.description}\nURL: ${metadata!.url}\nContent: ${metadata!.text}`,
)
.join('\n\n');
const prompt = `You are an agent that summarizes a page based on the query. Be direct and concise, don't say 'based on the context'.\n\n Context:\n${preparedContext} \nAnswer this question based on the context. Question: ${query}\nAnswer:`;
const output = await model.generateContentStream(prompt);
const response = new Response(
new ReadableStream({
async start(controller) {
const converter = new TextEncoder();
for await (const chunk of output.stream) {
const chunkText = await chunk.text();
const encodedChunk = converter.encode('data: ' + JSON.stringify({ response: chunkText }) + '\n\n');
controller.enqueue(encodedChunk);
}
const doneChunk = converter.encode('data: [DONE]');
controller.enqueue(doneChunk);
controller.close();
},
}),
);
return response;
}

View file

@ -1,95 +0,0 @@
import { CloudflareVectorizeStore } from '@langchain/cloudflare';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { seededRandom } from '../util';
export const queue = async (batch: MessageBatch, env: Env): Promise<void> => {
const messages = batch.messages[0].body as TweetData[];
const token = messages[0].saveToUser;
if (!token) {
return;
}
const limits = (await fetch('https://supermemory.dhr.wtf/api/getCount', {
headers: {
Authorization: `Bearer ${token}`,
},
}).then((res) => res.json())) as { tweetsLimit: number; tweetsCount: number; user: string };
if (messages.length > limits.tweetsLimit - limits.tweetsCount) {
messages.splice(limits.tweetsLimit - limits.tweetsCount);
}
if (messages.length === 0) {
return;
}
const embeddings = new OpenAIEmbeddings({
apiKey: env.OPENAI_API_KEY,
modelName: 'text-embedding-3-small',
});
const store = new CloudflareVectorizeStore(embeddings, {
index: env.VECTORIZE_INDEX,
});
const collectedDocsUUIDs: {
document: {
pageContent: string;
metadata: { title: string; description: string; space?: string; url: string; user: string };
id: string;
};
}[] = [];
messages.forEach(async (message) => {
const ourID = `${message.postUrl}-${limits.user}`;
const random = seededRandom(ourID);
const uuid = random().toString(36).substring(2, 15) + random().toString(36).substring(2, 15);
await env.KV.put(uuid, ourID);
const pageContent = `This is a tweet from ${message.authorName}, it was posted on ${message.time}. The tweet reads: ${message.tweetText}`;
collectedDocsUUIDs.push({
document: {
pageContent,
metadata: {
title: 'Twitter Bookmark',
description: '',
url: message.postUrl,
user: limits.user,
},
id: uuid,
},
});
});
console.log(collectedDocsUUIDs);
await store.addDocuments(
collectedDocsUUIDs.map(({ document }) => document),
{
ids: collectedDocsUUIDs.map(({ document }) => document.id),
},
);
console.log(token);
const res = await fetch('https://supermemory.dhr.wtf/api/addTweetsToDb', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(messages),
});
console.log(res.status, res.statusText);
if (res.status !== 200) {
console.log(await res.json());
console.error('Error adding tweets to db');
}
console.log(`consumed from our queue: ${messages}`);
};

View file

@ -1,19 +0,0 @@
import { Request } from '@cloudflare/workers-types';
import { type CloudflareVectorizeStore } from '@langchain/cloudflare';
import { OpenAIEmbeddings } from '../OpenAIEmbedder';
import { GenerativeModel } from '@google/generative-ai';
import { seededRandom } from '../util';
// TODO: Waiting for cloudflare to implement tojson so i can get all IDS for that user and delete them
export async function DELETE(request: Request, store: CloudflareVectorizeStore, _: OpenAIEmbeddings, m: GenerativeModel, env: Env) {
const { searchParams } = new URL(request.url);
const user = searchParams.get('user');
console.log(store.toJSONNotImplemented());
// for (const match of matches.matches) {
// await store.delete({ ids: [match.id] });
// }
return new Response(JSON.stringify({ message: 'Document deleted' }), { status: 200 });
}

View file

@ -0,0 +1,48 @@
import { z } from "zod";
export type Env = {
VECTORIZE_INDEX: VectorizeIndex;
AI: Fetcher;
SECURITY_KEY: string;
OPENAI_API_KEY: string;
GOOGLE_AI_API_KEY: string;
MY_QUEUE: Queue<TweetData[]>;
KV: KVNamespace;
MYBROWSER: unknown;
ANTHROPIC_API_KEY: string;
};
export interface TweetData {
tweetText: string;
postUrl: string;
authorName: string;
handle: string;
time: string;
saveToUser: string;
}
export const contentObj = z.object({
role: z.string(),
parts: z
.array(
z.object({
text: z.string(),
}),
)
.transform((val) => val.map((v) => v.text))
.optional(),
content: z.string().optional(),
});
export const chatObj = z.object({
chatHistory: z.array(contentObj).optional(),
});
export const vectorObj = z.object({
pageContent: z.string(),
title: z.string().optional(),
description: z.string().optional(),
space: z.string().optional(),
url: z.string(),
user: z.string(),
});

View file

@ -1,7 +0,0 @@
export function seededRandom(seed: string) {
let x = [...seed].reduce((acc, cur) => acc + cur.charCodeAt(0), 0);
return () => {
x = (x * 9301 + 49297) % 233280;
return x / 233280;
};
}

View file

@ -0,0 +1,43 @@
interface OpenAIEmbeddingsParams {
apiKey: string;
modelName: string;
}
export class OpenAIEmbeddings {
private apiKey: string;
private modelName: string;
constructor({ apiKey, modelName }: OpenAIEmbeddingsParams) {
this.apiKey = apiKey;
this.modelName = modelName;
}
async embedDocuments(texts: string[]): Promise<number[][]> {
const responses = await Promise.all(
texts.map((text) => this.embedQuery(text)),
);
return responses;
}
async embedQuery(text: string): Promise<number[]> {
const response = await fetch("https://api.openai.com/v1/embeddings", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
input: text,
model: this.modelName,
}),
});
const data = (await response.json()) as {
data: {
embedding: number[];
}[];
};
return data.data[0].embedding;
}
}

View file

@ -0,0 +1,44 @@
import nlp from "compromise";
export default function chunkText(
text: string,
maxChunkSize: number,
overlap: number = 0.2,
): string[] {
const sentences = nlp(text).sentences().out("array");
const chunks = [];
let currentChunk: string[] = [];
let currentSize = 0;
for (let i = 0; i < sentences.length; i++) {
const sentence = sentences[i];
currentChunk.push(sentence);
currentSize += sentence.length;
if (currentSize >= maxChunkSize) {
// Calculate overlap
const overlapSize = Math.floor(currentChunk.length * overlap);
const chunkText = currentChunk.join(" ");
chunks.push({
text: chunkText,
start: i - currentChunk.length + 1,
end: i,
});
// Prepare the next chunk with overlap
currentChunk = currentChunk.slice(-overlapSize);
currentSize = currentChunk.reduce((sum, s) => sum + s.length, 0);
}
}
if (currentChunk.length > 0) {
const chunkText = currentChunk.join(" ");
chunks.push({
text: chunkText,
start: sentences.length - currentChunk.length,
end: sentences.length,
});
}
return chunks.map((chunk) => chunk.text);
}

View file

@ -0,0 +1,18 @@
import { MersenneTwister19937, integer } from "random-js";
function hashString(seed: string) {
let hash = 0;
for (let i = 0; i < seed.length; i++) {
const char = seed.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
export function seededRandom(seed: string) {
const seedHash = hashString(seed);
const engine = MersenneTwister19937.seed(seedHash);
return () =>
integer(0, Number.MAX_SAFE_INTEGER)(engine) / Number.MAX_SAFE_INTEGER;
}

View file

@ -1,103 +1,6 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "es2022" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": [
"@cloudflare/workers-types/2023-07-01"
] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */,
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
"noEmit": true /* Disable emitting files from a compilation. */,
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
// "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
"compilerOptions": {
"lib": ["ES2020"],
"types": ["jest", "@cloudflare/workers-types"]
}
}

View file

@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import honox from "honox/vite";
export default defineConfig({
plugins: [honox()],
});

View file

@ -1,29 +1,26 @@
name = "cf-ai-backend"
main = "src/index.ts"
name = "new-cf-ai-backend"
main = "src/server.ts"
compatibility_date = "2024-02-23"
compatibility_flags = ['nodejs_compat']
node_compat = true
[[vectorize]]
binding = "VECTORIZE_INDEX"
index_name = "supermem-vector-1"
index_name = "supermem-vector"
[ai]
binding = "AI"
[[queues.producers]]
queue = "batch-vector-queue"
binding = "MY_QUEUE"
# [[queues.producers]]
# queue = "batch-vector-queue"
# binding = "MY_QUEUE"
[[queues.consumers]]
queue = "batch-vector-queue"
# [[queues.consumers]]
# queue = "batch-vector-queue"
[[kv_namespaces]]
binding = "KV"
id = "37a90353da63401e84e20e71165531d0"
preview_id = "c58b6202814f4224acea97627d0c18aa"
[browser]
binding = "MYBROWSER"
[placement]
mode = "smart"

View file

@ -1,20 +1,18 @@
"use client";
import SessionProviderWrapper from "@/components/dev/SessionProviderWrapper";
import Main from "@/components/Main";
import Sidebar from "@/components/Sidebar/index";
import { SessionProvider } from "next-auth/react";
import { useState } from "react";
export default function Content({ jwt }: { jwt: string }) {
const [selectedItem, setSelectedItem] = useState<string | null>(null);
return (
<div>
<SessionProvider>
<div className="flex w-screen">
<Sidebar jwt={jwt} selectChange={setSelectedItem} />
<Main sidebarOpen={selectedItem !== null} />
</div>
</SessionProvider>
</div>
<SessionProviderWrapper>
<div className="flex w-screen">
<Sidebar jwt={jwt} selectChange={setSelectedItem} />
<Main sidebarOpen={selectedItem !== null} />
</div>
</SessionProviderWrapper>
);
}

View file

@ -53,7 +53,10 @@ export default function RootLayout({
></script>
</head>
<body className={inter.className}>
<div vaul-drawer-wrapper="" className="min-w-screen overflow-x-hidden">
<div
vaul-drawer-wrapper=""
className="min-w-screen overflow-x-hidden text-black"
>
{children}
</div>
</body>

View file

@ -12,6 +12,8 @@ import { redirect } from "next/navigation";
import { fetchContentForSpace, fetchFreeMemories } from "@/actions/db";
import { MemoryProvider } from "@/contexts/MemoryContext";
import Content from "./content";
import Main from "@/components/Main";
import { TailwindIndicator } from "@/components/dev/tailwindindicator";
export const runtime = "edge";
@ -88,6 +90,7 @@ export default async function Home() {
cachedMemories={contents}
>
<Content jwt={token} />
<TailwindIndicator />
{/* <MessagePoster jwt={token} /> */}
</MemoryProvider>
);

View file

@ -14,8 +14,8 @@ export const MemoryIcon: React.FC<React.SVGAttributes<SVGElement>> = (
height="43.0286"
rx="5.5"
transform="rotate(-12 0.40697 8.52821)"
fill="var(--gray-5)"
stroke="var(--gray-10)"
fill="white"
stroke="black"
/>
<rect
x="20.8257"
@ -23,8 +23,8 @@ export const MemoryIcon: React.FC<React.SVGAttributes<SVGElement>> = (
width="43"
height="43"
rx="5.5"
fill="var(--gray-5)"
stroke="var(--gray-10)"
fill="white"
stroke="black"
/>
<rect
x="47.6965"
@ -33,8 +33,8 @@ export const MemoryIcon: React.FC<React.SVGAttributes<SVGElement>> = (
height="43.0286"
rx="5.5"
transform="rotate(15 47.6965 -0.612372)"
fill="var(--gray-5)"
stroke="var(--gray-10)"
fill="white"
stroke="black"
/>
</svg>
);

View file

@ -20,7 +20,7 @@ export const MemoryWithImage: React.FC<
width="72.207"
height="72.207"
rx="10"
fill="var(--gray-4)"
fill="white"
/>
</g>
<rect
@ -116,7 +116,7 @@ export const MemoryWithImages2: React.FC<
height="72.207"
rx="10"
transform="rotate(-24.1922 7 51.5427)"
fill="var(--gray-4)"
fill="#F4F3F2"
/>
</g>
<rect
@ -135,7 +135,7 @@ export const MemoryWithImages2: React.FC<
height="72.207"
rx="10"
transform="rotate(10.2301 59.9409 42.2124)"
fill="var(--gray-4)"
fill="#ffffff"
/>
</g>
<rect
@ -294,7 +294,8 @@ export const MemoryWithImages3: React.FC<
height="72.207"
rx="10"
transform="rotate(14.9009 53.5242 12)"
fill="var(--gray-4)"
fill="white"
className="shadow-md"
/>
</g>
<rect
@ -313,7 +314,8 @@ export const MemoryWithImages3: React.FC<
height="72.207"
rx="10"
transform="rotate(-24.1922 7 51.5427)"
fill="var(--gray-4)"
fill="#F4F3F2"
className="shadow-md"
/>
</g>
<rect
@ -332,7 +334,8 @@ export const MemoryWithImages3: React.FC<
height="72.207"
rx="10"
transform="rotate(10.2301 59.9409 42.2124)"
fill="var(--gray-4)"
fill="#E7E5E4"
className="shadow-md"
/>
</g>
<rect

View file

@ -1,8 +1,12 @@
import { useRef, useState } from "react";
import { Drawer, DrawerContent, DrawerOverlay, DrawerTrigger } from "./ui/drawer";
import {
Drawer,
DrawerContent,
DrawerOverlay,
DrawerTrigger,
} from "./ui/drawer";
import { cn } from "@/lib/utils";
import { ProfileTab } from "./Sidebar";
import { SettingsTab } from "./Sidebar/SettingsTab";
import { useSession } from "next-auth/react";
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@ -10,27 +14,26 @@ export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
}
export function ProfileDrawer({ className, hide = false, ...props }: Props) {
const { data: session } = useSession();
const { data: session } = useSession();
return (
<Drawer
snapPoints={[0.9]}
shouldScaleBackground={false}
>
<DrawerTrigger>
<img src={session?.user?.image ?? "/icons/white_without_bg.png"} className="w-10 h-10 rounded-full" />
</DrawerTrigger>
<Drawer snapPoints={[0.9]} shouldScaleBackground={false}>
<DrawerTrigger>
<img
src={session?.user?.image ?? "/icons/white_without_bg.png"}
className="h-10 w-10 rounded-full"
/>
</DrawerTrigger>
<DrawerContent
overlay={false}
className={cn(
"border-rgray-6 z-[101] bg-rgray-3 DrawerContent data-[expanded=true]:bg-rgray-3 h-full w-screen border transition-[background] focus-visible:outline-none",
"border-rgray-6 DrawerContent data-[expanded=true]:bg-rgray-3 z-[101] h-full w-screen border bg-white transition-[background] focus-visible:outline-none",
hide ? "hidden" : "",
)}
>
<div className="w-full h-[85vh] overflow-y-auto">
<ProfileTab open={true} />
</div>
<div className="h-[85vh] w-full overflow-y-auto">
<SettingsTab open={true} />
</div>
</DrawerContent>
</Drawer>
);

View file

@ -18,19 +18,29 @@ import { cleanUrl } from "@/lib/utils";
import { motion } from "framer-motion";
import { getMetaData } from "@/server/helpers";
export function AddMemoryPage({ closeDialog, defaultSpaces, onAdd }: { closeDialog: () => void, defaultSpaces?: number[], onAdd?: (addedData: StoredContent) => void }) {
export function AddMemoryPage({
closeDialog,
defaultSpaces,
onAdd,
}: {
closeDialog: () => void;
defaultSpaces?: number[];
onAdd?: (addedData: StoredContent) => void;
}) {
const { addMemory } = useMemory();
const [loading, setLoading] = useState(false);
const [url, setUrl] = useState("");
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>(defaultSpaces ?? []);
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>(
defaultSpaces ?? [],
);
return (
<div className="max-w-[80vw] w-[80vw] md:w-[40vw]">
<div className="w-[80vw] max-w-[80vw] md:w-[40vw]">
<DialogHeader>
<DialogTitle>Add a web page to memory</DialogTitle>
<DialogDescription>
This will fetch the content of the web page and add it to the memory
This will fetch the content of the web page and add it to the memory
</DialogDescription>
</DialogHeader>
<Label className="mt-5 block">URL</Label>
@ -38,7 +48,7 @@ export function AddMemoryPage({ closeDialog, defaultSpaces, onAdd }: { closeDial
placeholder="Enter the URL of the page"
type="url"
data-modal-autofocus
className="bg-rgray-4 mt-2 w-full disabled:cursor-not-allowed disabled:opacity-70"
className="mt-2 w-full disabled:cursor-not-allowed disabled:opacity-70"
value={url}
onChange={(e) => setUrl(e.target.value)}
disabled={loading}
@ -47,7 +57,7 @@ export function AddMemoryPage({ closeDialog, defaultSpaces, onAdd }: { closeDial
<FilterSpaces
selectedSpaces={selectedSpacesId}
setSelectedSpaces={setSelectedSpacesId}
className="hover:bg-rgray-5 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70"
className="mr-auto bg-white/5 hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-70"
name={"Spaces"}
disabled={loading}
/>
@ -69,10 +79,10 @@ export function AddMemoryPage({ closeDialog, defaultSpaces, onAdd }: { closeDial
},
selectedSpacesId,
);
if (data) onAdd?.(data.memory)
if (data) onAdd?.(data.memory);
closeDialog();
}}
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
className="bg-rgray-4 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
<motion.div
initial={{ x: "-50%", y: "-100%" }}
@ -90,7 +100,7 @@ export function AddMemoryPage({ closeDialog, defaultSpaces, onAdd }: { closeDial
</button>
<DialogClose
disabled={loading}
className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
className="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
Cancel
</DialogClose>
@ -99,10 +109,20 @@ export function AddMemoryPage({ closeDialog, defaultSpaces, onAdd }: { closeDial
);
}
export function NoteAddPage({ closeDialog, defaultSpaces, onAdd }: { closeDialog: () => void, defaultSpaces?: number[], onAdd?: (addedData: StoredContent) => void }) {
export function NoteAddPage({
closeDialog,
defaultSpaces,
onAdd,
}: {
closeDialog: () => void;
defaultSpaces?: number[];
onAdd?: (addedData: StoredContent) => void;
}) {
const { addMemory } = useMemory();
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>(defaultSpaces ?? []);
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>(
defaultSpaces ?? [],
);
const inputRef = useRef<HTMLInputElement>(null);
const [name, setName] = useState("");
@ -137,7 +157,7 @@ export function NoteAddPage({ closeDialog, defaultSpaces, onAdd }: { closeDialog
<Input
ref={inputRef}
data-error="false"
className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400"
className="w-full border-none p-0 text-xl ring-0 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400"
placeholder="Title of the note"
data-modal-autofocus
value={name}
@ -152,7 +172,7 @@ export function NoteAddPage({ closeDialog, defaultSpaces, onAdd }: { closeDialog
setContent(editor.storage.markdown.getMarkdown());
}}
extensions={[Markdown]}
className="novel-editor w-full bg-rgray-4 border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] md:w-[50vw] overflow-y-auto rounded-lg border [&>div>div]:p-5"
className="novel-editor border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-full overflow-y-auto rounded-lg border bg-white md:w-[50vw] [&>div>div]:p-5"
/>
<DialogFooter>
<FilterSpaces
@ -176,13 +196,13 @@ export function NoteAddPage({ closeDialog, defaultSpaces, onAdd }: { closeDialog
},
selectedSpacesId,
).then((data) => {
if (data?.memory) onAdd?.(data.memory)
closeDialog()
});
if (data?.memory) onAdd?.(data.memory);
closeDialog();
});
}
}}
disabled={loading}
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
className="hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md bg-[#F4F3F2] px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
<motion.div
initial={{ x: "-50%", y: "-100%" }}
@ -210,7 +230,13 @@ export function NoteAddPage({ closeDialog, defaultSpaces, onAdd }: { closeDialog
);
}
export function SpaceAddPage({ closeDialog, onAdd }: { closeDialog: () => void, onAdd?: (addedData: StoredSpace) => void }) {
export function SpaceAddPage({
closeDialog,
onAdd,
}: {
closeDialog: () => void;
onAdd?: (addedData: StoredSpace) => void;
}) {
const { addSpace } = useMemory();
const inputRef = useRef<HTMLInputElement>(null);
@ -256,7 +282,7 @@ export function SpaceAddPage({ closeDialog, onAdd }: { closeDialog: () => void,
value={name}
disabled={loading}
onChange={(e) => setName(e.target.value)}
className="bg-rgray-4 mt-2 w-full placeholder:transition placeholder:duration-500 data-[error=true]:placeholder:text-red-400 focus-visible:data-[error=true]:ring-red-500/10"
className="mt-2 w-full placeholder:transition placeholder:duration-500 data-[error=true]:placeholder:text-red-400 focus-visible:data-[error=true]:ring-red-500/10"
/>
{selected.length > 0 && (
<>
@ -279,7 +305,7 @@ export function SpaceAddPage({ closeDialog, onAdd }: { closeDialog: () => void,
selected={selected}
setSelected={setSelected}
disabled={loading}
className="hover:bg-rgray-4 focus-visible:bg-rgray-4 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70"
className="mr-auto bg-white/5 hover:hover:bg-slate-100 focus-visible:hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-70"
>
<Plus className="h-5 w-5" />
Memory
@ -293,13 +319,13 @@ export function SpaceAddPage({ closeDialog, onAdd }: { closeDialog: () => void,
name,
selected.map((s) => s.id),
).then((data) => {
if (data) onAdd?.(data.space)
closeDialog()
});
if (data) onAdd?.(data.space);
closeDialog();
});
}
}}
disabled={loading}
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
className="bg-rgray-4 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition hover:hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-70"
>
<motion.div
initial={{ x: "-50%", y: "-100%" }}
@ -317,7 +343,7 @@ export function SpaceAddPage({ closeDialog, onAdd }: { closeDialog: () => void,
</button>
<DialogClose
disabled={loading}
className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
className="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
Cancel
</DialogClose>
@ -338,15 +364,17 @@ export function MemorySelectedItem({
<div className="hover:bg-rgray-4 focus-within-bg-rgray-4 flex w-full items-center justify-start gap-2 rounded-md p-2 px-3 text-sm [&:hover_[data-icon]]:block [&:hover_img]:hidden">
<button
onClick={onRemove}
className="m-0 h-5 w-5 p-0 [&:focus-visible>[data-icon]]:block [&:focus-visible>img]:hidden focus-visible:outline-none focus-visible:ring-2 ring-rgray-7 rounded-sm ring-offset-2 ring-offset-rgray-3"
className="ring-rgray-7 ring-offset-rgray-3 m-0 h-5 w-5 rounded-sm p-0 ring-offset-2 focus-visible:outline-none focus-visible:ring-2 [&:focus-visible>[data-icon]]:block [&:focus-visible>img]:hidden"
>
<img
src={
type === "note" ? "/note.svg" : image ?? "/icons/logo_without_bg.png"
}
className="h-5 w-5"
/>
<X data-icon className="h-5 w-5 hidden scale-90" />
<img
src={
type === "note"
? "/note.svg"
: image ?? "/icons/logo_without_bg.png"
}
className="h-5 w-5"
/>
<X data-icon className="hidden h-5 w-5 scale-90" />
</button>
<span>{title}</span>
<span className="ml-auto block opacity-50">
@ -357,17 +385,17 @@ export function MemorySelectedItem({
}
export function AddExistingMemoryToSpace({
space,
closeDialog,
fromSpaces,
notInSpaces,
onAdd
}: {
space: { title: string, id: number },
closeDialog: () => void,
fromSpaces?: number[],
notInSpaces?: number[],
onAdd?: () => void;
space,
closeDialog,
fromSpaces,
notInSpaces,
onAdd,
}: {
space: { title: string; id: number };
closeDialog: () => void;
fromSpaces?: number[];
notInSpaces?: number[];
onAdd?: () => void;
}) {
const { addMemoriesToSpace } = useMemory();
@ -379,9 +407,9 @@ export function AddExistingMemoryToSpace({
<div className="w-[80vw] md:w-[40vw]">
<DialogHeader>
<DialogTitle>Add an existing memory to {space.title}</DialogTitle>
<DialogDescription>
Pick the memories you want to add to this space
</DialogDescription>
<DialogDescription>
Pick the memories you want to add to this space
</DialogDescription>
</DialogHeader>
{selected.length > 0 && (
<>
@ -404,8 +432,8 @@ export function AddExistingMemoryToSpace({
selected={selected}
setSelected={setSelected}
disabled={loading}
fromSpaces={fromSpaces}
notInSpaces={notInSpaces}
fromSpaces={fromSpaces}
notInSpaces={notInSpaces}
className="hover:bg-rgray-4 focus-visible:bg-rgray-4 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70"
>
<Plus className="h-5 w-5" />
@ -414,11 +442,14 @@ export function AddExistingMemoryToSpace({
<button
type={undefined}
onClick={() => {
setLoading(true);
addMemoriesToSpace(space.id, selected.map(i => i.id)).then(() => {
onAdd?.()
closeDialog();
});
setLoading(true);
addMemoriesToSpace(
space.id,
selected.map((i) => i.id),
).then(() => {
onAdd?.();
closeDialog();
});
}}
disabled={loading}
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
@ -439,7 +470,7 @@ export function AddExistingMemoryToSpace({
</button>
<DialogClose
disabled={loading}
className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
className="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
Cancel
</DialogClose>

View file

@ -1,9 +1,5 @@
import { Editor } from "novel";
import {
DialogClose,
DialogFooter,
} from "../ui/dialog";
import { DialogClose, DialogFooter } from "../ui/dialog";
import { Input } from "../ui/input";
import { Markdown } from "tiptap-markdown";
import { useEffect, useRef, useState } from "react";
@ -16,11 +12,18 @@ import { fetchContent } from "@/actions/db";
import { isArraysEqual } from "@/lib/utils";
import DeleteConfirmation from "./DeleteConfirmation";
export function NoteEdit({ memory, closeDialog, onDelete }: { memory: StoredContent, closeDialog: () => any, onDelete?: () => void }) {
export function NoteEdit({
memory,
closeDialog,
onDelete,
}: {
memory: StoredContent;
closeDialog: () => any;
onDelete?: () => void;
}) {
const { updateMemory, deleteMemory } = useMemory();
const [initialSpaces, setInitialSpaces] = useState<number[]>([])
const [initialSpaces, setInitialSpaces] = useState<number[]>([]);
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
@ -51,21 +54,21 @@ export function NoteEdit({ memory, closeDialog, onDelete }: { memory: StoredCont
return true;
}
useEffect(() => {
fetchContent(memory.id).then((data) => {
if (data?.spaces) {
setInitialSpaces(data.spaces)
setSelectedSpacesId(data.spaces)
}
})
}, [])
useEffect(() => {
fetchContent(memory.id).then((data) => {
if (data?.spaces) {
setInitialSpaces(data.spaces);
setSelectedSpacesId(data.spaces);
}
});
}, []);
return (
<div>
<Input
ref={inputRef}
data-error="false"
className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400"
className="w-full border-none p-0 text-xl ring-0 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400"
placeholder="Title of the note"
value={name}
disabled={loading}
@ -79,69 +82,70 @@ export function NoteEdit({ memory, closeDialog, onDelete }: { memory: StoredCont
setContent(editor.storage.markdown.getMarkdown());
}}
extensions={[Markdown]}
className="novel-editor bg-rgray-4 border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border [&>div>div]:p-5"
className="novel-editor border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border bg-white [&>div>div]:p-5"
/>
<DialogFooter>
<FilterSpaces
selectedSpaces={selectedSpacesId}
setSelectedSpaces={setSelectedSpacesId}
className="hover:bg-rgray-5 mr-auto bg-white/5"
className="mr-auto bg-white hover:bg-slate-100"
name={"Spaces"}
/>
<DeleteConfirmation onDelete={() => {
deleteMemory(memory.id)
onDelete?.()
}}>
<button
type={undefined}
disabled={loading}
className="focus-visible:bg-red-100 focus-visible:text-red-400 dark:focus-visible:bg-red-100/10 hover:bg-red-100 dark:hover:bg-red-100/10 hover:text-red-400 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
<Trash className="w-5 h-5" />
</button>
</DeleteConfirmation>
<DeleteConfirmation
onDelete={() => {
deleteMemory(memory.id);
onDelete?.();
}}
>
<button
type={undefined}
disabled={loading}
className="rounded-md px-3 py-2 ring-transparent transition hover:bg-red-100 hover:text-red-400 focus-visible:bg-red-100 focus-visible:text-red-400 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
<Trash className="h-5 w-5" />
</button>
</DeleteConfirmation>
<button
onClick={() => {
if (check()) {
setLoading(true);
console.log(
{
title: name === memory.title ? undefined : name,
content: content === memory.content ? undefined : content,
spaces: isArraysEqual(initialSpaces, selectedSpacesId) ? undefined : selectedSpacesId,
},
)
updateMemory(
memory.id,
{
title: name === memory.title ? undefined : name,
content: content === memory.content ? undefined : content,
spaces: isArraysEqual(initialSpaces, selectedSpacesId) ? undefined : selectedSpacesId,
},
).then(closeDialog);
console.log({
title: name === memory.title ? undefined : name,
content: content === memory.content ? undefined : content,
spaces: isArraysEqual(initialSpaces, selectedSpacesId)
? undefined
: selectedSpacesId,
});
updateMemory(memory.id, {
title: name === memory.title ? undefined : name,
content: content === memory.content ? undefined : content,
spaces: isArraysEqual(initialSpaces, selectedSpacesId)
? undefined
: selectedSpacesId,
}).then(closeDialog);
}
}}
disabled={loading}
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
className="focus-visible:ring-rgray-7 relative rounded-md bg-white px-4 py-2 ring-transparent transition hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
<motion.div
initial={{ x: "-50%", y: "-100%" }}
animate={loading && { y: "-50%", x: "-50%", opacity: 1 }}
className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0"
>
<Loader className="text-rgray-11 h-5 w-5 animate-spin" />
<Loader className="h-5 w-5 animate-spin" />
</motion.div>
<motion.div
initial={{ y: "0%" }}
animate={loading && { opacity: 0, y: "30%" }}
>
Save
Save
</motion.div>
</button>
<DialogClose
type={undefined}
disabled={loading}
className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
Cancel
</DialogClose>
@ -149,4 +153,3 @@ export function NoteEdit({ memory, closeDialog, onDelete }: { memory: StoredCont
</div>
);
}

View file

@ -64,88 +64,90 @@ export function FilterSpaces({
}, [open]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type={undefined}
data-state-on={open}
className={cn(
"text-rgray-11/70 on:bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none",
className,
)}
{...props}
>
<SpaceIcon className="mr-1 h-5 w-5" />
{name}
<ChevronsUpDown className="h-4 w-4" />
<div
data-state-on={selectedSpaces.length > 0}
className="on:flex text-rgray-11 border-rgray-6 bg-rgray-2 absolute left-0 top-0 hidden aspect-[1] h-4 w-4 -translate-x-1/3 -translate-y-1/3 items-center justify-center rounded-full border text-center text-[9px]"
>
{selectedSpaces.length}
</div>
</button>
</PopoverTrigger>
<PopoverContent
align={align}
side={side}
className="w-[200px] p-0"
onCloseAutoFocus={e => e.preventDefault()}
>
<Command
filter={(val, search) =>
spaces
.find((s) => s.id.toString() === val)
?.name.toLowerCase()
.includes(search.toLowerCase().trim())
? 1
: 0
}
>
<CommandInput placeholder="Filter spaces..." />
<CommandList asChild>
<motion.div layoutScroll>
<CommandEmpty>Nothing found</CommandEmpty>
<CommandGroup>
{sortedSpaces.map((space) => (
<CommandItem
key={space.id}
value={space.id.toString()}
onSelect={(val) => {
setSelectedSpaces((prev: number[]) =>
prev.includes(parseInt(val))
? prev.filter((v) => v !== parseInt(val))
: [...prev, parseInt(val)],
);
}}
asChild
>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.05 } }}
transition={{ duration: 0.15 }}
layout
layoutId={`space-combobox-${space.id}`}
className="text-rgray-11"
>
<SpaceIcon className="mr-2 h-4 w-4" />
{space.name.length > 10 ? space.name.slice(0, 10) + "..." : space.name}
{selectedSpaces.includes(space.id)}
<Check
data-state-on={selectedSpaces.includes(space.id)}
className={cn(
"on:opacity-100 ml-auto h-4 w-4 opacity-0",
)}
/>
</motion.div>
</CommandItem>
))}
</CommandGroup>
</motion.div>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type={undefined}
data-state-on={open}
className={cn(
"focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none",
className,
)}
{...props}
>
<SpaceIcon className="mr-1 h-5 w-5" />
{name}
<ChevronsUpDown className="h-4 w-4" />
<div
data-state-on={selectedSpaces.length > 0}
className="on:flex text-rgray-11 border-rgray-6 bg-rgray-2 absolute left-0 top-0 hidden aspect-[1] h-4 w-4 -translate-x-1/3 -translate-y-1/3 items-center justify-center rounded-full border text-center text-[9px]"
>
{selectedSpaces.length}
</div>
</button>
</PopoverTrigger>
<PopoverContent
align={align}
side={side}
className="w-[200px] p-0"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<Command
filter={(val, search) =>
spaces
.find((s) => s.id.toString() === val)
?.name.toLowerCase()
.includes(search.toLowerCase().trim())
? 1
: 0
}
>
<CommandInput placeholder="Filter spaces..." />
<CommandList asChild>
<motion.div layoutScroll>
<CommandEmpty>Nothing found</CommandEmpty>
<CommandGroup>
{sortedSpaces.map((space) => (
<CommandItem
key={space.id}
value={space.id.toString()}
onSelect={(val) => {
setSelectedSpaces((prev: number[]) =>
prev.includes(parseInt(val))
? prev.filter((v) => v !== parseInt(val))
: [...prev, parseInt(val)],
);
}}
asChild
>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.05 } }}
transition={{ duration: 0.15 }}
layout
layoutId={`space-combobox-${space.id}`}
className="text-rgray-11"
>
<SpaceIcon className="mr-2 h-4 w-4" />
{space.name.length > 10
? space.name.slice(0, 10) + "..."
: space.name}
{selectedSpaces.includes(space.id)}
<Check
data-state-on={selectedSpaces.includes(space.id)}
className={cn(
"on:opacity-100 ml-auto h-4 w-4 opacity-0",
)}
/>
</motion.div>
</CommandItem>
))}
</CommandGroup>
</motion.div>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
@ -155,8 +157,8 @@ export type FilterMemoriesProps = {
onClose?: () => void;
selected: StoredContent[];
setSelected: React.Dispatch<React.SetStateAction<StoredContent[]>>;
fromSpaces?: number[];
notInSpaces?: number[];
fromSpaces?: number[];
notInSpaces?: number[];
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function FilterMemories({
@ -166,8 +168,8 @@ export function FilterMemories({
onClose,
selected,
setSelected,
fromSpaces,
notInSpaces,
fromSpaces,
notInSpaces,
...props
}: FilterMemoriesProps) {
const { search } = useMemory();
@ -195,9 +197,10 @@ export function FilterMemories({
memories: true,
spaces: false,
},
memoriesRelativeToSpace: {
fromSpaces, notInSpaces
}
memoriesRelativeToSpace: {
fromSpaces,
notInSpaces,
},
});
setSearchResults(results);
setIsSearching(false);
@ -275,7 +278,9 @@ export function FilterMemories({
}
className="mr-2 h-4 w-4"
/>
{(m.title && m.title?.length > 14) ? m.title?.slice(0, 14) + "..." : m.title}
{m.title && m.title?.length > 14
? m.title?.slice(0, 14) + "..."
: m.title}
<Check
data-state-on={
selected.find((i) => i.id === m.id) !== undefined

View file

@ -28,19 +28,16 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { Variant, useAnimate, motion } from "framer-motion";
import { SearchResult, useMemory } from "@/contexts/MemoryContext";
import { SpaceIcon } from "@/assets/Memories";
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
DialogHeader,
DialogFooter,
DialogClose,
} from "../ui/dialog";
import { Dialog, DialogContent } from "../ui/dialog";
import useViewport from "@/hooks/useViewport";
import useTouchHold from "@/hooks/useTouchHold";
import { DialogTrigger } from "@radix-ui/react-dialog";
import { AddExistingMemoryToSpace, AddMemoryPage, MemorySelectedItem, NoteAddPage, SpaceAddPage } from "./AddMemoryDialog";
import {
AddExistingMemoryToSpace,
AddMemoryPage,
NoteAddPage,
SpaceAddPage,
} from "./AddMemoryDialog";
import { ExpandedSpace } from "./ExpandedSpace";
import { StoredContent, StoredSpace } from "@/server/db/schema";
import { useDebounce } from "@/hooks/useDebounce";
@ -48,7 +45,6 @@ import { NoteEdit } from "./EditNoteDialog";
import DeleteConfirmation from "./DeleteConfirmation";
export function MemoriesBar({ isOpen }: { isOpen: boolean }) {
const [parent, enableAnimations] = useAutoAnimate();
const { spaces, deleteSpace, freeMemories, search } = useMemory();
@ -80,17 +76,17 @@ export function MemoriesBar({ isOpen }: { isOpen: boolean }) {
})();
}, [query]);
useEffect(() => {
if (!isOpen) {
setExpandedSpace(null)
}
}, [isOpen])
useEffect(() => {
if (!isOpen) {
setExpandedSpace(null);
}
}, [isOpen]);
if (expandedSpace) {
return (
<ExpandedSpace
spaceId={expandedSpace}
back={() => setExpandedSpace(null)}
back={() => setExpandedSpace(null)}
// close={() => setExpandedSpace(null)}
/>
);
@ -160,30 +156,34 @@ export function MemoriesBar({ isOpen }: { isOpen: boolean }) {
</div>
<div
ref={parent}
className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5"
className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5"
>
{query.trim().length > 0 ? (
<>
{searchResults.map(({ type, space, memory }, i) => (
<>
{type === "memory" && (
<MemoryItem
{...memory!}
key={i}
onDelete={() => {
setSearchResults(prev => prev.filter(i => i.memory?.id !== memory.id))
}}
/>
)}
<MemoryItem
{...memory!}
key={i}
onDelete={() => {
setSearchResults((prev) =>
prev.filter((i) => i.memory?.id !== memory.id),
);
}}
/>
)}
{type === "space" && (
<SpaceItem
{...space!}
key={i}
onDelete={() => {
setSearchResults(prev => prev.filter(i => i.space?.id !== space.id))
deleteSpace(space.id)
}}
/>
<SpaceItem
{...space!}
key={i}
onDelete={() => {
setSearchResults((prev) =>
prev.filter((i) => i.space?.id !== space.id),
);
deleteSpace(space.id);
}}
/>
)}
</>
))}
@ -218,11 +218,15 @@ const SpaceExitVariant: Variant = {
},
};
export function MemoryItem(props: StoredContent & { onDelete?: () => void; removeFromSpace?: () => Promise<void>; }) {
const { id, title, image, type, url, onDelete, removeFromSpace } = props
export function MemoryItem(
props: StoredContent & {
onDelete?: () => void;
removeFromSpace?: () => Promise<void>;
},
) {
const { id, title, image, type, url, onDelete, removeFromSpace } = props;
const { deleteMemory } = useMemory()
const { deleteMemory } = useMemory();
const name = title
? title.length > 10
@ -230,9 +234,9 @@ export function MemoryItem(props: StoredContent & { onDelete?: () => void; remov
: title
: "<no title>";
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [moreDropdownOpen, setMoreDropdownOpen] = useState(false)
const [moreDropdownOpen, setMoreDropdownOpen] = useState(false);
const touchEventProps = useTouchHold({
onHold() {
@ -240,52 +244,81 @@ export function MemoryItem(props: StoredContent & { onDelete?: () => void; remov
},
});
return (
<Dialog open={type === "note" ? isDialogOpen : false} onOpenChange={setIsDialogOpen}>
<div {...touchEventProps} className="cursor-pointer hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100">
{
type === "note" ?
(
<DialogTrigger asChild>
<button data-space-text className="focus-visible:outline-none">
{name}
</button>
</DialogTrigger>
) : (
<button onClick={() => window.open(url)} data-space-text className="focus-visible:outline-none">
{name}
</button>
)
}
<Dialog
open={type === "note" ? isDialogOpen : false}
onOpenChange={setIsDialogOpen}
>
<div
{...touchEventProps}
className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex cursor-pointer select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100"
>
{type === "note" ? (
<DialogTrigger asChild>
<button data-space-text className="focus-visible:outline-none">
{name}
</button>
</DialogTrigger>
) : (
<button
onClick={() => window.open(url)}
data-space-text
className="focus-visible:outline-none"
>
{name}
</button>
)}
{type === "page" ?
<PageMoreButton isOpen={moreDropdownOpen} setIsOpen={setMoreDropdownOpen} removeFromSpace={removeFromSpace} onDelete={() => { deleteMemory(id); onDelete?.() }} url={url} /> :
type === "note" ?
<NoteMoreButton isOpen={moreDropdownOpen} setIsOpen={setMoreDropdownOpen} removeFromSpace={removeFromSpace} onEdit={() => setIsDialogOpen(true)} onDelete={() => { deleteMemory(id); onDelete?.() }} />
: null}
{type === "page" ? (
<PageMoreButton
isOpen={moreDropdownOpen}
setIsOpen={setMoreDropdownOpen}
removeFromSpace={removeFromSpace}
onDelete={() => {
deleteMemory(id);
onDelete?.();
}}
url={url}
/>
) : type === "note" ? (
<NoteMoreButton
isOpen={moreDropdownOpen}
setIsOpen={setMoreDropdownOpen}
removeFromSpace={removeFromSpace}
onEdit={() => setIsDialogOpen(true)}
onDelete={() => {
deleteMemory(id);
onDelete?.();
}}
/>
) : null}
<div className="flex h-24 w-24 items-center justify-center">
{type === "page" ? (
<img
onClick={() => window.open(url)}
className="h-16 w-16"
id={id.toString()}
src={image!}
onError={(e) => {
(e.target as HTMLImageElement).src =
"/icons/white_without_bg.png";
}}
/>
) : type === "note" ? (
<Text onClick={() => setIsDialogOpen(true)} className="h-16 w-16" />
) : (
<></>
)}
</div>
</div>
<DialogContent className="w-max max-w-[auto]">
<NoteEdit onDelete={onDelete} closeDialog={() => setIsDialogOpen(false)} memory={props} />
</DialogContent>
</Dialog>
<div className="flex h-24 w-24 items-center justify-center">
{type === "page" ? (
<img
onClick={() => window.open(url)}
className="h-16 w-16"
id={id.toString()}
src={image!}
onError={(e) => {
(e.target as HTMLImageElement).src =
"/icons/white_without_bg.png";
}}
/>
) : type === "note" ? (
<Text onClick={() => setIsDialogOpen(true)} className="h-16 w-16" />
) : (
<></>
)}
</div>
</div>
<DialogContent className="w-max max-w-[auto]">
<NoteEdit
onDelete={onDelete}
closeDialog={() => setIsDialogOpen(false)}
memory={props}
/>
</DialogContent>
</Dialog>
);
}
@ -314,20 +347,23 @@ export function SpaceItem({
const _name = name.length > 10 ? name.slice(0, 10) + "..." : name;
return (
<motion.div
ref={itemRef}
{...touchEventProps}
className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100"
>
<button onClick={onClick} data-space-text className="focus-visible:outline-none">
<button
onClick={onClick}
data-space-text
className="focus-visible:outline-none"
>
{_name}
</button>
<SpaceMoreButton
isOpen={moreDropdownOpen}
setIsOpen={setMoreDropdownOpen}
onEdit={onClick}
onEdit={onClick}
onDelete={() => {
onDelete();
return;
@ -412,7 +448,7 @@ export function SpaceItem({
/>
{spaceMemories.length > 2 ? (
<MemoryWithImages3
onClick={onClick}
onClick={onClick}
className="h-24 w-24"
id={id.toString()}
images={
@ -423,7 +459,7 @@ export function SpaceItem({
/>
) : spaceMemories.length > 1 ? (
<MemoryWithImages2
onClick={onClick}
onClick={onClick}
className="h-24 w-24"
id={id.toString()}
images={
@ -434,7 +470,7 @@ export function SpaceItem({
/>
) : spaceMemories.length === 1 ? (
<MemoryWithImage
onClick={onClick}
onClick={onClick}
className="h-24 w-24"
id={id.toString()}
image={
@ -444,7 +480,10 @@ export function SpaceItem({
}
/>
) : (
<div onClick={onClick} className="bg-rgray-4 shadow- h-24 w-24 scale-50 rounded-full opacity-30"></div>
<div
onClick={onClick}
className="bg-rgray-4 shadow- h-24 w-24 scale-50 rounded-full opacity-30"
></div>
)}
</motion.div>
);
@ -454,15 +493,15 @@ export function SpaceMoreButton({
onDelete,
isOpen,
setIsOpen,
onEdit,
onEdit,
}: {
onDelete?: () => void;
isOpen?: boolean;
onEdit?: () => void;
onEdit?: () => void;
setIsOpen?: (open: boolean) => void;
}) {
return (
<DeleteConfirmation onDelete={onDelete} trigger={false}>
<DeleteConfirmation onDelete={onDelete} trigger={false}>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<button
@ -478,7 +517,7 @@ export function SpaceMoreButton({
Edit
</DropdownMenuItem>
<DialogTrigger asChild>
<DropdownMenuItem className="focus:bg-red-100 focus:text-red-400 dark:focus:bg-red-100/10">
<DropdownMenuItem className="focus:bg-red-100 focus:text-red-400">
<Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} />
Delete
</DropdownMenuItem>
@ -493,17 +532,17 @@ export function PageMoreButton({
onDelete,
isOpen,
setIsOpen,
url,
removeFromSpace
url,
removeFromSpace,
}: {
onDelete?: () => void;
isOpen?: boolean;
url: string;
url: string;
setIsOpen?: (open: boolean) => void;
removeFromSpace?: () => Promise<void>;
removeFromSpace?: () => Promise<void>;
}) {
return (
<DeleteConfirmation onDelete={onDelete} trigger={false}>
<DeleteConfirmation onDelete={onDelete} trigger={false}>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<button
@ -521,14 +560,14 @@ export function PageMoreButton({
/>
Open
</DropdownMenuItem>
{removeFromSpace && (
<DropdownMenuItem onClick={removeFromSpace}>
<Minus className="mr-2 h-4 w-4" strokeWidth={1.5} />
Remove from space
</DropdownMenuItem>
)}
{removeFromSpace && (
<DropdownMenuItem onClick={removeFromSpace}>
<Minus className="mr-2 h-4 w-4" strokeWidth={1.5} />
Remove from space
</DropdownMenuItem>
)}
<DialogTrigger asChild>
<DropdownMenuItem className="focus:bg-red-100 focus:text-red-400 dark:focus:bg-red-100/10">
<DropdownMenuItem className="focus:bg-red-100 focus:text-red-400">
<Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} />
Delete
</DropdownMenuItem>
@ -543,17 +582,17 @@ export function NoteMoreButton({
onDelete,
isOpen,
setIsOpen,
onEdit,
removeFromSpace
onEdit,
removeFromSpace,
}: {
onDelete?: () => void;
isOpen?: boolean;
onEdit?: () => void;
onEdit?: () => void;
setIsOpen?: (open: boolean) => void;
removeFromSpace?: () => Promise<void>;
removeFromSpace?: () => Promise<void>;
}) {
return (
<DeleteConfirmation onDelete={onDelete} trigger={false}>
<DeleteConfirmation onDelete={onDelete} trigger={false}>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<button
@ -568,14 +607,14 @@ export function NoteMoreButton({
<Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} />
Edit
</DropdownMenuItem>
{removeFromSpace && (
<DropdownMenuItem onClick={removeFromSpace}>
<Minus className="mr-2 h-4 w-4" strokeWidth={1.5} />
Remove from space
</DropdownMenuItem>
)}
{removeFromSpace && (
<DropdownMenuItem onClick={removeFromSpace}>
<Minus className="mr-2 h-4 w-4" strokeWidth={1.5} />
Remove from space
</DropdownMenuItem>
)}
<DialogTrigger asChild>
<DropdownMenuItem className="focus:bg-red-100 focus:text-red-400 dark:focus:bg-red-100/10">
<DropdownMenuItem className="focus:bg-red-100 focus:text-red-400">
<Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} />
Delete
</DropdownMenuItem>
@ -589,22 +628,22 @@ export function NoteMoreButton({
export function AddMemoryModal({
type,
children,
defaultSpaces,
onAdd,
data
defaultSpaces,
onAdd,
data,
}: {
type: "page" | "note" | "space" | "existing-memory" | null;
children?: React.ReactNode | React.ReactNode[];
defaultSpaces?: number[];
data?: {
space?: {
title: string,
id: number,
},
fromSpaces?: number[],
notInSpaces?: number[],
},
onAdd?: (data?: StoredSpace | StoredContent | StoredContent[]) => void,
defaultSpaces?: number[];
data?: {
space?: {
title: string;
id: number;
};
fromSpaces?: number[];
notInSpaces?: number[];
};
onAdd?: (data?: StoredSpace | StoredContent | StoredContent[]) => void;
}) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
@ -637,14 +676,31 @@ export function AddMemoryModal({
className="w-max max-w-[auto]"
>
{type === "page" ? (
<AddMemoryPage onAdd={onAdd} defaultSpaces={defaultSpaces} closeDialog={() => setIsDialogOpen(false)} />
<AddMemoryPage
onAdd={onAdd}
defaultSpaces={defaultSpaces}
closeDialog={() => setIsDialogOpen(false)}
/>
) : type === "note" ? (
<NoteAddPage onAdd={onAdd} defaultSpaces={defaultSpaces} closeDialog={() => setIsDialogOpen(false)} />
<NoteAddPage
onAdd={onAdd}
defaultSpaces={defaultSpaces}
closeDialog={() => setIsDialogOpen(false)}
/>
) : type === "space" ? (
<SpaceAddPage onAdd={onAdd} closeDialog={() => setIsDialogOpen(false)} />
<SpaceAddPage
onAdd={onAdd}
closeDialog={() => setIsDialogOpen(false)}
/>
) : type === "existing-memory" ? (
<AddExistingMemoryToSpace onAdd={onAdd} fromSpaces={data?.fromSpaces} notInSpaces={data?.notInSpaces} space={data!.space!} closeDialog={() => setIsDialogOpen(false)} />
) : (
<AddExistingMemoryToSpace
onAdd={onAdd}
fromSpaces={data?.fromSpaces}
notInSpaces={data?.notInSpaces}
space={data!.space!}
closeDialog={() => setIsDialogOpen(false)}
/>
) : (
<></>
)}
</DialogContent>

View file

@ -1,14 +1,13 @@
"use client";
import { MemoryIcon } from "../../assets/Memories";
import { Box, LogOut, Trash2, User2 } from "lucide-react";
import React, { useEffect, useState } from "react";
import { MemoriesBar } from "./MemoriesBar";
import { AnimatePresence, motion } from "framer-motion";
import { Bin } from "@/assets/Bin";
import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar";
import { signOut, useSession } from "next-auth/react";
import MessagePoster from "@/app/MessagePoster";
import Chrome from "@/lib/icons";
import Link from "next/link";
import { SettingsTab } from "./SettingsTab";
import { Avatar, AvatarImage } from "@radix-ui/react-avatar";
import { AvatarFallback } from "../ui/avatar";
export type MenuItem = {
icon: React.ReactNode | React.ReactNode[];
@ -28,40 +27,13 @@ export default function Sidebar({
const [selectedItem, setSelectedItem] = useState<string | null>(null);
const menuItemsTop: Array<MenuItem> = [
{
icon: <MemoryIcon className="h-10 w-10" />,
label: "Memories",
content: <MemoriesBar isOpen={selectedItem !== null} />,
},
];
const menuItemsTop: Array<MenuItem> = [];
const menuItemsBottom: Array<MenuItem> = [
{
icon: <Trash2 strokeWidth={1.3} className="h-6 w-6" />,
label: "Trash",
},
{
icon: (
<div>
<Avatar>
{session?.user?.image ? (
<AvatarImage
className="h-6 w-6 rounded-full"
src={session?.user?.image}
alt="user pfp"
/>
) : (
<User2 strokeWidth={1.3} className="h-6 w-6" />
)}
<AvatarFallback>
{session?.user?.name?.split(" ").map((n) => n[0])}{" "}
</AvatarFallback>
</Avatar>
</div>
),
label: "Profile",
content: <ProfileTab open={selectedItem !== null} />,
label: "Settings",
content: <SettingsTab open={selectedItem !== null} />,
icon: <></>,
},
];
@ -76,69 +48,70 @@ export default function Sidebar({
}, [selectedItem]);
return (
<>
<div className="relative hidden h-screen max-h-screen w-max flex-col items-center text-sm font-light md:flex">
<div className="bg-rgray-3 border-r-rgray-6 relative z-[50] flex h-full w-full flex-col items-center justify-center border-r px-2 py-5 ">
<MenuItem
item={{
label: "Memories",
icon: <MemoryIcon className="h-10 w-10" />,
content: <MemoriesBar isOpen={selectedItem !== null} />,
}}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
<div className="mt-auto" />
{/*
<MenuItem
item={{
label: "Trash",
icon: <Bin id="trash" className="z-[300] h-7 w-7" />,
}}
selectedItem={selectedItem}
id="trash-button"
setSelectedItem={setSelectedItem}
/>
*/}
<MenuItem
item={{
label: "Profile",
icon: (
<div className="mb-2">
<Avatar>
{session?.user?.image ? (
<AvatarImage
className="h-6 w-6 rounded-full"
src={session?.user?.image}
alt="@shadcn"
/>
) : (
<User2 strokeWidth={1.3} className="h-6 w-6" />
)}
<AvatarFallback>
{session?.user?.name?.split(" ").map((n) => n[0])}{" "}
</AvatarFallback>
</Avatar>
</div>
),
content: <ProfileTab open={selectedItem !== null} />,
}}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
<a
className="mb-4 flex items-center justify-center p-2 text-center text-sm text-sky-500"
href="https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc?hl=en-GB&authuser=0"
>
<Chrome className="h-6 w-6" />
</a>
<MessagePoster jwt={jwt} />
<div className="relative hidden h-screen max-h-screen w-max flex-col items-center text-sm font-light md:flex">
<div
className={`relative z-[50] flex h-full w-full flex-col items-center justify-center border-r bg-stone-100 px-2 py-5 `}
>
<Link
data-state-on={selectedItem === "Memories"}
href="/"
onClick={() => setSelectedItem(null)}
className="focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 opacity-80 ring-2 ring-transparent transition hover:bg-stone-300 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none"
>
<MemoryIcon className="h-12 w-12" />
<span className="text-black">Memories</span>
</Link>
<div className="mt-auto" />
<MenuItem
item={{
label: "Settings",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="white"
viewBox="0 0 24 24"
strokeWidth={0.5}
stroke="black"
className="h-10 w-10"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
),
content: <SettingsTab open={selectedItem !== null} />,
}}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
{/* <MessagePoster jwt={jwt} /> */}
<div className="mt-4 flex cursor-pointer flex-col items-center justify-center gap-2 rounded-b-md border-t border-stone-600 px-2 py-3 pt-4 text-black hover:bg-stone-300 hover:opacity-100">
<Avatar>
<AvatarImage
className="h-10 w-10 rounded-full"
src={session?.user?.image!}
alt="Profile picture"
/>
<AvatarFallback>
{session?.user?.name?.split(" ").map((n) => n[0])}{" "}
</AvatarFallback>
</Avatar>
<span>{session?.user?.name?.split(" ")[0]}</span>
</div>
<AnimatePresence>
{selectedItem && <SubSidebar>{Subbar}</SubSidebar>}
</AnimatePresence>
</div>
</>
<AnimatePresence>
{selectedItem && <SubSidebar>{Subbar}</SubSidebar>}
</AnimatePresence>
</div>
);
}
@ -147,21 +120,26 @@ const MenuItem = ({
selectedItem,
setSelectedItem,
...props
}: React.HTMLAttributes<HTMLButtonElement> & {
}: {
item: MenuItem;
selectedItem: string | null;
setSelectedItem: React.Dispatch<React.SetStateAction<string | null>>;
}) => (
<button
data-state-on={selectedItem === label}
onClick={() => setSelectedItem((prev) => (prev === label ? null : label))}
className="on:opacity-100 on:bg-rgray-4 focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 opacity-80 ring-2 ring-transparent transition hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none"
{...props}
>
{icon}
<span className="">{labelDisplay ?? label}</span>
</button>
);
}) => {
const handleClick = () =>
setSelectedItem((prev) => (prev === label ? null : label));
return (
<button
data-state-on={selectedItem === label}
onClick={handleClick}
className="on:opacity-100 on:bg-stone-300 focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 text-black opacity-80 ring-2 ring-transparent transition hover:bg-stone-300 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none"
{...props}
>
{icon}
<span className="">{labelDisplay ?? label}</span>
</button>
);
};
export function SubSidebar({ children }: { children?: React.ReactNode }) {
return (
@ -176,7 +154,7 @@ export function SubSidebar({ children }: { children?: React.ReactNode }) {
transition={{
duration: 0.2,
}}
className="bg-rgray-3 border-r-rgray-6 absolute left-[100%] top-0 z-[10] hidden h-screen w-[30vw] items-start justify-center overflow-x-hidden border-r font-light md:flex"
className="absolute left-[100%] top-0 z-[10] hidden h-screen w-[30vw] items-start justify-center overflow-x-hidden border-r bg-stone-100 font-light md:flex"
>
<motion.div
initial={{ opacity: 0 }}
@ -192,99 +170,3 @@ export function SubSidebar({ children }: { children?: React.ReactNode }) {
</motion.div>
);
}
export function ProfileTab({ open }: { open: boolean }) {
const { data: session } = useSession();
const [tweetStat, setTweetStat] = useState<[number, number] | null>();
const [memoryStat, setMemoryStat] = useState<[number, number] | null>();
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/getCount").then(async (resp) => {
const data = (await resp.json()) as any;
setTweetStat([data.tweetsCount, data.tweetsLimit]);
setMemoryStat([data.pageCount, data.pageLimit]);
setLoading(false);
});
}, [open]);
return (
<div className="text-rgray-11 flex h-full w-full flex-col items-start py-3 text-left font-normal md:py-8">
<div className="w-full px-6">
<h1 className="w-full text-2xl font-medium">Profile</h1>
<div className="mt-5 grid w-full grid-cols-3 gap-1">
<img
className="rounded-full"
src={session?.user?.image ?? "/icons/white_without_bg.png"}
onError={(e) => {
(e.target as HTMLImageElement).src =
"/icons/white_without_bg.png";
}}
/>
<div className="col-span-2 flex flex-col items-start justify-center">
<h1 className="text-xl font-medium">{session?.user?.name}</h1>
<span>{session?.user?.email}</span>
<button
onClick={() => signOut()}
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative mt-auto flex items-center justify-center gap-2 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
<LogOut className="h-4 w-4" />
Logout
</button>
</div>
</div>
</div>
<div className="border-rgray-5 mt-auto w-full border-t px-8 pt-8">
<h1 className="flex w-full items-center gap-2 text-xl">
<Box className="h-6 w-6" />
Storage
</h1>
{loading ? (
<div className="my-5 flex w-full flex-col items-center justify-center gap-5">
<div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div>
<div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div>
</div>
) : (
<>
<div className="my-5">
<h2 className="text-md flex w-full items-center justify-between">
Memories
<div className="bg-rgray-4 flex rounded-md px-2 py-2 text-xs text-white/50">
{memoryStat?.join("/")}
</div>
</h2>
<div className="bg-rgray-2 mt-2 h-5 w-full overflow-hidden rounded-full">
<div
style={{
width: `${((memoryStat?.[0] ?? 0) / (memoryStat?.[1] ?? 100)) * 100}%`,
minWidth: memoryStat?.[0] ?? 0 > 0 ? "5%" : "0%",
}}
className="bg-rgray-5 h-full rounded-full"
/>
</div>
</div>
<div className="my-5">
<h2 className="text-md flex w-full items-center justify-between">
Tweets
<div className="bg-rgray-4 flex rounded-md px-2 py-2 text-xs text-white/50">
{tweetStat?.join("/")}
</div>
</h2>
<div className="bg-rgray-2 mt-2 h-5 w-full overflow-hidden rounded-full">
<div
style={{
width: `${((tweetStat?.[0] ?? 0) / (tweetStat?.[1] ?? 100)) * 100}%`,
minWidth: tweetStat?.[0] ?? 0 > 0 ? "5%" : "0%",
}}
className="bg-rgray-5 h-full rounded-full"
/>
</div>
</div>
</>
)}
</div>
</div>
);
}

View file

@ -39,7 +39,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-gray-100 dark:bg-gray-800",
"flex h-full w-full items-center justify-center rounded-full bg-gray-100",
className,
)}
{...props}

View file

@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 dark:border-gray-800 dark:focus:ring-gray-300",
"inline-flex items-center rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2",
{
variants: {
variant: {

View file

@ -38,13 +38,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] border-rgray-6 bg-rgray-3 fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border p-6 shadow-lg duration-200 rounded-lg",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] border-rgray-6 fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-lg border bg-[#F4F3F2] p-6 text-black shadow-lg duration-200",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-rgray-2 focus:ring-rgray-7 data-[state=open]:bg-rgray-3 data-[state=open]:text-rgray-11 absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<DialogPrimitive.Close className="ring-offset-rgray-2 focus:ring-rgray-7 absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:text-black">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
@ -58,10 +58,7 @@ const DialogHeader = ({
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-left",
className,
)}
className={cn("flex flex-col space-y-1.5 text-left", className)}
{...props}
/>
);
@ -102,7 +99,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-rgray-11 text-sm", className)}
className={cn("text-sm text-slate-800", className)}
{...props}
/>
));

View file

@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-950 shadow-lg dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-950 shadow-lg",
className,
)}
{...props}
@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"data-[state=open]:animate-in bg-rgray-3 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-rgray-6 text-rgray-11 z-50 min-w-[9rem] overflow-hidden rounded-md border p-1 shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-rgray-6 z-50 min-w-[9rem] overflow-hidden rounded-md border bg-white p-1 text-black shadow-md",
className,
)}
{...props}
@ -83,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"focus:bg-rgray-4 focus:text-rgray-12 relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-stone-200 focus:text-slate-800 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ",
inset && "pl-8",
className,
)}
@ -162,7 +162,7 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-gray-100 dark:bg-gray-800", className)}
className={cn("-mx-1 my-1 h-px bg-gray-100", className)}
{...props}
/>
));

View file

@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"border-rgray-6 text-rgray-11 placeholder:text-rgray-11 focus-visible:ring-rgray-7 flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 ",
"border-rgray-6 focus-visible:ring-rgray-7 flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-black/50 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 ",
className,
)}
ref={ref}
@ -31,7 +31,7 @@ const InputWithIcon = React.forwardRef<HTMLInputElement, InputWithIconProps>(
return (
<div
className={cn(
"border-rgray-6 text-rgray-11 focus-within:ring-rgray-7 flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition focus-within:outline-none focus-within:ring-2 ",
"border-rgray-1/70 text-rgray-11 focus-within:ring-rgray-7 flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition focus-within:outline-none focus-within:ring-2 ", // TODO: change to black
className,
)}
>
@ -39,7 +39,7 @@ const InputWithIcon = React.forwardRef<HTMLInputElement, InputWithIconProps>(
<input
type={type}
className={
"placeholder:text-rgray-11/50 w-full bg-transparent font-normal file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
"w-full bg-transparent font-normal file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-black/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
}
ref={ref}
{...props}

View file

@ -25,18 +25,27 @@
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"async-hook-jl": "^1.7.6",
"autoprefixer": "^10.4.17",
"drizzle-kit": "beta",
"esbuild-jest": "^0.5.0",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.4",
"if-env": "^1.0.4",
"jest": "^29.7.0",
"jest-environment-miniflare": "^2.14.2",
"postcss": "^8.4.35",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"tailwindcss": "^3.4.1",
"typescript": "^5.1.6"
"ts-loader": "^9.5.1",
"typescript": "^5.1.6",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@ai-sdk/anthropic": "^0.0.13",
"@ai-sdk/google": "^0.0.10",
"@ai-sdk/openai": "^0.0.10",
"@auth/d1-adapter": "^0.6.0",
"@auth/drizzle-adapter": "^0.7.0",
"@cloudflare/ai": "^1.0.52",
@ -51,14 +60,18 @@
"@radix-ui/react-label": "^2.0.2",
"@t3-oss/env-nextjs": "^0.9.2",
"@types/chrome": "^0.0.261",
"ai": "^3.0.0",
"ai": "^3.1.5",
"better-sqlite3": "^9.4.3",
"class-variance-authority": "^0.7.0",
"cloudflare": "^3.0.0",
"compromise": "^14.13.0",
"crypto": "^1.0.1",
"dotenv-cli": "^7.3.0",
"drizzle-orm": "^0.29.4",
"eslint-plugin-next-on-pages": "^1.9.0",
"framer-motion": "^11.0.6",
"hono": "^4.3.7",
"honox": "^0.1.17",
"html-metadata": "^1.7.1",
"html-metadata-parser": "^2.0.4",
"husky": "^9.0.11",
@ -66,9 +79,13 @@
"lucide-react": "^0.343.0",
"next-auth": "beta",
"nuqs": "^1.17.1",
"random-js": "^2.1.0",
"react-markdown": "^9.0.1",
"react-tweet": "^3.2.1",
"remark-gfm": "^4.0.0",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"wrangler": "3.56.0",
"zod": "^3.23.8"
}
}