diff --git a/backend/InMemoryStore.ts b/backend/InMemoryStore.ts index 2c7d9808..60033bc0 100644 --- a/backend/InMemoryStore.ts +++ b/backend/InMemoryStore.ts @@ -4,49 +4,53 @@ const EVICTION_TIME = 5 * 60 * 1000; const EVICTION_CLOCK_TIME = 1 * 60 * 1000; export class InMemoryStore { - private static store: InMemoryStore; - private store: Record; - - private clock: NodeJS.Timeout; - - private constructor() { - this.store = {}; - this.clock = setInterval(() => { - Object.entries(this.store).forEach(([key, {evictionTime}]) => { - if (evictionTime > Date.now()) { - delete this.store[key] - } - }); - }, EVICTION_CLOCK_TIME); + private static store: InMemoryStore; + private store: Record< + string, + { + messages: Message[]; + evictionTime: number; } + >; - public destroy() { - clearInterval(this.clock) - } + private clock: NodeJS.Timeout; - static getInstance() { - if (!InMemoryStore.store) { - InMemoryStore.store = new InMemoryStore() + private constructor() { + this.store = {}; + this.clock = setInterval(() => { + Object.entries(this.store).forEach(([key, { evictionTime }]) => { + if (evictionTime > Date.now()) { + delete this.store[key]; } - return InMemoryStore.store; - } + }); + }, EVICTION_CLOCK_TIME); + } + + public destroy() { + clearInterval(this.clock); + } - get(conversationId: string): Message[] { - return this.store[conversationId]?.messages ?? [] + static getInstance() { + if (!InMemoryStore.store) { + InMemoryStore.store = new InMemoryStore(); + } + return InMemoryStore.store; + } + + get(conversationId: string): Message[] { + return this.store[conversationId]?.messages ?? []; + } + + add(conversationId: string, message: Message) { + if (!this.store[conversationId]) { + this.store[conversationId] = { + messages: [], + evictionTime: Date.now() + EVICTION_TIME, + }; } - add(conversationId: string, message: Message) { - if (!this.store[conversationId]) { - this.store[conversationId] = { - messages: [], - evictionTime: Date.now() + EVICTION_TIME - } - } + this.store[conversationId]?.messages?.push(message); + this.store[conversationId].evictionTime = Date.now() + EVICTION_TIME; + } +} - this.store[conversationId]?.messages?.push(message); - this.store[conversationId].evictionTime = Date.now() + EVICTION_TIME; - } -} \ No newline at end of file diff --git a/backend/openrouter.ts b/backend/openrouter.ts index f9d56d08..952078cc 100644 --- a/backend/openrouter.ts +++ b/backend/openrouter.ts @@ -1,84 +1,96 @@ -import type { Message, MODEL, SUPPORTER_MODELS } from "./types"; +import { Role, type Message, type MODEL, type SUPPORTER_MODELS } from "./types"; const OPENROUTER_KEY = process.env.OPENROUTER_KEY!; const MAX_TOKEN_ITERATIONS = 1000; +type CreateCompletionOptions = { + plugins?: { id: string }[]; +}; + export const createCompletion = async ( - messages: Message[], - model: MODEL, - cb: (chunk: string) => void, - systemPrompt?: string + messages: Message[], + model: MODEL, + cb: (chunk: string) => void, + systemPrompt?: string, + options?: CreateCompletionOptions, ) => { - return new Promise(async (resolve, reject) => { - const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { - method: 'POST', - headers: { - Authorization: `Bearer ${OPENROUTER_KEY}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - model, - messages: messages, - stream: true, - system: systemPrompt, - }), - }); - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error('Response body is not readable'); + return new Promise(async (resolve, reject) => { + const response = await fetch( + "https://openrouter.ai/api/v1/chat/completions", + { + method: "POST", + headers: { + Authorization: `Bearer ${OPENROUTER_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model, + messages: [ + ...(systemPrompt + ? [{ role: Role.System, content: systemPrompt }] + : []), + ...messages, + ], + stream: true, + system: systemPrompt, + plugins: options?.plugins, + }), + }, + ); + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Response body is not readable"); + } + + const decoder = new TextDecoder(); + let buffer = ""; + + try { + let tokenIterations = 0; + while (true) { + tokenIterations++; + if (tokenIterations > MAX_TOKEN_ITERATIONS) { + console.log("max token iterations"); + resolve(); + return; + } + const { done, value } = await reader.read(); + if (done) break; + + // Append new chunk to buffer + buffer += decoder.decode(value, { stream: true }); + + // Process complete lines from buffer + while (true) { + const lineEnd = buffer.indexOf("\n"); + if (lineEnd === -1) { + console.log("max token iterations 2"); + break; } - - const decoder = new TextDecoder(); - let buffer = ''; - - try { - let tokenIterations = 0; - while (true) { - tokenIterations++; - if (tokenIterations > MAX_TOKEN_ITERATIONS) { - console.log("max token iterations"); - resolve() - return; - } - const { done, value } = await reader.read(); - if (done) break; - - // Append new chunk to buffer - buffer += decoder.decode(value, { stream: true }); - - // Process complete lines from buffer - while (true) { - const lineEnd = buffer.indexOf('\n'); - if (lineEnd === -1) { - console.log("max token iterations 2"); - break - }; - - const line = buffer.slice(0, lineEnd).trim(); - buffer = buffer.slice(lineEnd + 1); - - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') break; - - try { - const parsed = JSON.parse(data); - const content = parsed.choices?.[0]?.delta?.content; - if (content) { - cb(content); - } - } catch (e) { - // Ignore invalid JSON - this is common in SSE streams - console.warn("Failed to parse SSE data:", data, e); - } - } + const line = buffer.slice(0, lineEnd).trim(); + buffer = buffer.slice(lineEnd + 1); + + if (line.startsWith("data: ")) { + const data = line.slice(6); + if (data === "[DONE]") break; + + try { + const parsed = JSON.parse(data); + const content = parsed.choices?.[0]?.delta?.content; + if (content) { + cb(content); } + } catch (e) { + // Ignore invalid JSON - this is common in SSE streams + console.warn("Failed to parse SSE data:", data, e); } - } finally { - resolve() - reader.cancel(); } - }) -} - + } + } + } finally { + resolve(); + reader.cancel(); + } + }); +}; diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 00000000..8ce3b6a6 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1482 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "dependencies": { + "@prisma/client": "6.14.0", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "cors": "^2.8.5", + "express": "^5.1.0", + "express-rate-limit": "^8.0.1", + "hi-base32": "^0.5.1", + "jsonwebtoken": "^9.0.2", + "prisma": "^6.14.0", + "razorpay": "^2.9.6", + "totp-generator": "^1.0.0", + "ytdl-core": "^4.11.5", + "zod": "^4.0.17" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } + }, + "node_modules/@prisma/client": { + "version": "6.14.0", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.14.0", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.14.0", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.14.0", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.14.0", + "@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", + "@prisma/fetch-engine": "6.14.0", + "@prisma/get-platform": "6.14.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.14.0", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.14.0", + "@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", + "@prisma/get-platform": "6.14.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.14.0", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.14.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bun": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.21.tgz", + "integrity": "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.2.21" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.0", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/send": { + "version": "0.17.5", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "license": "BSD-3-Clause" + }, + "node_modules/bun-types": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.21.tgz", + "integrity": "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "peerDependencies": { + "@types/react": "^19" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/debug": { + "version": "4.4.1", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.16.12", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.0.1", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hi-base32": { + "version": "0.5.1", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.5.1", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jssha": { + "version": "3.3.1", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/m3u8stream": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz", + "integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==", + "license": "MIT", + "dependencies": { + "miniget": "^4.2.2", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/miniget": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.3.tgz", + "integrity": "sha512-SjbDPDICJ1zT+ZvQwK0hUcRY4wxlhhNpHL9nJOB2MEAXRGagTljsO8MEDzQMTFf0Q8g4QNi8P9lEm/g7e+qgzA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "license": "MIT" + }, + "node_modules/nypm": { + "version": "0.6.1", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/prisma": { + "version": "6.14.0", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.14.0", + "@prisma/engines": "6.14.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/razorpay": { + "version": "2.9.6", + "license": "MIT", + "dependencies": { + "axios": "^1.6.8" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/router": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.7.2", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totp-generator": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "jssha": "^3.3.1" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/ytdl-core": { + "version": "4.11.5", + "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.5.tgz", + "integrity": "sha512-27LwsW4n4nyNviRCO1hmr8Wr5J1wLLMawHCQvH8Fk0hiRqrxuIu028WzbJetiYH28K8XDbeinYW4/wcHQD1EXA==", + "license": "MIT", + "dependencies": { + "m3u8stream": "^0.8.6", + "miniget": "^4.2.2", + "sax": "^1.1.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "4.0.17", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 43dcffd2..dcf5da56 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -40,6 +40,7 @@ model Execution { enum ExecutionType { CONVERSATION ARTICLE_SUMMARIZER + YOUTUBE_SUMMARIZER } model Conversation { @@ -112,4 +113,12 @@ model ArticleSummarizer { summary String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt -} \ No newline at end of file +} + +model YoutubeSummarizer { + id String @id @default(uuid()) + videoId String + summary String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/backend/routes/apps/index.ts b/backend/routes/apps/index.ts index c971bc81..9e82e6b2 100644 --- a/backend/routes/apps/index.ts +++ b/backend/routes/apps/index.ts @@ -2,80 +2,162 @@ import { Router } from "express"; import { ArticleSummarizer } from "./article-summarizer"; import { authMiddleware } from "../../auth-middleware"; import { PrismaClient } from "../../generated/prisma"; +import { YoutubeSummarizer } from "./youtube-summarizer/youtube-summarizer"; const router = Router(); router.get("/", (req, res) => { - res.json(["article-summarizer"]); + res.json(["article-summarizer"]); }); const prismaClient = new PrismaClient(); -router.get("/article-summarizer/:executionId", authMiddleware, async (req, res) => { +router.get( + "/article-summarizer/:executionId", + authMiddleware, + async (req, res) => { const execution = await prismaClient.execution.findFirst({ - where: { - id: req.params.executionId, - userId: req.userId - } + where: { + id: req.params.executionId, + userId: req.userId, + }, }); if (!execution) { - res.status(404).json({ error: "Execution not found" }); - return; + res.status(404).json({ error: "Execution not found" }); + return; } const articleSummarizer = await prismaClient.articleSummarizer.findFirst({ - where: { - id: req.params.executionId - } + where: { + id: req.params.executionId, + }, }); if (!articleSummarizer) { - res.status(404).json({ error: "Article summarizer not found" }); - return; + res.status(404).json({ error: "Article summarizer not found" }); + return; } res.json({ - id: articleSummarizer.id, - article: articleSummarizer.article, - summary: articleSummarizer.summary, - createdAt: articleSummarizer.createdAt, - updatedAt: articleSummarizer.updatedAt + id: articleSummarizer.id, + article: articleSummarizer.article, + summary: articleSummarizer.summary, + createdAt: articleSummarizer.createdAt, + updatedAt: articleSummarizer.updatedAt, }); -}); + }, +); router.post("/article-summarizer", authMiddleware, (req, res) => { - // Set SSE headers - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Headers', 'Cache-Control'); - - const articleSummarizer = new ArticleSummarizer(); - - // Validate the input using the app's schema - const result = articleSummarizer.zodSchema.safeParse({ - ...req.body, - userId: req.userId + // Set SSE headers + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Headers", "Cache-Control"); + + const articleSummarizer = new ArticleSummarizer(); + + // Validate the input using the app's schema + const result = articleSummarizer.zodSchema.safeParse({ + ...req.body, + userId: req.userId, + }); + + if (!result.success) { + res.status(400).json({ error: result.error.message }); + return; + } + + articleSummarizer + .runStreamable(result.data as any, (chunk: string) => { + // Send data in SSE format + res.write(`data: ${chunk}\n\n`); + }) + .then(() => { + // Close the connection when streaming is complete + res.end(); + }) + .catch((error) => { + // Handle errors properly + res.write(`data: {"error": "${error.message}"}\n\n`); + res.end(); + }); +}); + +router.get( + "/youtube-summarizer/:executionId", + authMiddleware, + async (req, res) => { + const execution = await prismaClient.execution.findFirst({ + where: { + id: req.params.executionId, + userId: req.userId, + }, }); - - if (!result.success) { - res.status(400).json({ error: result.error.message }); - return; + + if (!execution) { + res.status(404).json({ error: "Execution not found" }); + return; } - - articleSummarizer.runStreamable(result.data as any, (chunk: string) => { - // Send data in SSE format - res.write(`data: ${chunk}\n\n`); - }).then(() => { - // Close the connection when streaming is complete - res.end(); - }).catch((error) => { - // Handle errors properly - res.write(`data: {"error": "${error.message}"}\n\n`); - res.end(); + + const youtubeSummarizer = await prismaClient.youtubeSummarizer.findFirst({ + where: { + id: req.params.executionId, + }, + }); + + if (!youtubeSummarizer) { + res.status(404).json({ error: "Youtube summarizer not found" }); + return; + } + + res.json({ + id: youtubeSummarizer.id, + videoId: youtubeSummarizer.videoId, + summary: youtubeSummarizer.summary, + createdAt: youtubeSummarizer.createdAt, + updatedAt: youtubeSummarizer.updatedAt, + }); + }, +); + +router.post("/youtube-summarizer", authMiddleware, (req, res) => { + // Set SSE headers + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Headers", "Cache-Control"); + + const youtubeSummarizer = new YoutubeSummarizer(); + + // Validate the input using the app's schema + const result = youtubeSummarizer.zodSchema.safeParse({ + ...req.body, + userId: req.userId, + }); + + if (!result.success) { + res.status(400).json({ error: result.error.message }); + return; + } + + youtubeSummarizer + .runStreamable(result.data as any, (chunk: string) => { + // Send data in SSE format + res.write(`data: ${chunk}\n\n`); + }) + .then(() => { + // Close the connection when streaming is complete + res.end(); + }) + .catch((error) => { + // Handle errors properly + res.write(`data: {"error": "${error.message}"}\n\n`); + res.end(); }); }); -export default router; \ No newline at end of file +export default router; diff --git a/backend/routes/apps/youtube-summarizer.ts b/backend/routes/apps/youtube-summarizer.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/routes/apps/youtube-summarizer/helper.ts b/backend/routes/apps/youtube-summarizer/helper.ts new file mode 100644 index 00000000..4b26c70f --- /dev/null +++ b/backend/routes/apps/youtube-summarizer/helper.ts @@ -0,0 +1,20 @@ +export function extractVideoId(youtube_url: string): string { + const patterns = [ + /(?:v=|\/)([0-9A-Za-z_-]{11}).*/, // Standard and shared URLs + /(?:embed\/)([0-9A-Za-z_-]{11})/, // Embed URLs + /(?:youtu\.be\/)([0-9A-Za-z_-]{11})/, // Shortened URLs + /(?:shorts\/)([0-9A-Za-z_-]{11})/, // YouTube Shorts + /^([0-9A-Za-z_-]{11})$/, // Just the video ID + ]; + + const url = youtube_url.trim(); + + for (const pattern of patterns) { + const match = url.match(pattern); + if (match) { + return match[1]; + } + } + + throw new Error("Could not extract video ID from URL"); +} diff --git a/backend/routes/apps/youtube-summarizer/youtube-summarizer.ts b/backend/routes/apps/youtube-summarizer/youtube-summarizer.ts new file mode 100644 index 00000000..23d96564 --- /dev/null +++ b/backend/routes/apps/youtube-summarizer/youtube-summarizer.ts @@ -0,0 +1,99 @@ +import { z } from "zod"; +import { PrismaClient } from "../../../generated/prisma"; +import { App, AppType } from "../app"; +import { createCompletion } from "../../../openrouter"; +import { Role } from "../../../types"; +import { extractVideoId } from "./helper"; + +const YoutubeSummarizerSchema = z.object({ + videoLink: z.string(), + userId: z.string(), +}); + +const MODEL = "google/gemini-2.5-flash:online"; +const prismaClient = new PrismaClient(); + +const SYSTEM_PROMPT = ` + You are a helpful assistant that summarizes YouTube videos. Your task is to make the web search toolcall and create clear, concise, and well-structured summaries. + Instructions: + - You will receive a YouTube video ID + - Extract and analyze the video's content, including title, description, and transcript if available + - Create a comprehensive summary that captures the main points, key insights, and important details + - Structure your summary with clear sections: overview, main points, and key takeaways + - Use bullet points or numbered lists when appropriate for better readability + - Keep the language accessible and avoid unnecessary jargon + - Aim for a summary that's informative yet concise +`; + +export class YoutubeSummarizer extends App { + constructor() { + super({ + name: "youtube Summarizer", + route: "/youtube-summarizer", + description: "Summarize a youtube video", + icon: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png", + per_execution_credit: 2, + zodSchema: YoutubeSummarizerSchema, + appType: AppType.StreamableLLM, + }); + } + + private async createExecutionRecord( + db: { execution: { create: (args: any) => Promise } }, + userId: string, + ) { + await db.execution.create({ + data: { + title: "Youtube Summarizer", + type: "YOUTUBE_SUMMARIZER", + createdAt: new Date(), + updatedAt: new Date(), + user: { + connect: { + id: userId, + }, + }, + }, + }); + } + + async runStreamable( + data: z.infer, + callback: (chunk: string) => void, + ) { + const { videoLink } = data; + const videoId = extractVideoId(videoLink); + let response = ""; + await createCompletion( + [ + { + role: Role.User, + content: videoLink, + }, + ], + MODEL, + (chunk) => { + callback(chunk); + response += chunk; + }, + SYSTEM_PROMPT, + { plugins: [{ id: "web" }] }, + ); + + try { + await prismaClient.$transaction(async (tx) => { + await tx.youtubeSummarizer.create({ + data: { + videoId: videoId, + summary: response, + }, + }); + await this.createExecutionRecord(tx as any, data.userId); + }); + } catch (error) { + console.error("Error saving article summary to database:", error); + } + + return response; + } +} diff --git a/backend/types.ts b/backend/types.ts index a637f976..1fe4baa1 100644 --- a/backend/types.ts +++ b/backend/types.ts @@ -150,5 +150,6 @@ export type Messages = Message[]; export enum Role { Agent = "assistant", - User = "user" + User = "user", + System = "system" } diff --git a/frontend/app/(app)/apps/[id]/page.tsx b/frontend/app/(app)/apps/[id]/page.tsx index 3c941ee7..3636582a 100644 --- a/frontend/app/(app)/apps/[id]/page.tsx +++ b/frontend/app/(app)/apps/[id]/page.tsx @@ -1,18 +1,7 @@ "use client"; -import React, { useState, useRef } from "react"; -import { Textarea } from "@/components/ui/textarea"; -import { Button } from "@/components/ui/button"; -import { ArrowUpIcon, SpinnerGapIcon, CopyIcon, CheckIcon } from "@phosphor-icons/react"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; -import SyntaxHighlighter from "react-syntax-highlighter"; -import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; -import { useTheme } from "next-themes"; -import { cn } from "@/lib/utils"; -import { useUser } from "@/hooks/useUser"; -import { useRouter } from "next/navigation"; - -const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:3000"; +import React from "react"; +import { AppRunner, AppRunnerConfig } from "../_components/AppRunner"; +import { getRunnerConfig } from "@/lib/apps-registry"; interface AppPageProps { params: Promise<{ id: string }>; @@ -20,315 +9,21 @@ interface AppPageProps { export default function AppPage({ params }: AppPageProps) { const [appId, setAppId] = React.useState(""); - const [input, setInput] = useState(""); - const [response, setResponse] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [copied, setCopied] = useState(false); - const [error, setError] = useState(null); - const abortControllerRef = useRef(null); - const { resolvedTheme } = useTheme(); - const { user, isLoading: isUserLoading } = useUser(); - const router = useRouter(); React.useEffect(() => { params.then(({ id }) => setAppId(id)); }, [params]); - const handleCopy = async (content: string) => { - try { - await navigator.clipboard.writeText(content); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error("Failed to copy: ", err); - } - }; - - const processStream = async (response: Response) => { - if (!response.ok) { - const errorText = await response.text(); - console.error("Error from API:", response.statusText, errorText); - setError(`Error ${response.status}: ${response.statusText}`); - setIsLoading(false); - return; - } - - try { - const reader = response.body?.getReader(); - if (!reader) { - console.error("No reader available"); - setIsLoading(false); - return; - } - - setResponse(""); // Clear previous response - let buffer = ""; - - while (true) { - const { done, value } = await reader.read(); - - if (done) { - break; - } - - const chunk = new TextDecoder().decode(value); - buffer += chunk; - - const lines = buffer.split("\n"); - buffer = lines.pop() ?? ""; - - for (const line of lines) { - if (line.trim() === "") continue; - - if (line.startsWith("data: ")) { - const data = line.substring(6); - - if (data === "[DONE]") { - continue; - } + if (!appId) { + return null; + } - try { - // The backend sends raw text chunks directly, but we need to handle potential JSON error responses - if (data.startsWith("{") && data.endsWith("}")) { - try { - const parsedData = JSON.parse(data); - if (parsedData.error) { - setResponse(prev => prev + `Error: ${parsedData.error}\n`); - continue; - } - } catch { - // If parsing fails, treat as plain text - } - } - - // For normal streaming, the data is raw text content - if (data && data !== "[DONE]") { - setResponse(prev => prev + data); - } - } catch (e) { - console.error("Error processing data:", e); - } - } - } - } - } catch (error) { - console.error("Error processing stream:", error); - setResponse("Error: Failed to process response"); - } finally { - setIsLoading(false); - abortControllerRef.current = null; - } + const config: AppRunnerConfig = getRunnerConfig(appId) ?? { + title: appId.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()), + description: `Use the ${appId} app`, + placeholder: "Enter your input...", + makeRequestBody: (input: string) => ({ input }), }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!user) { - router.push("/auth"); - return; - } - - if (!input.trim() || isLoading) return; - - setIsLoading(true); - setResponse(""); - setError(null); - - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - - abortControllerRef.current = new AbortController(); - - try { - console.log(input); - console.log(appId); - console.log(BACKEND_URL); - console.log(`${BACKEND_URL}/apps/${appId}`); - const response = await fetch(`${BACKEND_URL}/apps/${appId}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify({ - article: input, - }), - signal: abortControllerRef.current?.signal, - }); - - await processStream(response); - } catch (error) { - if ((error as Error).name !== "AbortError") { - console.error("Error sending request:", error); - setResponse("Error: Failed to send request"); - } - setIsLoading(false); - } - }; - - return ( -
-
-

- {appId ? appId.replace(/-/g, " ").replace(/\b\w/g, l => l.toUpperCase()) : "App"} -

-

- {appId === "article-summarizer" - ? "Enter article text to get a summary" - : `Use the ${appId} app` - } -

- -
-
-