diff --git a/.github/actions/buildextension/action.yml b/.github/actions/buildextension/action.yml index dc1dc80d..b65953d7 100644 --- a/.github/actions/buildextension/action.yml +++ b/.github/actions/buildextension/action.yml @@ -26,4 +26,4 @@ runs: uses: actions/upload-artifact@v3 with: name: extension-${{ github.sha }} - path: apps/extensions/dist/extension-${{ github.sha }}.zip \ No newline at end of file + path: apps/extension/dist/extension-${{ github.sha }}.zip \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 056fca08..c99080ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,10 +1,18 @@ -name: Build +name: Build and Deploy Changes on: push: branches: [main] + paths: + - 'apps/web/**' + - 'apps/extension/**' + - 'apps/cf-ai-backend/**' pull_request: branches: [main] + paths: + - 'apps/web/**' + - 'apps/extension/**' + - 'apps/cf-ai-backend/**' jobs: build-extension: @@ -12,6 +20,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/actions/buildextension + build-app: runs-on: ubuntu-latest steps: @@ -41,9 +50,25 @@ jobs: - name: Publish to Cloudflare Pages uses: cloudflare/pages-action@v1 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' with: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} directory: apps/web/.vercel/output/static branch: main + + # deploy-cf-worker: + # runs-on: ubuntu-latest + # steps: + # - name: Checkout repo + # uses: actions/checkout@v3 + + # - name: Deploy to Cloudflare Workers + # uses: cloudflare/wrangler-action@1.2.0 + # with: + # apiToken: ${{ secrets.CF_API_TOKEN }} + # workingDirectory: apps/cf-ai-backend + # env: + # OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # SECURITY_KEY: ${{ secrets.BACKEND_SECURITY_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57168bb0..bdc9d474 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,4 +27,4 @@ jobs: clientID: ${{ secrets.GOOGLE_CLIENT_ID }} clientSecret: ${{ secrets.GOOGLE_CLIENT_SECRET }} clientRefreshToken: ${{ secrets.GOOGLE_REFRESH_TOKEN }} - extensionFile: apps/extension/extension-${{ github.sha }}.zip + extensionFile: apps/extension/dist/extension-${{ github.sha }}.zip diff --git a/apps/cf-ai-backend/src/env.d.ts b/apps/cf-ai-backend/src/env.d.ts new file mode 100644 index 00000000..acbd6c43 --- /dev/null +++ b/apps/cf-ai-backend/src/env.d.ts @@ -0,0 +1,7 @@ +interface Env { + VECTORIZE_INDEX: VectorizeIndex; + AI: Fetcher; + SECURITY_KEY: string; + OPENAI_API_KEY: string; + GOOGLE_AI_API_KEY: string; +} diff --git a/apps/cf-ai-backend/src/index.ts b/apps/cf-ai-backend/src/index.ts index fc7241a0..f55c465b 100644 --- a/apps/cf-ai-backend/src/index.ts +++ b/apps/cf-ai-backend/src/index.ts @@ -7,29 +7,20 @@ import type { import { CloudflareVectorizeStore, } from "@langchain/cloudflare"; -import { Ai } from '@cloudflare/ai'; import { OpenAIEmbeddings } from "./OpenAIEmbedder"; -import { AiTextGenerationOutput } from "@cloudflare/ai/dist/ai/tasks/text-generation"; - -export interface Env { - VECTORIZE_INDEX: VectorizeIndex; - AI: Fetcher; - SECURITY_KEY: string; - OPENAI_API_KEY: string; -} - +import { GoogleGenerativeAI } from "@google/generative-ai"; +import routeMap from "./routes"; function isAuthorized(request: Request, env: Env): boolean { return request.headers.get('X-Custom-Auth-Key') === env.SECURITY_KEY; } export default { - async fetch(request: Request, env: Env) { + async fetch(request: Request, env: Env, ctx: ExecutionContext) { if (!isAuthorized(request, env)) { return new Response('Unauthorized', { status: 401 }); } - const pathname = new URL(request.url).pathname; const embeddings = new OpenAIEmbeddings({ apiKey: env.OPENAI_API_KEY, modelName: 'text-embedding-3-small', @@ -38,128 +29,26 @@ export default { const store = new CloudflareVectorizeStore(embeddings, { index: env.VECTORIZE_INDEX, }); - const ai = new Ai(env.AI) - if (pathname === "/add" && request.method === "POST") { + const genAI = new GoogleGenerativeAI(env.GOOGLE_AI_API_KEY); - const body = await request.json() as { - pageContent: string, - title?: string, - description?: string, - url: string, - user: string - }; + const model = genAI.getGenerativeModel({ model: "gemini-pro" }); + const url = new URL(request.url); + const path = url.pathname; + const method = request.method.toUpperCase(); - if (!body.pageContent || !body.url) { - return new Response(JSON.stringify({ message: "Invalid Page Content" }), { status: 400 }); - } - const newPageContent = `Title: ${body.title}\nDescription: ${body.description}\nURL: ${body.url}\nContent: ${body.pageContent}` + const routeHandlers = routeMap.get(path); - - await store.addDocuments([ - { - pageContent: newPageContent, - metadata: { - title: body.title ?? "", - description: body.description ?? "", - url: body.url, - user: body.user, - }, - }, - ], { - ids: [`${body.url}`] - }) - - return new Response(JSON.stringify({ message: "Document Added" }), { status: 200 }); + if (!routeHandlers) { + return new Response('Not Found', { status: 404 }); } - else if (pathname === "/query" && request.method === "GET") { - 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 handler = routeHandlers[method]; - 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: { - $eq: user - } - } - - 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: 400 }); - } - - const highScoreIds = resp.matches.filter(({ score }) => score > 0.3).map(({ id }) => id) - - if (sourcesOnly === "true") { - return new Response(JSON.stringify({ ids: highScoreIds }), { 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 metadatas = vec.map(({ metadata }) => metadata) - - console.log(metadatas) - - // TODO: TAKE ALL THE HIGH SCORED IDS INTO CONSIDERATION - const output: AiTextGenerationOutput = await ai.run('@hf/thebloke/mistral-7b-instruct-v0.1-awq', { - 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${vec[0].metadata!.text} \nAnswer this question based on the context. Question: ${query}\nAnswer:`, - stream: true - }) as ReadableStream - - - return new Response(output, { - headers: { - "content-type": "text/event-stream", - }, - }); + if (!handler) { + return new Response('Method Not Allowed', { status: 405 }); } - - else if (pathname === "/ask" && request.method === "POST") { - const body = await request.json() as { - query: string - }; - - if (!body.query) { - return new Response(JSON.stringify({ message: "Invalid Page Content" }), { status: 400 }); - } - - const output: AiTextGenerationOutput = await ai.run('@hf/thebloke/mistral-7b-instruct-v0.1-awq', { - prompt: `You are an agent that answers a question based on the query. Be direct and concise, don't say 'based on the context'.\n\n Context:\n${body.query} \nAnswer this question based on the context. Question: ${body.query}\nAnswer:`, - stream: true - }) as ReadableStream - - - return new Response(output, { - headers: { - "content-type": "text/event-stream", - }, - }); - } - - return new Response(JSON.stringify({ message: "Invalid Request" }), { status: 400 }); - + return await handler(request, store, embeddings, model, env, ctx); }, }; diff --git a/apps/cf-ai-backend/src/routes.ts b/apps/cf-ai-backend/src/routes.ts new file mode 100644 index 00000000..4a2d2827 --- /dev/null +++ b/apps/cf-ai-backend/src/routes.ts @@ -0,0 +1,23 @@ +import { CloudflareVectorizeStore } from '@langchain/cloudflare'; +import * as apiAdd from './routes/add'; +import * as apiQuery from "./routes/query" +import * as apiAsk from "./routes/ask" +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; + +const routeMap = new Map>(); + +routeMap.set('/add', apiAdd); + +routeMap.set('/query', apiQuery); + +routeMap.set('/ask', apiAsk); + +// Add more route mappings as needed +// routeMap.set('/api/otherRoute', { ... }); + +export default routeMap; diff --git a/apps/cf-ai-backend/src/routes/add.ts b/apps/cf-ai-backend/src/routes/add.ts new file mode 100644 index 00000000..9b05e9f0 --- /dev/null +++ b/apps/cf-ai-backend/src/routes/add.ts @@ -0,0 +1,36 @@ +import { Request } from "@cloudflare/workers-types"; +import { type CloudflareVectorizeStore } from "@langchain/cloudflare"; + +export async function POST(request: Request, store: CloudflareVectorizeStore) { + 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 newPageContent = `Title: ${body.title}\nDescription: ${body.description}\nURL: ${body.url}\nContent: ${body.pageContent}` + + + await store.addDocuments([ + { + pageContent: newPageContent, + metadata: { + title: body.title ?? "", + description: body.description ?? "", + space: body.space ?? "", + url: body.url, + user: body.user, + }, + }, + ], { + ids: [`${body.url}`] + }) + + return new Response(JSON.stringify({ message: "Document Added" }), { status: 200 }); +} diff --git a/apps/cf-ai-backend/src/routes/ask.ts b/apps/cf-ai-backend/src/routes/ask.ts new file mode 100644 index 00000000..1c48dde8 --- /dev/null +++ b/apps/cf-ai-backend/src/routes/ask.ts @@ -0,0 +1,35 @@ +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; +} diff --git a/apps/cf-ai-backend/src/routes/query.ts b/apps/cf-ai-backend/src/routes/query.ts new file mode 100644 index 00000000..bf94c13e --- /dev/null +++ b/apps/cf-ai-backend/src/routes/query.ts @@ -0,0 +1,79 @@ +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: { + $eq: user + } + } + + if (space) { + filter.space = { + $eq: 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: 400 }); + } + + const highScoreIds = resp.matches.filter(({ score }) => score > 0.3).map(({ id }) => id) + + if (sourcesOnly === "true") { + return new Response(JSON.stringify({ ids: highScoreIds }), { 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; +} diff --git a/apps/cf-ai-backend/wrangler.toml b/apps/cf-ai-backend/wrangler.toml new file mode 100644 index 00000000..9e3ee21b --- /dev/null +++ b/apps/cf-ai-backend/wrangler.toml @@ -0,0 +1,58 @@ +name = "cf-ai-backend" +main = "src/index.ts" +compatibility_date = "2024-02-23" + +[[vectorize]] +binding = "VECTORIZE_INDEX" +index_name = "any-vector" + +[ai] +binding = "AI" + +# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) +# Note: Use secrets to store sensitive data. +# Docs: https://developers.cloudflare.com/workers/platform/environment-variables +# [vars] +# MY_VARIABLE = "production_value" + +# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. +# Docs: https://developers.cloudflare.com/workers/runtime-apis/kv +# [[kv_namespaces]] +# binding = "MY_KV_NAMESPACE" +# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. +# Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ +# [[r2_buckets]] +# binding = "MY_BUCKET" +# bucket_name = "my-bucket" + +# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. +# Docs: https://developers.cloudflare.com/queues/get-started +# [[queues.producers]] +# binding = "MY_QUEUE" +# queue = "my-queue" + +# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. +# Docs: https://developers.cloudflare.com/queues/get-started +# [[queues.consumers]] +# queue = "my-queue" + +# Bind another Worker service. Use this binding to call another Worker without network overhead. +# Docs: https://developers.cloudflare.com/workers/platform/services +# [[services]] +# binding = "MY_SERVICE" +# service = "my-service" + +# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. +# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. +# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects +# [[durable_objects.bindings]] +# name = "MY_DURABLE_OBJECT" +# class_name = "MyDurableObject" + +# Durable Object migrations. +# Docs: https://developers.cloudflare.com/workers/learning/using-durable-objects#configure-durable-object-classes-with-migrations +# [[migrations]] +# tag = "v1" +# new_classes = ["MyDurableObject"] diff --git a/apps/extension/.gitignore b/apps/extension/.gitignore index a547bf36..97d5fffe 100644 --- a/apps/extension/.gitignore +++ b/apps/extension/.gitignore @@ -1,4 +1,6 @@ # Logs +*.zip + logs *.log npm-debug.log* diff --git a/apps/extension/README.md b/apps/extension/README.md index 0d6babed..7619225f 100644 --- a/apps/extension/README.md +++ b/apps/extension/README.md @@ -28,3 +28,4 @@ export default { - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list + \ No newline at end of file diff --git a/apps/extension/package.json b/apps/extension/package.json index 95957e47..1543535f 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "package": "zip -r extension.zip dist/" }, "dependencies": { "@radix-ui/react-tooltip": "^1.0.7", diff --git a/apps/extension/src/App.tsx b/apps/extension/src/App.tsx index dbba707e..a442cbb9 100644 --- a/apps/extension/src/App.tsx +++ b/apps/extension/src/App.tsx @@ -14,7 +14,7 @@ function App() { if (loginButton) { if (jwt) { - fetch('https://anycontext.dhr.wtf/api/me', { + fetch('https://supermemory.dhr.wtf/api/me', { headers: { Authorization: `Bearer ${jwt}`, }, @@ -43,7 +43,7 @@ function App() { + + {websites.map((item) => ( + + ))} + + + ); +} + +export const ListItem: React.FC<{ item: StoredContent }> = ({ item }) => { + const [isEditing, setIsEditing] = useState(false); + const editInputRef = useRef(null); + + useEffect(() => { + if (isEditing) { + setTimeout(() => { + editInputRef.current?.focus(); + }, 500); + } + }, [isEditing]); + + return ( +
+ isEditing && e.preventDefault()} + className="flex w-[90%] items-center gap-2 focus:outline-none" + > + {isEditing ? ( + + ) : ( + <> + {item.title + + + )} + {isEditing ? ( + setIsEditing(false)} + onKeyDown={(e) => e.key === 'Escape' && setIsEditing(false)} + /> + ) : ( + + {item.title ?? 'Untitled website'} + + )} + + + + + + + window.open(item.url)}> + + Open + + { + setIsEditing(true); + }} + > + + Edit + + + + Delete + + + +
+ ); +}; diff --git a/apps/web/src/components/Sidebar/CategoryItem.tsx b/apps/web/src/components/Sidebar/CategoryItem.tsx index 7fb571b5..c2e72ba5 100644 --- a/apps/web/src/components/Sidebar/CategoryItem.tsx +++ b/apps/web/src/components/Sidebar/CategoryItem.tsx @@ -50,6 +50,7 @@ const pages: StoredContent[] = [ image: "https://code.visualstudio.com/favicon.ico", baseUrl: "https://code.visualstudio.com", savedAt: new Date(), + space: "" }, { id: 2, @@ -60,6 +61,7 @@ const pages: StoredContent[] = [ image: "https://github.com/favicon.ico", baseUrl: "https://github.com", savedAt: new Date(), + space: "" }, { id: 3, @@ -70,6 +72,7 @@ const pages: StoredContent[] = [ image: "https://github.com/favicon.ico", baseUrl: "https://github.com", savedAt: new Date(), + space: "" }, { id: 4, @@ -80,6 +83,7 @@ const pages: StoredContent[] = [ image: "https://github.com/favicon.ico", baseUrl: "https://github.com", savedAt: new Date(), + space: "" }, { id: 5, @@ -90,6 +94,7 @@ const pages: StoredContent[] = [ image: "https://github.com/favicon.ico", baseUrl: "https://github.com", savedAt: new Date(), + space: "" }, { id: 6, @@ -100,6 +105,7 @@ const pages: StoredContent[] = [ image: "https://github.com/favicon.ico", baseUrl: "https://github.com", savedAt: new Date(), + space: "" }, { id: 7, @@ -110,6 +116,7 @@ const pages: StoredContent[] = [ image: "https://github.com/favicon.ico", baseUrl: "https://github.com", savedAt: new Date(), + space: "" }, { id: 8, @@ -120,6 +127,7 @@ const pages: StoredContent[] = [ image: "https://github.com/favicon.ico", baseUrl: "https://github.com", savedAt: new Date(), + space: "" }, { id: 9, @@ -130,6 +138,7 @@ const pages: StoredContent[] = [ image: "https://github.com/favicon.ico", baseUrl: "https://github.com", savedAt: new Date(), + space: "" }, ]; export const CategoryItem: React.FC<{ item: StoredContent }> = ({ item }) => { diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index e9f675a5..969d3e53 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -2,92 +2,98 @@ import { MemoryWithImage, MemoryWithImages3, MemoryWithImages2, -} from "@/assets/MemoryWithImages"; -import { type Space } from "../../../types/memory"; -import { InputWithIcon } from "../ui/input"; -import { Search } from "lucide-react"; +} from '@/assets/MemoryWithImages'; +import { type Space } from '../../../types/memory'; +import { InputWithIcon } from '../ui/input'; +import { Search } from 'lucide-react'; export function MemoriesBar() { const spaces: Space[] = [ { id: 1, - title: "Cool Tech", - description: "Really cool mind blowing tech", + title: 'Cool Tech', + description: 'Really cool mind blowing tech', content: [ { id: 1, - title: "Perplexity", - description: "A good ui", - content: "", - image: "https://perplexity.ai/favicon.ico", - url: "https://perplexity.ai", + title: 'Perplexity', + description: 'A good ui', + content: '', + image: 'https://perplexity.ai/favicon.ico', + url: 'https://perplexity.ai', savedAt: new Date(), - baseUrl: "https://perplexity.ai", + baseUrl: 'https://perplexity.ai', + space: 'Cool tech', }, { id: 2, - title: "Pi.ai", - description: "A good ui", - content: "", - image: "https://pi.ai/pi-logo-192.png?v=2", - url: "https://pi.ai", + title: 'Pi.ai', + description: 'A good ui', + content: '', + image: 'https://pi.ai/pi-logo-192.png?v=2', + url: 'https://pi.ai', savedAt: new Date(), - baseUrl: "https://pi.ai", + baseUrl: 'https://pi.ai', + space: 'Cool tech', }, { id: 3, - title: "Visual Studio Code", - description: "A good ui", - content: "", - image: "https://code.visualstudio.com/favicon.ico", - url: "https://code.visualstudio.com", + title: 'Visual Studio Code', + description: 'A good ui', + content: '', + image: 'https://code.visualstudio.com/favicon.ico', + url: 'https://code.visualstudio.com', savedAt: new Date(), - baseUrl: "https://code.visualstudio.com", + baseUrl: 'https://code.visualstudio.com', + space: 'Cool tech', }, ], }, { id: 2, - title: "Cool Courses", - description: "Amazng", + title: 'Cool Courses', + description: 'Amazng', content: [ { id: 1, - title: "Animation on the web", - description: "A good ui", - content: "", - image: "https://animations.dev/favicon.ico", - url: "https://animations.dev", + title: 'Animation on the web', + description: 'A good ui', + content: '', + image: 'https://animations.dev/favicon.ico', + url: 'https://animations.dev', savedAt: new Date(), - baseUrl: "https://animations.dev", + baseUrl: 'https://animations.dev', + space: 'Cool courses', }, { id: 2, - title: "Tailwind Course", - description: "A good ui", - content: "", + title: 'Tailwind Course', + description: 'A good ui', + content: '', image: - "https://tailwindcss.com/_next/static/media/tailwindcss-mark.3c5441fc7a190fb1800d4a5c7f07ba4b1345a9c8.svg", - url: "https://tailwindcss.com", + 'https://tailwindcss.com/_next/static/media/tailwindcss-mark.3c5441fc7a190fb1800d4a5c7f07ba4b1345a9c8.svg', + url: 'https://tailwindcss.com', savedAt: new Date(), - baseUrl: "https://tailwindcss.com", + baseUrl: 'https://tailwindcss.com', + space: 'Cool courses', }, ], }, { id: 3, - title: "Cool Libraries", - description: "Really cool mind blowing tech", + title: 'Cool Libraries', + description: 'Really cool mind blowing tech', content: [ { id: 1, - title: "Perplexity", - description: "A good ui", - content: "", - image: "https://yashverma.me/logo.jpg", - url: "https://perplexity.ai", + title: 'Perplexity', + description: 'A good ui', + content: '', + image: 'https://yashverma.me/logo.jpg', + url: 'https://perplexity.ai', savedAt: new Date(), - baseUrl: "https://perplexity.ai", + baseUrl: 'https://perplexity.ai', + space: 'Cool libraries', }, ], }, diff --git a/apps/web/src/server/db/schema.ts b/apps/web/src/server/db/schema.ts index 55a2ea1e..46f00f71 100644 --- a/apps/web/src/server/db/schema.ts +++ b/apps/web/src/server/db/schema.ts @@ -114,6 +114,7 @@ export const storedContent = createTable( title: text("title", { length: 255 }), description: text("description", { length: 255 }), url: text("url").notNull().unique(), + space: text("space", { length: 255 }), savedAt: int("savedAt", { mode: "timestamp" }).notNull(), baseUrl: text("baseUrl", { length: 255 }), image: text("image", { length: 255 }), @@ -122,6 +123,7 @@ export const storedContent = createTable( urlIdx: index("storedContent_url_idx").on(sc.url), savedAtIdx: index("storedContent_savedAt_idx").on(sc.savedAt), titleInx: index("storedContent_title_idx").on(sc.title), + spaceIdx: index("storedContent_space_idx").on(sc.space), }), ); diff --git a/apps/web/wrangler.toml b/apps/web/wrangler.toml index cb866aec..12619fdb 100644 --- a/apps/web/wrangler.toml +++ b/apps/web/wrangler.toml @@ -10,11 +10,3 @@ index_name = "anycontext-idx" binding = "DATABASE" database_name = "dev-d1-anycontext" database_id = "fc562605-157a-4f60-b439-2a24ffed5b4c" - - -[vars] -GOOGLE_CLIENT_ID="872261398704-lntqrjrprbq3bih3cvv9o60huh0pf3l2.apps.googleusercontent.com" -GOOGLE_CLIENT_SECRET="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" -NEXTAUTH_SECRET="sec" -NEXTAUTH_URL="https://opulent-funicular-94rx4v9w775f96q-3000.app.github.dev/" -BACKEND_SECURITY_KEY="XP1FNh+rTYghq2TmrFbMLCCBy+eBAvi8iuoMuV9zDUc=" diff --git a/package.json b/package.json index 5ba4eea6..145a2384 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@cloudflare/ai": "^1.0.52", "@cloudflare/next-on-pages-next-dev": "^0.0.1", "@crxjs/vite-plugin": "^1.0.14", + "@google/generative-ai": "^0.3.1", "@heroicons/react": "^2.1.1", "@langchain/cloudflare": "^0.0.3", "@radix-ui/colors": "^3.0.0",