mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-05 23:40:57 +00:00
feat: Rewrite backend to hono ⚡
This commit is contained in:
parent
54cc2687f3
commit
c68433d9d8
48 changed files with 1268 additions and 1656 deletions
|
|
@ -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
|
||||
178
apps/cf-ai-backend/.gitignore
vendored
178
apps/cf-ai-backend/.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"printWidth": 140,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"useTabs": true
|
||||
}
|
||||
58
apps/cf-ai-backend/README.md
Normal file
58
apps/cf-ai-backend/README.md
Normal 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
|
||||
7
apps/cf-ai-backend/jest.config.js
Normal file
7
apps/cf-ai-backend/jest.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
testEnvironment: "miniflare",
|
||||
testMatch: ["**/test/**/*.+(ts|tsx)", "**/src/**/(*.)+(spec|test).+(ts|tsx)"],
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "esbuild-jest",
|
||||
},
|
||||
};
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
19
apps/cf-ai-backend/src/env.d.ts
vendored
19
apps/cf-ai-backend/src/env.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
139
apps/cf-ai-backend/src/helper.ts
Normal file
139
apps/cf-ai-backend/src/helper.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
13
apps/cf-ai-backend/src/index.test.ts
Normal file
13
apps/cf-ai-backend/src/index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
47
apps/cf-ai-backend/src/prompts/prompt1.ts
Normal file
47
apps/cf-ai-backend/src/prompts/prompt1.ts
Normal 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();
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
};
|
||||
|
|
@ -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 });
|
||||
}
|
||||
48
apps/cf-ai-backend/src/types.ts
Normal file
48
apps/cf-ai-backend/src/types.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
43
apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts
Normal file
43
apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
44
apps/cf-ai-backend/src/utils/chonker.ts
Normal file
44
apps/cf-ai-backend/src/utils/chonker.ts
Normal 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);
|
||||
}
|
||||
18
apps/cf-ai-backend/src/utils/seededRandom.ts
Normal file
18
apps/cf-ai-backend/src/utils/seededRandom.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
apps/cf-ai-backend/vite.config.ts
Normal file
6
apps/cf-ai-backend/vite.config.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { defineConfig } from "vite";
|
||||
import honox from "honox/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [honox()],
|
||||
});
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
23
package.json
23
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue