diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..035290e3 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Dependant on what stack you want to run + +OPENAI_API_KEY=sk- +HELICONE_API_KEY=sk- +PINECONE_API_KEY= +PINECONE_ENVIRONMENT= +# SUPABASE_URL= +# SUPABASE_KEY= +GEMINI_API_KEY= \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d28ac535..0e7abd47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,9 +41,6 @@ jobs: - name: Install Dependencies run: npm install - - name: Run TypeScript Compiler - run: tsc - - name: Fetch Latest Changes run: git fetch diff --git a/.github/workflows/pullrequest_validation.yml b/.github/workflows/pullrequest_validation.yml new file mode 100644 index 00000000..dfcefb13 --- /dev/null +++ b/.github/workflows/pullrequest_validation.yml @@ -0,0 +1,52 @@ +name: Validate Pull Request + +on: + pull_request: + types: + - opened + - synchronize + +jobs: + check-format: + name: Validate Stack PR + + # Only run if the PR branch starts with `stack/` + if: startsWith(github.head_ref, 'stack/') + + runs-on: ubuntu-latest # can also change to: windows-latest || macos-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # OR "2" -> To retrieve the preceding commit. + + - name: Get all changed files + id: changed-files + uses: tj-actions/changed-files@v40 + + - name: List all changed files + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + echo "$file was changed" + done + + - name: Fail if unexpected files were changed + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + if [[ ! "$file" =~ ^(stacks/[^/]*/index.test.ts|stacks/[^/]*/index.ts|stacks/[^/]*/index.test.txt)$ ]]; then + echo "😡 File '$file' was changed. This is not allowed in a stack Pull Request." + echo "" + echo "You're only allowed to change the following:" + echo " - stacks/*/index.ts" + echo " - stacks/*/index.test.ts" + echo " - stacks/*/index.test.txt" + exit 1 + fi + done + + - name: Fail if more than one (1) stack was made + run: | + if ((${{ steps.changed-files.outputs.all_changed_files_count }} > 3)); then + echo "😡 Detected more than 1 stack. Please make only 1 stack per pull request." + exit 1 + fi diff --git a/.gitignore b/.gitignore index 9daa8247..5d37bd0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +.vscode +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc .DS_Store -node_modules +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.env \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index f06e0928..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Change Log - -All notable changes to the "stackwise" extension will be documented in this file. - -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. - -## [Unreleased] - -- Initial release diff --git a/README.md b/README.md index 85d186d0..8c09e8fa 100644 --- a/README.md +++ b/README.md @@ -1,194 +1,19 @@ -# Stackwise: Explain what you want a function to do, and AI builds it. +# The open source AI app collection. [![Discord Follow](https://dcbadge.vercel.app/api/server/KfUxa8h3s6?style=flat)](https://discord.gg/KfUxa8h3s6) [![Twitter Follow](https://img.shields.io/twitter/follow/stackwiseai?style=social)](https://twitter.com/stackwiseai) [![GitHub Repo stars](https://img.shields.io/github/stars/stackwiseai/stackwise?style=social)](https://github.com/stackwiseai/stackwise/stargazers) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -Stackwise is a VS Code extension that automatically writes and imports nodejs functions so that you can write code without context switching. +### [Visit the Stackwise collection](https://stackwise.ai/stacks) -### Usage - -Stackwise introduces a straightforward command structure, where you specify a 'brief' for the desired action, along with the inputs and outputs: - -```typescript -stack("brief describing a specific action", { - in: /* single input or object {} with multiple inputs */, - out: // same as above, but output -}) -``` - -This approach streamlines API interactions, reducing the need for intricate coding and extensive API knowledge. Fully typed, and editable within your repo. - -Upon saving your command, Stackwise replaces it with a collapsed function with your inputs and an import statement at the top of your file. The generated code resides in the /stacks directory within your project root, ensuring clean and maintainable codebase. - -### Special Note for NextJS Developers - -NextJS developers can leverage Stackwise for server actions, making API integrations even smoother. Here’s an example demonstrating the use of Stackwise in a NextJS environment: - -![example image](example.png) - -### Current Integrations - -Stackwise currently integrates with three APIs: - -- Replicate -- OpenAI -- Pinecone - -Contributions to improve these or add new integrations are welcome. If you're interested in expanding Stackwise's capabilities, feel free to submit a pull request or contact us (join the Discord or contact@stackwise.ai) for collaboration. - -### Getting Started - -Prerequisites: - -- Typescript project -- openai api key -- pinecone api key - -To start using Stackwise, follow these steps: - -1. Clone the Stackwise repository: - -```bash -git clone https://github.com/stackwiseai/stackwise.git -``` - -2. cd stackwise -3. npm install -4. copy the launch.json.example into launch.json and fill these environment variables: - -- PINECONE_API_KEY -- PINECONE_ENVIRONMENT=PINECONE_ENVIRONMENT -- OPENAI_API_KEY=OPENAI_API_KEY - -5. Click Run and Debug -> Click on the play button "Run Extension" -6. Open your typescript project. In your typescript project, type: - -```typescript -const prompt = "What's the capital of the United States ?" -result = await stack( - "Ask a question to GPT-4", - { - in: prompt - output: "Washington D.C" - } -) -``` - -7. Save your file. The function should collapse into something like this. - const prompt = "What's the capital of the United States ?" - -```typescript -const result = await askGPT4(prompt); -``` - -8. You can cmd + click (ctrl + click on windows) on the function to see the code of the function. -9. If you don't like the code, edit it. - -### How to be a 'Stacker' - - - -A **stacker** is someone who creates _stacks_—these are essentially training data -to make **Stackwise** even smarter! As a stacker, you have an important role in -improving the accuracy and usefulness of Stackwise's results. - -Stacks are stored in the `stacks/` folder of this repository. This folder is like -a completely new node project with its own `package.json`, `node_modules`, and -test environment. - -In order to **Get Started** writing _stacks_, do the following first: - -```sh -# Clone and change directory into the repo if you haven't yet. -git clone https://github.com//stackwise -cd stackwise - -# Change directory into the 'stacks/' folder. -cd stacks - -# Install the dependencies -npm install -``` - -Each _stack_ is a folder following this folder structure: - -```sh -stacks/ -└─ / - ├─ index.test.ts # Unit-Test Collapsed by Stackwise. - ├─ index.test.txt # Unit-Test that isn't collapsed by Stackwise yet. - └─ index.ts # Your stack's function implementation. -``` - -- **index.test.ts** - This is the file where you write the testcases for your - function implementation. It should already be as if it was already collapsed - by Stackwise. Example: - - ```ts - import addTwoNumbers from "."; - - test("Add two numbers", async () => { - const firstNumber = 1; - const secondNumber = 2; - - const result = addTwoNumbers(firstNumber, secondNumber); - - expect(result).toBe(2); - }); - ``` - -- **index.test.txt** - This is the file that is the same as `index.test.ts`, but - not yet collapsed by Stackwise. Example: - - ```ts - test("Add two numbers", async () => { - const firstNumber = 1; - const secondNumber = 2; - - const result = stack('Add two numbers', { - in: { - firstNumber, - secondNumber, - }, - out: 2 - }); - - expect(result).toBe(2); - }); - ``` - -- **index.ts** - is the file where you write the function implementation. - ```ts - export default async function addTwoNumbers(firstNumber, secondNumber) { - return firstNumber + secondNumber; - } - ``` - -When you're done, just make a pull request to the official stackwise repo. - -### Roadmap - -Our future developments include: - -- [ ] Interactive stack modification: Chat with your stack to refine and improve it. - -- [ ] Dynamic input chains: Link multiple stacks to each other using dynamic inputs. Currently only static briefs work. - -- [ ] Expand integrations: Continually adding more APIs based on community feedback. Let us know what you want to see next! - -- [ ] API insights: See what APIs you're calling most and how what you're using them for. - -- [ ] Non-collapsible stacks: Edit your stacks continuously without automatic collapsing. If something with the api changes, it will automatically heal to work. +![Stackwise stacks collections](public/stacks_homepage.png) ### Join The Community [![Discord Follow](https://dcbadge.vercel.app/api/server/KfUxa8h3s6?style=flat)](https://discord.gg/KfUxa8h3s6) -We welcome contributions, feedback, and suggestions to further enhance Stackwise. - -We want to make API integration easy, without the hassle of reading documentation or ever leaving your IDE. If you made it here you're at the very least intruiged and we'd love to have you :) +We welcome contributions, feedback, and suggestions to further enhance Stackwise. If you made it here you're at the very least intrigued and we'd love to have you :) --- diff --git a/app/api/stacks/basic-openai/route.ts b/app/api/stacks/basic-openai/route.ts new file mode 100644 index 00000000..e3d95f7a --- /dev/null +++ b/app/api/stacks/basic-openai/route.ts @@ -0,0 +1,15 @@ +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +export async function POST(req: Request) { + const { messages } = await req.json(); + const response = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [{ role: 'user', content: messages }], + }); + const content = response.choices[0].message.content; + return new Response(JSON.stringify({ content })); +} diff --git a/app/api/stacks/boilerplate-basic/route.ts b/app/api/stacks/boilerplate-basic/route.ts new file mode 100644 index 00000000..d063e77a --- /dev/null +++ b/app/api/stacks/boilerplate-basic/route.ts @@ -0,0 +1,6 @@ +export async function POST(req: Request) { + const { input } = await req.json(); + return new Response( + JSON.stringify({ output: `You sent this message to the server: ${input}` }), + ); +} diff --git a/app/api/stacks/chat-gemini-streaming-langchain/route.ts b/app/api/stacks/chat-gemini-streaming-langchain/route.ts new file mode 100644 index 00000000..d7e14754 --- /dev/null +++ b/app/api/stacks/chat-gemini-streaming-langchain/route.ts @@ -0,0 +1,30 @@ +import { ChatGoogleGenerativeAI } from '@langchain/google-genai'; + +export async function POST(req: Request) { + const chat = new ChatGoogleGenerativeAI(); + const { messages } = await req.json(); + + try { + const textResponse = await chat.invoke([['human', messages]]); + const stream = await chat.stream([ + ['human', 'Tell me a joke about bears.'], + ]); + + for await (const chunk of stream) { + console.log(chunk); + } + const output = textResponse.content; // assuming the text you want is in `content` + + const responseJson = JSON.stringify({ output }); + + return new Response(responseJson, { + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.error(error); + return new Response(JSON.stringify({ error: 'An error occurred' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} diff --git a/app/api/stacks/chat-with-gemini-langchain/route.ts b/app/api/stacks/chat-with-gemini-langchain/route.ts new file mode 100644 index 00000000..01c415e1 --- /dev/null +++ b/app/api/stacks/chat-with-gemini-langchain/route.ts @@ -0,0 +1,23 @@ +import { ChatGoogleGenerativeAI } from '@langchain/google-genai'; + +export async function POST(req: Request) { + const llm = new ChatGoogleGenerativeAI(); + const { messages } = await req.json(); + + try { + const textResponse = await llm.invoke([['human', messages]]); + const output = textResponse.content; // assuming the text you want is in `content` + + const responseJson = JSON.stringify({ output }); + + return new Response(responseJson, { + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.error(error); + return new Response(JSON.stringify({ error: 'An error occurred' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} diff --git a/app/api/stacks/chat-with-gemini-streaming/route.ts b/app/api/stacks/chat-with-gemini-streaming/route.ts new file mode 100644 index 00000000..20a1267b --- /dev/null +++ b/app/api/stacks/chat-with-gemini-streaming/route.ts @@ -0,0 +1,84 @@ +import { + AIStreamCallbacksAndOptions, + createCallbacksTransformer, + createStreamDataTransformer, + readableFromAsyncIterable, + StreamingTextResponse, +} from 'ai'; + +export async function POST(req: Request) { + const { GoogleGenerativeAI } = require('@google/generative-ai'); + const { messages } = await req.json(); + + const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); + const model = genAI.getGenerativeModel({ model: 'gemini-pro' }); + const geminiStream = await model.generateContentStream(messages); + const stream = GoogleGeminiStream(geminiStream); + + // Respond with the stream + return new StreamingTextResponse(stream); +} + +interface GenerateContentResponse { + candidates: GenerateContentCandidate[]; +} + +interface GenerateContentCandidate { + content: Content; + index?: number; +} + +interface Content { + parts: Part[]; +} + +type Part = TextPart | InlineDataPart | FileDataPart; + +interface TextPart { + text: string; + inline_data?: never; +} + +interface InlineDataPart { + text?: never; + inline_data: GenerativeContentBlob; +} + +interface GenerativeContentBlob { + mime_type: string; + data: string; +} + +interface FileDataPart { + text?: never; + file_data: FileData; +} + +interface FileData { + mime_type: string; + file_uri: string; +} + +async function* streamable(response: { + stream: AsyncGenerator; +}) { + for await (const chunk of response.stream) { + const parts = chunk.candidates[0].content.parts; + const firstPart = parts[0]; + + if (typeof firstPart.text === 'string') { + yield firstPart.text; + } + } +} + +function GoogleGeminiStream( + response: { + stream: AsyncGenerator; + }, + cb?: AIStreamCallbacksAndOptions, +): ReadableStream { + return readableFromAsyncIterable(streamable(response)) + .pipeThrough(createCallbacksTransformer(cb)) + .pipeThrough(createStreamDataTransformer(cb?.experimental_streamData)); +} diff --git a/app/api/stacks/chat-with-gemini/route.ts b/app/api/stacks/chat-with-gemini/route.ts new file mode 100644 index 00000000..2ef92aea --- /dev/null +++ b/app/api/stacks/chat-with-gemini/route.ts @@ -0,0 +1,20 @@ +import { StreamingTextResponse } from 'ai'; + +export async function POST(req: Request) { + const { GoogleGenerativeAI } = require('@google/generative-ai'); + const { messages } = await req.json(); + + const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); + const model = genAI.getGenerativeModel({ model: 'gemini-pro' }); + + try { + const result = await model.generateContent(messages); + const response = await result.response; + const text = await response.text(); + return new StreamingTextResponse(text); + } catch (error) { + // Handle the error here + console.error('Error occurred:', error); + return new StreamingTextResponse(error); + } +} diff --git a/app/api/stacks/chat-with-openai-streaming-helicone/route.ts b/app/api/stacks/chat-with-openai-streaming-helicone/route.ts new file mode 100644 index 00000000..5b8694a9 --- /dev/null +++ b/app/api/stacks/chat-with-openai-streaming-helicone/route.ts @@ -0,0 +1,21 @@ +import { OpenAIStream, StreamingTextResponse } from 'ai'; +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: 'https://oai.hconeai.com/v1', + defaultHeaders: { + 'Helicone-Auth': `Bearer ${process.env.HELICONE_API_KEY}`, + }, +}); +export const runtime = 'edge'; +export async function POST(req: Request) { + const { messages } = await req.json(); + const response = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + stream: true, + messages: [{ role: 'user', content: messages }], + }); + const stream = OpenAIStream(response); + return new StreamingTextResponse(stream); +} diff --git a/app/api/stacks/chat-with-openai-streaming-langchain/route.ts b/app/api/stacks/chat-with-openai-streaming-langchain/route.ts new file mode 100644 index 00000000..2f150743 --- /dev/null +++ b/app/api/stacks/chat-with-openai-streaming-langchain/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Message as VercelChatMessage, StreamingTextResponse } from "ai"; + +import { ChatOpenAI } from "langchain/chat_models/openai"; +import { BytesOutputParser } from "langchain/schema/output_parser"; +import { PromptTemplate } from "langchain/prompts"; + +export const runtime = "edge"; + +const formatMessage = (message: VercelChatMessage) => { + return `${message.role}: ${message.content}`; +}; + +const TEMPLATE = ` +Current conversation: +{chat_history} + +User: {input} +AI:`; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const messages = body.messages ?? []; + const formattedPreviousMessages = messages.slice(0, -1).map(formatMessage); + const currentMessageContent = messages[messages.length - 1].content; + const prompt = PromptTemplate.fromTemplate(TEMPLATE); + + + const model = new ChatOpenAI({ + temperature: 0.8, + }); + + const outputParser = new BytesOutputParser(); + const chain = prompt.pipe(model).pipe(outputParser); + + const stream = await chain.stream({ + chat_history: formattedPreviousMessages.join("\n"), + input: currentMessageContent, + }); + + return new StreamingTextResponse(stream); + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/stacks/chat-with-openai-streaming/route.ts b/app/api/stacks/chat-with-openai-streaming/route.ts new file mode 100644 index 00000000..768aab41 --- /dev/null +++ b/app/api/stacks/chat-with-openai-streaming/route.ts @@ -0,0 +1,17 @@ +import { OpenAIStream, StreamingTextResponse } from 'ai'; +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); +export const runtime = 'edge'; +export async function POST(req: Request) { + const { messages } = await req.json(); + const response = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + stream: true, + messages: [{ role: 'user', content: messages }], + }); + const stream = OpenAIStream(response); + return new StreamingTextResponse(stream); +} diff --git a/app/api/stacks/cover-image-and-subtitle/route.ts b/app/api/stacks/cover-image-and-subtitle/route.ts new file mode 100644 index 00000000..f2dcfe10 --- /dev/null +++ b/app/api/stacks/cover-image-and-subtitle/route.ts @@ -0,0 +1,220 @@ +import { spawn } from 'child_process'; +import fs from 'fs'; +import { Readable } from 'stream'; +import AWS from 'aws-sdk'; +import OpenAI from 'openai'; +import Replicate from 'replicate'; +import { v4 as uuidv4 } from 'uuid'; + +AWS.config.update({ + region: process.env.AWS_REGION, + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, +}); + +const s3 = new AWS.S3(); +const transcoder = new AWS.ElasticTranscoder(); + +export const maxDuration = 300; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const replicate = new Replicate({ + auth: process.env.REPLICATE_API_TOKEN as string, +}); + +// Function to check the status of a transcoding job +const checkTranscodeJobStatus = async (jobId) => { + const params = { Id: jobId }; + return transcoder.readJob(params).promise(); +}; + +// Function to wait for the job to complete +const waitForJobCompletion = async ( + jobId, + interval = 1000, + timeout = 30000, +) => { + let timePassed = 0; + + while (timePassed < timeout) { + const { Job } = await checkTranscodeJobStatus(jobId); + if (Job) { + console.log('Job status:', Job.Status); + if (Job.Status === 'Complete') { + return true; + } else if (Job.Status === 'Error') { + // Log or return the specific error message from the job + throw new Error(`Transcoding job failed from an Error`); + } + + // Wait for the specified interval before checking again + await new Promise((resolve) => setTimeout(resolve, interval)); + timePassed += interval; + } else { + throw new Error('Transcoding job failed: Job status not available'); + } + } + + throw new Error('Transcoding job timed out'); +}; + +const startTranscodeJob = async (inputKey, outputKey, pipelineId, presetId) => { + const params = { + PipelineId: pipelineId, + Input: { Key: inputKey }, + Outputs: [{ Key: outputKey, PresetId: presetId }], + }; + return transcoder.createJob(params).promise(); +}; + +// Function to get the base64 string of the audio file +const getAudioBase64 = async (bucketName, audioKey) => { + const params = { + Bucket: bucketName, + Key: audioKey, + }; + try { + const data = await s3.getObject(params).promise(); + if (data.Body) { + return data.Body.toString('base64'); + } else { + throw new Error('No data body in response'); + } + } catch (error) { + console.error('Error getting audio base64:', error); + throw error; // Re-throw the error for handling it in the calling function + } +}; + +const createAudioFile = async (fileName: string): Promise => { + const pipelineId = '1705538698802-kk2tc9'; // Replace with your pipeline ID + const presetId = '1705539076861-v8ozl7'; // MP3 preset ID + const inputBucket = 'cover-image-and-subtitle-stack'; // Your input bucket name + const outputBucket = 'cover-image-and-subtitle-stack'; // Your output bucket name + const inputKey = fileName; + // Replace only the last occurrence of .mp4 with .mp3 + let outputKey = fileName.replace(/\.mp4$/, '.mp3'); + + try { + // Check if the output file already exists + try { + await s3.headObject({ Bucket: outputBucket, Key: outputKey }).promise(); + // File exists, create a new unique name + outputKey = `${fileName.split('.')[0]}-${Date.now()}.mp3`; + } catch (error) { + if (error.statusCode !== 404) { + throw error; // An error other than 'Not Found' + } + // If error is 404 (Not Found), it means file does not exist and we can proceed + } + // Start the transcoding job + const transcodeResponse = await startTranscodeJob( + inputKey, + outputKey, + pipelineId, + presetId, + ); + + if (transcodeResponse.Job) { + // Wait for the job to complete + await waitForJobCompletion(transcodeResponse.Job.Id); + + // Get the base64 encoded audio string + const audioBase64 = await getAudioBase64(outputBucket, outputKey); + + return `data:audio/mp3;base64,${audioBase64}`; + } else { + throw new Error('No job ID in response'); + } + } catch (error) { + console.error('Error in createAudioFile:', error); + return ''; + } +}; + +export async function POST(req: Request) { + try { + const body = await req.json(); + + //extract the audio and create an audiofile from the video buffer + const audioUri = await createAudioFile(body.fileName); + + // send the audio to replicate and getback the subtitles and long descrp + const output = await replicate.run( + 'm1guelpf/whisper-subtitles:7f686e243a96c7f6f0f481bcef24d688a1369ed3983cea348d1f43b879615766', + { + input: { + format: 'vtt', + audio_path: audioUri, + model_name: 'base', + }, + }, + ); + + if (!output || !('text' in output) || !('subtitles' in output)) { + return Response.json({ + message: 'Some Error occured in fetching subtitles !!', + }); + } + + console.log('Subtitle has beeen generate Successfully !!'); + + // send the long prompt to openAI and get back a short summary + + const prompt = output.text as string; + const subtitle = output.subtitles as string; + + const completion = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'system', + content: `You are a subtitle summarizer. You will be given a large paragraph of subtitles from a video. Your goal is to summarize the video based on what was said within. Keep the summary to only a few sentences and not more than that. Give the summary directly, don't use words like "Okay,Sure" or "The paragraph" or "author" or any other words about the author or speaker. Here are the original subtitles ${prompt}.`, + }, + ], + }); + + const summarizedText = completion.choices[0]?.message?.content; + + if (!summarizedText) { + return Response.json({ message: 'Summarizer is missing !!!' }); + } + + console.log('Generated Short Description !'); + + // send the short summary to replicate and generate back an image + const imgArr = await replicate.run( + 'stability-ai/stable-diffusion:ac732df83cea7fff18b8472768c88ad041fa750ff7682a21affe81863cbe77e4', + { + input: { + prompt: `Generate a thumbnail for the following content. It should be scenic with no text on it, make sure it ABSOLUTELY does not have any text embedded on it. Understand the following prompt and generate a high quality image without any text: ${summarizedText}`, + width: 1024, + height: 576, + scheduler: 'K_EULER', + }, + }, + ); + + if (!imgArr || !imgArr[0]) { + return Response.json({ + message: 'Some Error occured in Image Generation !!', + }); + } + + console.log('The Image generated Successfully !!!'); + const imgUrl = imgArr[0] as string; + + return Response.json({ + message: 'The Audio has been extracted and stored in the server !', + subtitle, + imgUrl, + summarizedText, + }); + } catch (error) { + console.error(error); + return Response.error(); + } +} diff --git a/app/api/stacks/create-stack-boilerplate/create-stack.ts b/app/api/stacks/create-stack-boilerplate/create-stack.ts new file mode 100644 index 00000000..6e2fe188 --- /dev/null +++ b/app/api/stacks/create-stack-boilerplate/create-stack.ts @@ -0,0 +1,83 @@ +import { getSupabaseClient } from '@/app/components/stacks/utils/stack-db'; + +import getFileFromGithub from './get-file-from-github'; +import pushMultipleFilesToBranch from './push-multiple-files-to-branch'; + +export default async function createStack(data, token) { + const stackId = data.name + .replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/\s+/g, '-') + .toLowerCase(); + + const supabase = await getSupabaseClient(); + const stackInfo = { + name: data.name, + id: stackId, + description: data.description, + tags: ['draft'], + }; + const { data: insertedData, error } = await supabase + .from('stack') + .insert([stackInfo]) + .single(); + if (error) { + if (error.message.includes('duplicate key ')) { + throw new Error('This app already exists.'); + } + throw error; + } + + // creating api key + let path = `ui/app/components/stacks/${stackInfo.id}.tsx`; + let message = `Frontend For ${stackInfo.id} created`; + let response = await getFileFromGithub( + 'ui/public/stacks/boilerplate-basic.tsx', + token, + ); + await new Promise((resolve) => setTimeout(resolve, 500)); + + let filesArray = [ + { + path: path, + sha: response.content, + message: message, + }, + ]; + + path = `ui/app/api/stacks/${stackInfo.id}/route.ts`; + message = `Backend For ${stackInfo.id} created`; + response = await getFileFromGithub( + 'ui/public/stacks/boilerplate-basic/route.ts', + token, + ); + await new Promise((resolve) => setTimeout(resolve, 500)); + + filesArray.push({ + path: path, + sha: response.content, + message: message, + }); + + path = `ui/public/stack-pictures/${stackInfo.id}.png`; + message = `Preview For ${stackInfo.id} created`; + + response = await getFileFromGithub( + 'ui/public/stack-pictures/boilerplate-basic.png', + token, + ); + await new Promise((resolve) => setTimeout(resolve, 500)); + + filesArray.push({ + path: path, + sha: response.content, + message: message, + }); + // const sourceBranch = process.env.VERCEL_GIT_COMMIT_REF ?? ''; // or 'master', depending on your repository + + const prLink = await pushMultipleFilesToBranch( + filesArray, + stackInfo.id, + token, + ); + return prLink; +} diff --git a/app/api/stacks/create-stack-boilerplate/get-file-from-github.ts b/app/api/stacks/create-stack-boilerplate/get-file-from-github.ts new file mode 100644 index 00000000..bc6dc014 --- /dev/null +++ b/app/api/stacks/create-stack-boilerplate/get-file-from-github.ts @@ -0,0 +1,14 @@ +export default async function getFileFromGithub(path, token) { + const owner = 'stackwiseai'; + const repo = 'stackwise'; + const sourceBranch = process.env.VERCEL_GIT_COMMIT_REF ?? ''; // or 'master', depending on your repository + + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=main`; + console.log(url, 'url'); + const response = await fetch(url, { + headers: { + Authorization: `token ${token}`, + }, + }); + return response.json(); +} diff --git a/app/api/stacks/create-stack-boilerplate/push-multiple-files-to-branch.ts b/app/api/stacks/create-stack-boilerplate/push-multiple-files-to-branch.ts new file mode 100644 index 00000000..f6d37dbd --- /dev/null +++ b/app/api/stacks/create-stack-boilerplate/push-multiple-files-to-branch.ts @@ -0,0 +1,80 @@ +import { Octokit } from '@octokit/rest'; + +const owner = 'stackwiseai'; +const repo = 'stackwise'; +const sourceBranch = process.env.VERCEL_GIT_COMMIT_REF ?? ''; // or 'master', depending on your repository +export const fetchCache = 'force-no-store'; // TODO: remove this line to enable caching but without making the app completely static +export const revalidate = 0; + +export default async function pushMultipleFilesToBranch( + filesArray, + branch, + token +) { + + const octokit = new Octokit({ auth: token }); + + try { + const response = await octokit.rest.repos.createFork({ + owner, + repo, + }); + + await octokit.rest.activity.starRepoForAuthenticatedUser({ + owner: owner, + repo: repo + }); + const { data: user } = await octokit.users.getAuthenticated(); + const forkedRepoOwner = user.login; + console.log(user, 'user'); + // wait 2 seconds + await new Promise((resolve) => setTimeout(resolve, 2000)); + const { data: refData } = await octokit.git.getRef({ + owner: forkedRepoOwner, + repo: repo, + ref: `heads/main` + }); + const sha = refData.object.sha; + + // Create a new branch using the SHA + + try { + await octokit.git.createRef({ + owner: forkedRepoOwner, + repo: repo, + ref: `refs/heads/${branch}`, + sha: sha + }); + } catch (error) { + console.log("branch already exists "); + } + + for (let i = 0; i < filesArray.length; i++) { + const file = filesArray[i]; + const response = await octokit.rest.repos.createOrUpdateFileContents({ + owner: forkedRepoOwner, + repo, + path: file.path, + branch: branch, + message: file.message, + content: file.sha, + }); + //wait .5 seconds + await new Promise((resolve) => setTimeout(resolve, 500)); + console.log('response', response); + } + + const { data } = await octokit.pulls.create({ + owner: "stackwiseai", + repo, + title: `Create stack ${branch}`, + head: `${forkedRepoOwner}:${branch}`, + base: "main", + body:"", + }); + + return data.html_url; + } catch (error) { + console.error('Error pushing multiple files to branch:', error); + } +} diff --git a/app/api/stacks/create-stack-boilerplate/route.ts b/app/api/stacks/create-stack-boilerplate/route.ts new file mode 100644 index 00000000..f63b06db --- /dev/null +++ b/app/api/stacks/create-stack-boilerplate/route.ts @@ -0,0 +1,46 @@ +import { Octokit } from '@octokit/rest'; + +import createStack from './create-stack'; + +const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); + +const vercelToken = process.env.VERCEL_TOKEN; +const teamId = process.env.TEAM_ID; +const repoId = process.env.REPO_ID; + +const owner = 'stackwiseai'; +const repo = 'stackwise'; +const sourceBranch = process.env.VERCEL_GIT_COMMIT_REF ?? ''; // or 'master', depending on your repository +export const fetchCache = 'force-no-store'; // TODO: remove this line to enable caching but without making the app completely static +export const revalidate = 0; +export async function POST(req: Request) { + const data = await req.json(); + // get the token from header and strip the Bearer + try { + if (req.headers) { + const header = req.headers.get('Authorization'); + if (header) { + const token = header.split(' ')[1]; + const prLink = await createStack(data, token); + return new Response(JSON.stringify({prLink}), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); + } else { + throw new Error('No token provided'); + + }} else { + throw new Error('No headers provided'); + } + } catch (error) { + console.error('Error during data insertion:', error); + return new Response(JSON.stringify({ message: error.message }), { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + }); + } +} diff --git a/app/api/stacks/elevenlabs-tts/route.ts b/app/api/stacks/elevenlabs-tts/route.ts new file mode 100644 index 00000000..3dfffd4b --- /dev/null +++ b/app/api/stacks/elevenlabs-tts/route.ts @@ -0,0 +1,96 @@ +import fs from 'fs'; +import { Readable } from 'stream'; + +const voice = require('elevenlabs-node'); +const axios = require('axios'); + +// export const runtime = 'edge' +const elevenLabsAPI = 'https://api.elevenlabs.io/v1'; + +// Need to adapt from elevenlabs-node because of https://github.com/FelixWaweru/elevenlabs-node/issues/16 +const textToSpeech = async ( + apiKey: string | undefined, + voiceID: string, + fileName: string, + textInput: string, + stability?: number, + similarityBoost?: number, + modelId?: string, +) => { + try { + if (!apiKey || !voiceID || !fileName || !textInput) { + console.log( + 'ERR: Missing parameter', + apiKey, + voiceID, + fileName, + textInput, + ); + } + + const voiceURL = `${elevenLabsAPI}/text-to-speech/${voiceID}`; + const stabilityValue = stability ? stability : 0; + const similarityBoostValue = similarityBoost ? similarityBoost : 0; + + const response = await axios({ + method: 'POST', + url: voiceURL, + data: { + text: textInput, + voice_settings: { + stability: stabilityValue, + similarity_boost: similarityBoostValue, + }, + model_id: modelId ? modelId : undefined, + }, + headers: { + Accept: 'audio/mpeg', + 'xi-api-key': apiKey, + 'Content-Type': 'application/json', + }, + responseType: 'stream', + }); + + return new Promise((resolve, reject) => { + const writeStream = fs.createWriteStream(fileName); + response.data.pipe(writeStream); + + writeStream.on('finish', () => resolve(fileName)); + writeStream.on('error', reject); + }); + } catch (error) { + console.log(error); + } +}; + +export async function POST(req: Request) { + const json = await req.json(); + console.log(json); + const { text } = json; + + const apiKey = process.env.ELEVEN_LABS_API_KEY; + const voiceID = 'ErXwobaYiN019PkySvjV'; + const filePath = '/tmp/audio.mp3'; + + try { + await textToSpeech(apiKey, voiceID, filePath, text).then((res) => { + console.log(res); + }); + // Stream the audio file + // Create the stream from the audio file + const audioStream = fs.createReadStream(filePath); + + audioStream.on('end', () => { + fs.unlinkSync(filePath); // Delete the file after streaming + }); + + // @ts-ignore + const response = new Response(Readable.from(audioStream), { + headers: { 'Content-Type': 'audio/mpeg' }, + }); + return response; + } catch (error) { + console.error(error); + // res.status(500).json({error: 'Error generating audio'}); + } +} diff --git a/app/api/stacks/get-image-description-openai/route.ts b/app/api/stacks/get-image-description-openai/route.ts new file mode 100644 index 00000000..724c77f3 --- /dev/null +++ b/app/api/stacks/get-image-description-openai/route.ts @@ -0,0 +1,45 @@ +import { OpenAIStream, StreamingTextResponse } from 'ai'; +import OpenAI from 'openai'; + +// Create an OpenAI API client (that's edge friendly!) +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY || '', +}); + +// IMPORTANT! Set the runtime to edge +export const runtime = 'edge'; + +export async function POST(req: Request) { + // 'data' contains the additional data that you have sent: + const { messages, data } = await req.json(); + + const initialMessages = messages.slice(0, -1); + const currentMessage = messages[messages.length - 1]; + + // Ask OpenAI for a streaming chat completion given the prompt + const response = await openai.chat.completions.create({ + model: 'gpt-4-vision-preview', + stream: true, + max_tokens: 150, + messages: [ + ...initialMessages, + { + ...currentMessage, + content: [ + { type: 'text', text: currentMessage.content }, + + // forward the image information to OpenAI: + { + type: 'image_url', + image_url: data.imageUrl, + }, + ], + }, + ], + }); + + // Convert the response into a friendly text-stream + const stream = OpenAIStream(response); + // Respond with the stream + return new StreamingTextResponse(stream); +} diff --git a/app/api/stacks/image-sharpener/route.ts b/app/api/stacks/image-sharpener/route.ts new file mode 100644 index 00000000..f00a6269 --- /dev/null +++ b/app/api/stacks/image-sharpener/route.ts @@ -0,0 +1,36 @@ +import Replicate from 'replicate'; + +const replicate = new Replicate({ + auth: process.env.REPLICATE_API_TOKEN!, +}); + +export const maxDuration = 300; + +export async function POST(request: Request) { + const form = await request.formData(); + const imgFile = form.get('img') as Blob; + const imgBuffer = Buffer.from(await imgFile.arrayBuffer()); + const imgBase64 = imgBuffer.toString('base64'); + const imgUri = `data:${imgFile.type};base64,${imgBase64}`; + + try { + const esrganVersion = + 'nightmareai/real-esrgan:42fed1c4974146d4d2414e2be2c5277c7fcf05fcc3a73abf41610695738c1d7b'; + const esrgan = await replicate.run(esrganVersion, { + input: { + image: imgUri, + face_enhance: true, + }, + }); + + return new Response(JSON.stringify(esrgan), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.log(error); + return new Response(JSON.stringify({ error }), { + status: 500, + }); + } +} diff --git a/app/api/stacks/image-to-music/route.ts b/app/api/stacks/image-to-music/route.ts new file mode 100644 index 00000000..8f1dae16 --- /dev/null +++ b/app/api/stacks/image-to-music/route.ts @@ -0,0 +1,87 @@ +import Replicate from 'replicate'; + +const replicate = new Replicate({ + auth: process.env.REPLICATE_API_TOKEN!, +}); + +export const maxDuration = 300; + +export async function POST(request: Request) { + const form = await request.formData(); + const musicLength = Number(form.get('length')); + const imgFile = form.get('img') as Blob; + const imgBuffer = Buffer.from(await imgFile.arrayBuffer()); + const imgBase64 = imgBuffer.toString('base64'); + const imgUri = `data:${imgFile.type};base64,${imgBase64}`; + + try { + const llavaVersion = + 'yorickvp/llava-13b:e272157381e2a3bf12df3a8edd1f38d1dbd736bbb7437277c8b34175f8fce358'; + const llava: string[] = (await replicate.run(llavaVersion, { + input: { + image: imgUri, + prompt: `Describe what kind of music this image invokes. Give a brief few word description of the image, then comment on the composition of musical elements to recreate this image through music. +Example responses: +Description: Sunrise illuminating a mountain range, with rays of light breaking through clouds, creating a scene of awe and grandeur. +Music: Edo25 major G melodies that sound triumphant and cinematic, leading up to a crescendo that resolves in a 9th harmonic, beginning with a gentle, mysterious introduction that builds into an epic, sweeping climax. + +Description: A cozy, dimly lit room with a warm ambience, filled with soft shadows and a sense of quiet introspection. +Music: A jazz piece in B flat minor with a smooth saxophone solo, featuring complex rhythms and a moody, reflective atmosphere, starting with a soft, contemplative melody that evolves into an expressive, passionate finale. + +Description: A bustling, neon-lit metropolis at night, alive with vibrant energy and a sense of futuristic progress. +Music: A techno track in A minor, characterized by fast-paced electronic beats, a pulsating bassline, and futuristic synth melodies, opening with a high-energy rhythm that climaxes in a whirlwind of electronic ecstasy. + +Description: Urban streets at dusk, vibrant with street art and a pulse of lively, youthful energy. +Music: A rap beat in D minor, with heavy bass, crisp snare hits, and a catchy, repetitive melody suitable for dynamic flow, begins with a bold, assertive introduction that leads into a rhythmically complex and compelling outro. + +Description: A peaceful beach with gentle waves, clear skies, and a sense of serene joy and relaxation. +Music: A reggae tune in E major, with a relaxed tempo, characteristic off-beat rhythms, and a laid-back, feel-good vibe, starts with a soothing, cheerful melody that gradually builds into a joyful, uplifting chorus. + +Description: An electrifying rock concert, filled with intense energy, dramatic lighting, and a crowd caught up in the excitement. +Music: A heavy metal track in F sharp minor, driven by aggressive guitar riffs, fast drumming, and powerful, energetic vocals, opens with an intense, thunderous intro that crescendos into a fiery, explosive climax. + +Description: A serene, mist-covered forest at dawn, bathed in a gentle, ethereal light that creates a sense of calm and wonder. +Music: An ambient piece in A flat major, featuring slow, ethereal synth pads, creating a calm, dreamy soundscape, begins with a delicate, otherworldly sound that slowly unfolds into a serene, peaceful conclusion. + +Description: A lively party scene, bursting with color and energy, where people are lost in the moment of celebration and dance. +Music: An electronic dance music (EDM) anthem in B major, with a catchy hook, upbeat tempo, and an infectious rhythm designed for dance floors, starts with a vibrant, exhilarating beat that builds to a euphoric, dance-inducing peak.`, + }, + })) as string[]; + + const llavaPrediction: string = llava.join(''); + + console.log(llavaPrediction); + + const regex = /Description:\s*(.*?)\s*Music:\s*(.*)/s; + const match = llavaPrediction.match(regex); + if (!match) { + throw new Error('No match'); + } + const musicGenVersion = + 'meta/musicgen:b05b1dff1d8c6dc63d14b0cdb42135378dcb87f6373b0d3d341ede46e59e2b38'; + const musicGen = await replicate.run(musicGenVersion, { + input: { + classifier_free_guidance: 10, + model_version: 'stereo-melody-large', + prompt: match[2], + duration: musicLength, + }, + }); + + return new Response( + JSON.stringify({ + llavaResponse: { description: match[1], prompt: match[2] }, + audio: musicGen, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ); + } catch (error) { + console.log(error); + return new Response(JSON.stringify({ error }), { + status: 500, + }); + } +} diff --git a/app/api/stacks/rag-pdf-with-langchain/route.ts b/app/api/stacks/rag-pdf-with-langchain/route.ts new file mode 100644 index 00000000..a300826b --- /dev/null +++ b/app/api/stacks/rag-pdf-with-langchain/route.ts @@ -0,0 +1,108 @@ +// app/api/ragPDFWithLangchain/route.ts +import { ConversationalRetrievalQAChain } from 'langchain/chains'; +import { ChatOpenAI } from 'langchain/chat_models/openai'; +import { WebPDFLoader } from 'langchain/document_loaders/web/pdf'; +import { OpenAIEmbeddings } from 'langchain/embeddings/openai'; +import { BufferMemory, ChatMessageHistory } from 'langchain/memory'; +import { AIMessage, BaseMessage, HumanMessage } from 'langchain/schema'; +import { MemoryVectorStore } from 'langchain/vectorstores/memory'; + +export const runtime = 'nodejs'; + +const chatHistoryDelimiter = `||~||`; + +// Function to create a HumanMessage or AIMessage based on the prefix +const createMessage = (text: string): BaseMessage => { + if (text.startsWith('Q: ')) { + return new HumanMessage({ content: text.slice(3) }); + } else if (text.startsWith('A: ')) { + return new AIMessage({ content: text.slice(3) }); + } else { + throw new Error('Unrecognized message type'); + } +}; + +// Function to transform the specially delimited string into ChatMessageHistory +const transformToChatMessageHistory = ( + chatString: string, +): ChatMessageHistory => { + const messageHistory = new ChatMessageHistory(); + const messages = chatString.split(chatHistoryDelimiter); + + messages.forEach((messagePart) => { + const trimmedMessage = messagePart.trim(); + if (trimmedMessage) { + const message = createMessage(trimmedMessage); + messageHistory.addMessage(message); + } + }); + + return messageHistory; +}; + +export async function POST(req: Request) { + const formData = await req.formData(); + const file = formData.get('pdf'); + const question = formData.get('question') as string; + const chatHistoryValue = formData.get('chatHistory') as string; + + console.log('Received request:', { file, question }); + + if (!file || !(file instanceof Blob)) { + return new Response( + JSON.stringify({ error: 'No PDF file provided or file is not a Blob' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + if (!question || typeof question !== 'string') { + return new Response( + JSON.stringify({ error: 'Question not provided or not a string' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + console.log('Processing PDF:', { file, question }); + + try { + const loader = new WebPDFLoader(file, { splitPages: true }); + const allDocs = await loader.load(); + const memoryVectorStore = await MemoryVectorStore.fromDocuments( + allDocs, + new OpenAIEmbeddings({ modelName: 'text-embedding-ada-002' }), + ); + const llm = new ChatOpenAI({}); + const chatHistory = chatHistoryValue + ? transformToChatMessageHistory(chatHistoryValue) + : new ChatMessageHistory(); + + const memory = new BufferMemory({ + chatHistory, + memoryKey: 'chat_history', // Must be set to "chat_history" + }); + + const chain = ConversationalRetrievalQAChain.fromLLM( + llm, + memoryVectorStore.asRetriever(), + { + memory, + qaChainOptions: { + type: 'map_reduce', + }, + }, + ); + + const answer = (await chain.invoke({ question })).text; + + // Return the result as a JSON response + return new Response(JSON.stringify({ answer }), { + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.error(error); + return new Response( + JSON.stringify({ error: 'Error processing PDF: ' + error.message }), + { status: 500, headers: { 'Content-Type': 'application/json' } }, + ); + } +} diff --git a/app/api/stacks/stable-video-diffusion/route.ts b/app/api/stacks/stable-video-diffusion/route.ts new file mode 100644 index 00000000..71c5c54e --- /dev/null +++ b/app/api/stacks/stable-video-diffusion/route.ts @@ -0,0 +1,55 @@ +import * as fal from '@fal-ai/serverless-client'; + +fal.config({ + credentials: `${process.env.FAL_KEY_ID}:${process.env.FAL_KEY_SECRET}`, +}); + +export const maxDuration = 300; + +export async function POST(request: Request) { + let resp = null; + + const form = await request.formData(); + const imgFile = form.get('img') as Blob; + const maskFile = form.get('mask') as Blob; + const degreeOfMotion = form.get('degreeOfMotion') as string; + + const imgBuffer = Buffer.from(await imgFile.arrayBuffer()); + const maskBuffer = Buffer.from(await maskFile.arrayBuffer()); + + const imgBase64 = imgBuffer.toString('base64'); + const maskBase64 = maskBuffer.toString('base64'); + + // Generate a full URI + const imgUri = `data:${imgFile.type};base64,${imgBase64}`; + const maskUri = `data:${maskFile.type};base64,${maskBase64}`; + + const payload = { + subscriptionId: '110602490-svd', + input: { + image_url: imgUri, + mask_image_url: maskUri, + motion_bucket_id: Number(degreeOfMotion), + cond_aug: 0.02, + steps: 100, + }, + pollInterval: 500, + logs: true, + }; + + try { + const result: any = await fal.subscribe(payload.subscriptionId, payload); + + resp = result; + } catch (error) { + console.log(error); + return new Response(JSON.stringify({ error }), { + status: 500, + }); + } + + return new Response(JSON.stringify(resp), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); +} diff --git a/app/api/stacks/text-to-qr/route.ts b/app/api/stacks/text-to-qr/route.ts new file mode 100644 index 00000000..56d024c7 --- /dev/null +++ b/app/api/stacks/text-to-qr/route.ts @@ -0,0 +1,38 @@ +import Replicate from 'replicate'; + +const replicate = new Replicate({ + auth: process.env.REPLICATE_API_TOKEN!, +}); + +export const maxDuration = 300; + +export async function POST(req: Request) { + const { qrPrompt, url } = await req.json(); + + try { + const controlNetVersion = + 'zylim0702/qr_code_controlnet:628e604e13cf63d8ec58bd4d238474e8986b054bc5e1326e50995fdbc851c557'; + const qrCode = await replicate.run(controlNetVersion, { + input: { + url: url, + prompt: qrPrompt, + qr_conditioning_scale: 1.3, + }, + }); + + return new Response( + JSON.stringify({ + img: qrCode, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ); + } catch (error) { + console.log(error); + return new Response(JSON.stringify({ error }), { + status: 500, + }); + } +} diff --git a/app/api/stacks/use-openai-assistant/route.ts b/app/api/stacks/use-openai-assistant/route.ts new file mode 100644 index 00000000..a850b1d6 --- /dev/null +++ b/app/api/stacks/use-openai-assistant/route.ts @@ -0,0 +1,154 @@ +import { experimental_AssistantResponse } from 'ai'; +import OpenAI from 'openai'; +import { MessageContentText } from 'openai/resources/beta/threads/messages/messages'; + +// Create an OpenAI API client (that's edge friendly!) +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY || '', +}); + +// IMPORTANT! Set the runtime to edge +export const runtime = 'edge'; + +const homeTemperatures = { + bedroom: 20, + 'home office': 21, + 'living room': 21, + kitchen: 22, + bathroom: 23, +}; + +export async function POST(req: Request) { + // Parse the request body + const input: { + threadId: string | null; + message: string; + } = await req.json(); + + // Create a thread if needed + const threadId = input.threadId ?? (await openai.beta.threads.create({})).id; + + // Add a message to the thread + const createdMessage = await openai.beta.threads.messages.create(threadId, { + role: 'user', + content: input.message, + }); + + return experimental_AssistantResponse( + { threadId, messageId: createdMessage.id }, + async ({ threadId, sendMessage, sendDataMessage }) => { + // Run the assistant on the thread + const run = await openai.beta.threads.runs.create(threadId, { + assistant_id: + process.env.ASSISTANT_ID ?? + (() => { + throw new Error('ASSISTANT_ID is not set'); + })(), + }); + + async function waitForRun(run: OpenAI.Beta.Threads.Runs.Run) { + // Poll for status change + while (run.status === 'queued' || run.status === 'in_progress') { + // delay for 500ms: + await new Promise((resolve) => setTimeout(resolve, 500)); + + run = await openai.beta.threads.runs.retrieve(threadId!, run.id); + } + + // Check the run status + if ( + run.status === 'cancelled' || + run.status === 'cancelling' || + run.status === 'failed' || + run.status === 'expired' + ) { + throw new Error(run.status); + } + + if (run.status === 'requires_action') { + if (run.required_action?.type === 'submit_tool_outputs') { + const tool_outputs = + run.required_action.submit_tool_outputs.tool_calls.map( + (toolCall) => { + const parameters = JSON.parse(toolCall.function.arguments); + + switch (toolCall.function.name) { + case 'getRoomTemperature': { + const temperature = + homeTemperatures[ + parameters.room as keyof typeof homeTemperatures + ]; + + return { + tool_call_id: toolCall.id, + output: temperature.toString(), + }; + } + + case 'setRoomTemperature': { + const oldTemperature = + homeTemperatures[ + parameters.room as keyof typeof homeTemperatures + ]; + + homeTemperatures[ + parameters.room as keyof typeof homeTemperatures + ] = parameters.temperature; + + sendDataMessage({ + role: 'data', + data: { + oldTemperature, + newTemperature: parameters.temperature, + description: `Temperature in ${parameters.room} changed from ${oldTemperature} to ${parameters.temperature}`, + }, + }); + + return { + tool_call_id: toolCall.id, + output: `temperature set successfully`, + }; + } + + default: + throw new Error( + `Unknown tool call function: ${toolCall.function.name}`, + ); + } + }, + ); + + run = await openai.beta.threads.runs.submitToolOutputs( + threadId!, + run.id, + { tool_outputs }, + ); + + await waitForRun(run); + } + } + } + + await waitForRun(run); + + // Get new thread messages (after our message) + const responseMessages = ( + await openai.beta.threads.messages.list(threadId, { + after: createdMessage.id, + order: 'asc', + }) + ).data; + + // Send the messages + for (const message of responseMessages) { + sendMessage({ + id: message.id, + role: 'assistant', + content: message.content.filter( + (content) => content.type === 'text', + ) as Array, + }); + } + }, + ); +} diff --git a/app/api/utils/getAWSPresignedUrl/route.ts b/app/api/utils/getAWSPresignedUrl/route.ts new file mode 100644 index 00000000..ee03359c --- /dev/null +++ b/app/api/utils/getAWSPresignedUrl/route.ts @@ -0,0 +1,37 @@ +import AWS from 'aws-sdk'; + +AWS.config.update({ + region: process.env.AWS_REGION, + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, +}); + +export async function POST(req: Request) { + try { + const body = await req.json(); + const s3 = new AWS.S3(); + const { fileName, fileType } = body; + const params = { + Bucket: 'cover-image-and-subtitle-stack', + Key: fileName, + Expires: 60, // URL expiration time in seconds + ContentType: fileType, + }; + + try { + const presignedUrl = await s3.getSignedUrlPromise('putObject', params); + return new Response(JSON.stringify({ url: presignedUrl }), { + status: 200, + }); + } catch (error) { + return new Response( + JSON.stringify({ error: 'Error creating presigned URL' }), + { status: 500 }, + ); + } + } catch (error) { + return new Response(JSON.stringify({ error: 'Bad Request' }), { + status: 400, + }); + } +} diff --git a/app/components/shared/ContactButton.tsx b/app/components/shared/ContactButton.tsx new file mode 100644 index 00000000..80c5c349 --- /dev/null +++ b/app/components/shared/ContactButton.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useState } from 'react'; +import tw from 'tailwind-styled-components'; + +const glowingShadowStyle = { + boxShadow: `0 0 10px rgba(0, 0, 0, 0.6), + 0 0 20px rgba(0, 0, 0, 0.4), + 0 0 30px rgba(0, 0, 0, 0.2)`, +}; + +const glowingShadowHoverStyle = { + boxShadow: `0 0 10px rgba(0, 0, 0, 0.7), + 0 0 20px rgba(0, 0, 0, 0.5), + 0 0 30px rgba(0, 0, 0, 0.3), + 0 0 40px rgba(0, 0, 0, 0.1)`, +}; + +const ContactButton: React.FC = () => { + const [isHovered, setIsHovered] = useState(false); + + return ( + + ); +}; + +export default ContactButton; + +const Button = tw.button` + bg-black + text-white + font-bold + py-2 + px-4 + rounded-full + absolute + top-4 + right-4 + transition duration-300 ease-in-out +`; diff --git a/app/components/shared/clipboard.tsx b/app/components/shared/clipboard.tsx new file mode 100644 index 00000000..c52c2f3c --- /dev/null +++ b/app/components/shared/clipboard.tsx @@ -0,0 +1,41 @@ +'use client'; + +import React, { useState } from 'react'; +import { FaCheckCircle, FaClipboard } from 'react-icons/fa'; // Importing icons + +interface ClipboardComponentProps { + code: string; + title?: any; +} + +const ClipboardComponent: React.FC = ({ + code, + title, +}) => { + const [icon, setIcon] = useState(); // Clipboard icon in black + + const handleClick = async () => { + try { + await navigator.clipboard.writeText(code); + console.log('Text copied to clipboard'); + setIcon(); + setTimeout(() => { + setIcon(); + }, 3000); + } catch (err) { + console.error('Failed to copy: ', err); + } + }; + + return ( + + ); +}; + +export default ClipboardComponent; diff --git a/app/components/stacks/boilerplate-basic.tsx b/app/components/stacks/boilerplate-basic.tsx new file mode 100644 index 00000000..cdcdd7da --- /dev/null +++ b/app/components/stacks/boilerplate-basic.tsx @@ -0,0 +1,69 @@ +import { useState } from 'react'; +import { IoSend } from 'react-icons/io5'; +import ReactMarkdown from 'react-markdown'; + +export const ChatWithOpenAIStreaming = () => { + const [inputValue, setInputValue] = useState(''); + const [output, setOutput] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + console.log('Submitting:', inputValue); + if (inputValue.trim()) { + setOutput(''); + setLoading(true); + + const response = await fetch('/api/stacks/boilerplate-basic', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ input: inputValue }), + }); + const data = await response.json(); + console.log('data', data); + setOutput(data.output); + setLoading(false); + } + }; + return ( +
+
+
+ setInputValue(e.target.value)} + placeholder="Ask anything..." + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + onKeyDown={(e) => { + if (e.key === 'Enter') + handleSubmit(e as unknown as React.FormEvent); + }} + /> + +
+
+
+ {loading ? ( + Generating... + ) : output ? ( + {output} + ) : ( +

Output here...

+ )} +
+
+ ); +}; + +export default ChatWithOpenAIStreaming; diff --git a/app/components/stacks/chat-gemini-streaming-langchain.tsx b/app/components/stacks/chat-gemini-streaming-langchain.tsx new file mode 100644 index 00000000..59b7e946 --- /dev/null +++ b/app/components/stacks/chat-gemini-streaming-langchain.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { IoSend } from 'react-icons/io5'; +import ReactMarkdown from 'react-markdown'; + +export const ChatWithOpenAIStreaming = () => { + const [inputValue, setInputValue] = useState(''); + const [generatedFileContents, setGeneratedFileContents] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + console.log('Submitting:', inputValue); + if (inputValue.trim()) { + setGeneratedFileContents(''); + setLoading(true); + + try { + const response = await fetch( + '/api/stacks/chat-gemini-streaming-langchain', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ messages: inputValue }), + }, + ); + const data = await response.body; + + if (!data) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = data.getReader(); + const decoder = new TextDecoder(); + let done = false; + let fullContent = ''; + + while (!done) { + const { value, done: doneReading } = await reader.read(); + done = doneReading; + const chunkValue = decoder.decode(value, { stream: !done }); + setGeneratedFileContents((prev) => prev + chunkValue); + setLoading(false); + fullContent += chunkValue; + } + } catch (error) { + console.error('Error during fetch:', error); + } finally { + setInputValue(''); // Clear the input field + setLoading(false); + } + } + }; + return ( +
+
+
+ setInputValue(e.target.value)} + placeholder="Ask anything..." + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + onKeyDown={(e) => { + if (e.key === 'Enter') + handleSubmit(e as unknown as React.FormEvent); + }} + /> + +
+
+
+ {loading ? ( + Generating... + ) : generatedFileContents ? ( + {generatedFileContents} + ) : ( +

Output here...

+ )} +
+
+ ); +}; + +export default ChatWithOpenAIStreaming; diff --git a/app/components/stacks/chat-with-gemini-langchain.tsx b/app/components/stacks/chat-with-gemini-langchain.tsx new file mode 100644 index 00000000..e29cfaf7 --- /dev/null +++ b/app/components/stacks/chat-with-gemini-langchain.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react'; +import { IoSend } from 'react-icons/io5'; +import ReactMarkdown from 'react-markdown'; + +export const ChatWithOpenAIStreaming = () => { + const [inputValue, setInputValue] = useState(''); + const [generatedFileContents, setGeneratedFileContents] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + console.log('Submitting:', inputValue); + if (inputValue.trim()) { + setGeneratedFileContents(''); + setLoading(true); + + try { + const response = await fetch('/api/stacks/chat-with-gemini-langchain', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ messages: inputValue }), + }); + const data = await response.json(); + console.log('data', data); + if (!data) { + throw new Error(`HTTP error! status: ${response.status}`); + } + setGeneratedFileContents(data.output); + } catch (error) { + console.error('Error during fetch:', error); + } finally { + setInputValue(''); // Clear the input field + setLoading(false); + } + } + }; + return ( +
+
+
+ setInputValue(e.target.value)} + placeholder="Ask anything..." + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + onKeyDown={(e) => { + if (e.key === 'Enter') + handleSubmit(e as unknown as React.FormEvent); + }} + /> + +
+
+
+ {loading ? ( + Generating... + ) : generatedFileContents ? ( + {generatedFileContents} + ) : ( +

Output here...

+ )} +
+
+ ); +}; + +export default ChatWithOpenAIStreaming; diff --git a/app/components/stacks/chat-with-gemini-streaming.tsx b/app/components/stacks/chat-with-gemini-streaming.tsx new file mode 100644 index 00000000..186481b5 --- /dev/null +++ b/app/components/stacks/chat-with-gemini-streaming.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; +import { IoSend } from 'react-icons/io5'; +import ReactMarkdown from 'react-markdown'; + +export const ChatWithOpenAIStreaming = () => { + const [inputValue, setInputValue] = useState(''); + const [generatedFileContents, setGeneratedFileContents] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + console.log('Submitting:', inputValue); + if (inputValue.trim()) { + setGeneratedFileContents(''); + setLoading(true); + + try { + const response = await fetch('/api/stacks/chat-with-gemini-streaming', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ messages: inputValue }), + }); + const data = await response.body; + + if (!data) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = data.getReader(); + const decoder = new TextDecoder(); + let done = false; + let fullContent = ''; + + while (!done) { + const { value, done: doneReading } = await reader.read(); + done = doneReading; + const chunkValue = decoder.decode(value, { stream: !done }); + setGeneratedFileContents((prev) => prev + chunkValue); + setLoading(false); + fullContent += chunkValue; + } + } catch (error) { + console.error('Error during fetch:', error); + } finally { + setInputValue(''); // Clear the input field + setLoading(false); + } + } + }; + return ( +
+
+
+ setInputValue(e.target.value)} + placeholder="Ask anything..." + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + onKeyDown={(e) => { + if (e.key === 'Enter') + handleSubmit(e as unknown as React.FormEvent); + }} + /> + +
+
+
+ {loading ? ( + Generating... + ) : generatedFileContents ? ( + {generatedFileContents} + ) : ( +

Output here...

+ )} +
+
+ ); +}; + +export default ChatWithOpenAIStreaming; diff --git a/app/components/stacks/chat-with-gemini.tsx b/app/components/stacks/chat-with-gemini.tsx new file mode 100644 index 00000000..f143bdc2 --- /dev/null +++ b/app/components/stacks/chat-with-gemini.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { IoSend } from 'react-icons/io5'; + +export const ChatWithOpenAIStreaming = () => { + const [inputValue, setInputValue] = useState(''); + const [generatedFileContents, setGeneratedFileContents] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + console.log('Submitting:', inputValue); + if (inputValue.trim()) { + setGeneratedFileContents(''); + setLoading(true); + + try { + const response = await fetch('/api/stacks/chat-with-gemini', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ messages: inputValue }), + }); + const data = await response.body; + + if (!data) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = data.getReader(); + const decoder = new TextDecoder(); + let done = false; + let fullContent = ''; + + while (!done) { + const { value, done: doneReading } = await reader.read(); + done = doneReading; + const chunkValue = decoder.decode(value, { stream: !done }); + setGeneratedFileContents((prev) => prev + chunkValue); + setLoading(false); + fullContent += chunkValue; + } + } catch (error) { + console.error('Error during fetch:', error); + } finally { + setInputValue(''); // Clear the input field + setLoading(false); + } + } + }; + return ( +
+
+
+ setInputValue(e.target.value)} + placeholder="Ask anything..." + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + onKeyDown={(e) => { + if (e.key === 'Enter') + handleSubmit(e as unknown as React.FormEvent); + }} + /> + +
+
+
+ {loading ? ( + Generating... + ) : generatedFileContents ? ( + generatedFileContents + ) : ( +

Output here...

+ )} +
+
+ ); +}; + +export default ChatWithOpenAIStreaming; diff --git a/app/components/stacks/chat-with-openai-streaming-helicone.tsx b/app/components/stacks/chat-with-openai-streaming-helicone.tsx new file mode 100644 index 00000000..20563f6e --- /dev/null +++ b/app/components/stacks/chat-with-openai-streaming-helicone.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; +import { IoSend } from 'react-icons/io5'; + +export const ChatWithOpenAIStreamingHelicone = () => { + const [inputValue, setInputValue] = useState(''); + const [generatedFileContents, setGeneratedFileContents] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + console.log('Submitting:', inputValue); + if (inputValue.trim()) { + setGeneratedFileContents(''); + setLoading(true); + + try { + const response = await fetch( + '/api/stacks/chat-openai-streaming-helicone', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ messages: inputValue }), + }, + ); + const data = await response.body; + + if (!data) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = data.getReader(); + const decoder = new TextDecoder(); + let done = false; + let fullContent = ''; + + while (!done) { + const { value, done: doneReading } = await reader.read(); + done = doneReading; + const chunkValue = decoder.decode(value, { stream: !done }); + setGeneratedFileContents((prev) => prev + chunkValue); + setLoading(false); + fullContent += chunkValue; + } + } catch (error) { + console.error('Error during fetch:', error); + } finally { + setInputValue(''); // Clear the input field + setLoading(false); + } + } + }; + return ( +
+
+
+ setInputValue(e.target.value)} + placeholder="Ask anything..." + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + onKeyDown={(e) => { + if (e.key === 'Enter') + handleSubmit(e as unknown as React.FormEvent); + }} + /> + +
+
+
+ {loading ? ( + Generating... + ) : generatedFileContents ? ( + generatedFileContents + ) : ( +

Output here...

+ )} +
+
+ ); +}; + +export default ChatWithOpenAIStreamingHelicone; diff --git a/app/components/stacks/chat-with-openai-streaming-langchain.tsx b/app/components/stacks/chat-with-openai-streaming-langchain.tsx new file mode 100644 index 00000000..625f10c3 --- /dev/null +++ b/app/components/stacks/chat-with-openai-streaming-langchain.tsx @@ -0,0 +1,76 @@ +import React, { useState, useEffect } from 'react'; +import { IoSend } from 'react-icons/io5'; +import { useChat } from 'ai/react'; + +export const ChatWithOpenAIStreaming = () => { + const [inputValue, setInputValue] = useState(''); + const { messages, input, handleInputChange, handleSubmit } = useChat({ + api: '/api/chat-with-openai-streaming-langchain' + }); + + const [loading, setLoading] = useState(false); + + const latestAssistantResponse = messages + .filter((m) => m.role === 'assistant') + .map((m) => m.content) + .pop(); + + const handleFormSubmit = async (event) => { + event.preventDefault(); + setLoading(true); + + try { + await handleSubmit(event); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (latestAssistantResponse !== undefined) { + setInputValue(''); + } + }, [latestAssistantResponse]); + + return ( +
+
+
+ { + setInputValue(e.target.value); + handleInputChange(e); + }} + placeholder="Ask anything..." + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + onKeyDown={(e) => { + if (e.key === 'Enter') handleFormSubmit(e); + }} + /> + +
+
+
+ {loading ? ( + Generating... + ) : latestAssistantResponse ? ( + latestAssistantResponse + ) : ( +

Output here...

+ )} +
+
+ ); +}; + +export default ChatWithOpenAIStreaming; diff --git a/app/components/stacks/chat-with-openai-streaming.tsx b/app/components/stacks/chat-with-openai-streaming.tsx new file mode 100644 index 00000000..8802a713 --- /dev/null +++ b/app/components/stacks/chat-with-openai-streaming.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { IoSend } from 'react-icons/io5'; + +export const ChatWithOpenAIStreaming = () => { + const [inputValue, setInputValue] = useState(''); + const [generatedFileContents, setGeneratedFileContents] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + console.log('Submitting:', inputValue); + if (inputValue.trim()) { + setGeneratedFileContents(''); + setLoading(true); + + try { + const response = await fetch('/api/stacks/chat-with-openai-streaming', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ messages: inputValue }), + }); + const data = await response.body; + + if (!data) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = data.getReader(); + const decoder = new TextDecoder(); + let done = false; + let fullContent = ''; + + while (!done) { + const { value, done: doneReading } = await reader.read(); + done = doneReading; + const chunkValue = decoder.decode(value, { stream: !done }); + setGeneratedFileContents((prev) => prev + chunkValue); + setLoading(false); + fullContent += chunkValue; + } + } catch (error) { + console.error('Error during fetch:', error); + } finally { + setInputValue(''); // Clear the input field + setLoading(false); + } + } + }; + return ( +
+
+
+ setInputValue(e.target.value)} + placeholder="Ask anything..." + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + onKeyDown={(e) => { + if (e.key === 'Enter') + handleSubmit(e as unknown as React.FormEvent); + }} + /> + +
+
+
+ {loading ? ( + Generating... + ) : generatedFileContents ? ( + generatedFileContents + ) : ( +

Output here...

+ )} +
+
+ ); +}; + +export default ChatWithOpenAIStreaming; diff --git a/app/components/stacks/cover-image-and-subtitle.tsx b/app/components/stacks/cover-image-and-subtitle.tsx new file mode 100644 index 00000000..e8f7bde9 --- /dev/null +++ b/app/components/stacks/cover-image-and-subtitle.tsx @@ -0,0 +1,209 @@ +'use client'; + +import { useState } from 'react'; + +export const GenerateImageAndSubtitle = () => { + const [video, setVideo] = useState(null); + const [loading, setLoading] = useState(false); + const [summary, setSummary] = useState(''); + const [subtitle, setSubtitle] = useState(''); + const [imgUrl, setImgUrl] = useState(''); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [videoUploading, setVideoUploading] = useState(false); + + const handleVideoUpload = async (e: React.ChangeEvent) => { + if (subtitle) { + setSubtitle(''); + } + if (imgUrl) { + setImgUrl(''); + } + if (e.target.files && e.target.files[0]) { + setVideoUploading(true); + const file = e.target.files[0]; + try { + // Fetch the presigned URL + const response = await fetch('/api/utils/getAWSPresignedUrl', { + method: 'POST', + body: JSON.stringify({ + fileName: file.name, + fileType: file.type, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const { url } = await response.json(); + + // Upload the file using the presigned URL + const uploadResponse = await fetch(url, { + method: 'PUT', + body: file, + headers: { + 'Content-Type': file.type, + }, + }); + + if (uploadResponse.ok) { + setVideo(file); + setVideoUploading(false); + } else { + console.error('Upload failed.'); + } + } catch (error) { + console.error('Error during upload:', error); + } + } + }; + + const getSubtitle = async () => { + try { + if (loading || !video) { + return; + } + + if (subtitle || imgUrl) { + setSubtitle(''); + setImgUrl(''); + return; + } + + setLoading(true); + if (!video) return; + + const response = await fetch('/api/stacks/cover-image-and-subtitle', { + method: 'POST', + body: JSON.stringify({ fileName: video.name }), + }); + + const data = await response.json(); + setSubtitle(data.subtitle); + setImgUrl(data.imgUrl); + setSummary(data.summarizedText); + setLoading(false); + } catch (error) { + console.error(error); + } + }; + + const downloadSubtitle = () => { + if (!subtitle) { + console.error('Subtitle not Present !!'); + return; + } + + const blob = new Blob([subtitle], { type: 'text/vtt' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = 'subtitle.vtt'; + + document.body.appendChild(link); + link.click(); + + // Clean up the link + document.body.removeChild(link); + }; + + return ( +
+
+ +
+ {videoUploading && <>Video Uploading...} +
+ {video && ( +
+ +
+ ); +}; + +export default GenerateImageAndSubtitle; diff --git a/app/components/stacks/create-ai-canvas.tsx b/app/components/stacks/create-ai-canvas.tsx new file mode 100644 index 00000000..d599b092 --- /dev/null +++ b/app/components/stacks/create-ai-canvas.tsx @@ -0,0 +1,86 @@ +import { useEffect, useRef, useState } from 'react'; +import * as fal from '@fal-ai/serverless-client'; +import { ReactSketchCanvas, ReactSketchCanvasRef } from 'react-sketch-canvas'; + +fal.config({ + credentials: `${process.env.NEXT_PUBLIC_FAL_KEY_ID}:${process.env.NEXT_PUBLIC_FAL_KEY_SECRET}`, +}); + +const CreateAICanvas: React.FC = () => { + const [dataUriImage, setDataUriImage] = useState(''); + const [image, setImage] = useState(''); + const [imagePrompt, setImagePrompt] = useState(''); + const canvasRef = useRef(null); + + const saveDrawing = () => { + canvasRef.current?.exportImage('png').then((originalDataUrl) => { + const image = new Image(); + image.onload = () => { + const scaleCanvas = document.createElement('canvas'); + scaleCanvas.width = 512; + scaleCanvas.height = 512; + const ctx = scaleCanvas.getContext('2d'); + if (ctx) { + ctx.drawImage(image, 0, 0, 512, 512); // Draw and scale the image to 512x512 + const scaledDataUrl = scaleCanvas.toDataURL('image/png'); + setDataUriImage(scaledDataUrl); // This is now the scaled 512x512 image + } + }; + image.src = originalDataUrl; // Load the original exported image + }); + }; + + const connection = fal.realtime.connect('110602490-lcm-sd15-i2i', { + connectionKey: 'fal-realtime-example', + clientOnly: false, + throttleInterval: 256, + onResult: (result) => { + console.log(result); + if (result.images && result.images[0]) { + setImage(result.images[0].url); + } + }, + onError: (error) => { + console.error(error); + }, + }); + + useEffect(() => { + connection.send({ + prompt: imagePrompt, + sync_mode: true, + image_url: dataUriImage, + strength: 0.65, + enable_safety_checks: false, + }); + }, [dataUriImage, imagePrompt]); + + return ( +
+ setImagePrompt(e.target.value)} + value={imagePrompt} + placeholder="Enter prompt..." + className="mb-2 w-full rounded-full border border-gray-300 p-2 pl-4 sm:w-3/4 md:w-1/2" + /> +
+ +
+
+
+ ); +}; + +export default CreateAICanvas; diff --git a/app/components/stacks/create-stack-boilerplate.tsx b/app/components/stacks/create-stack-boilerplate.tsx new file mode 100644 index 00000000..43a67afe --- /dev/null +++ b/app/components/stacks/create-stack-boilerplate.tsx @@ -0,0 +1,125 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import SignIn from '@/app/components/stacks/utils/signIn'; +import { supabaseClient } from '@/app/components/stacks/utils/stack-db'; + +export const BasicForm = () => { + const [formData, setFormData] = useState({ + name: 'My New App', + }); + const [formErrors, setFormErrors] = useState({ id: '' }); + const [Message, setMessage] = useState(''); + const [isUserSignedIn, setIsUserSignedIn] = useState(false); + const [username, setUsername] = useState(''); + const [token, setToken] = useState(''); + const [pullRequestUrl, setPullRequestUrl] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + // Check if user is signed in + async function checkUser() { + try { + const session = await supabaseClient.auth.getSession(); + const token = session?.data?.session?.provider_token; + + if (token) { + setIsUserSignedIn(true); + setUsername( + session?.data?.session?.user.user_metadata.preferred_username, + ); + setToken(token); + } + } catch { + console.log('Error getting user'); + } + } + checkUser(); + }, []); + // const { getToken } = useAuth(); + + const isKebabCase = (str) => /^[a-z0-9]+(-[a-z0-9]+)*$/.test(str); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value, + }); + + if (name === 'id' && value && !isKebabCase(value)) { + setFormErrors({ ...formErrors, id: 'ID must be in kebab-case.' }); + } else { + setFormErrors({ ...formErrors, id: '' }); + } + }; + + const handleSubmit = async (event) => { + setIsLoading(true); + setMessage(''); + event.preventDefault(); + + try { + const response = await fetch('/api/stacks/create-stack-boilerplate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(formData), + }); + + if (response.ok) { + const responseData = await response.json(); + setPullRequestUrl(responseData.prLink); + } else { + const errorData = await response.json(); + + setMessage(errorData.message); + } + } catch (error) { + console.log(error); + setMessage('error on form submission'); + } + }; + + if (!isUserSignedIn) { + return ; + } + + return ( +
+
+ + {/* Other form elements removed */} + + {isLoading &&
Loading...
} + {Message &&
{Message}
} + + {pullRequestUrl && ( + +
+

You can now view your pull Request

+
+ + )} +
+
+ ); +}; + +export default BasicForm; diff --git a/app/components/stacks/creation/Inputs.tsx b/app/components/stacks/creation/Inputs.tsx new file mode 100644 index 00000000..03bbbfa4 --- /dev/null +++ b/app/components/stacks/creation/Inputs.tsx @@ -0,0 +1,25 @@ +import parse from 'html-react-parser'; +import tw from 'tailwind-styled-components'; + +interface InputsProps { + formAction: (payload: FormData) => void; + state: string; +} + +const Inputs: React.FC = ({ formAction, state }) => { + return ( +
+ {parse(state)} + +
+ ); +}; + +export default Inputs; + +const Form = tw.form` + flex + flex-col + space-y-2 + w-1/2 +`; diff --git a/app/components/stacks/creation/content.tsx b/app/components/stacks/creation/content.tsx new file mode 100644 index 00000000..add436d4 --- /dev/null +++ b/app/components/stacks/creation/content.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useState } from 'react'; +import { useFormState } from 'react-dom'; +import tw from 'tailwind-styled-components'; + +import { callStack, parseFormData } from '../utils/actions'; +import InputWithButton from './input-with-button'; +import Inputs from './Inputs'; +import Outputs from './outputs'; + +const Content = ({ stackDB }) => { + const [outputState, functionAction] = useFormState(parseFormData, null); + const [stackIO, createStack] = useFormState(callStack, { + input: '', + output: '', + }); + const [brief, setBrief] = useState(''); + + return ( + <> + + {brief ? `"${brief}"` : ''} + + {stackIO.input && ( + <> + + + + )} + + {stackIO.input && Deploy} + + ); +}; + +const Brief = tw.div` + font-bold + text-lg + mb-2 + h-8 +`; + +const Container = tw.div` + flex + justify-center + items-center + w-2/3 + space-x-6 +`; + +const DeployButton = tw.button` + text-white + bg-black + font-bold + py-3 + px-6 + rounded + mt-4 +`; + +export default Content; diff --git a/app/components/stacks/creation/input-with-button.tsx b/app/components/stacks/creation/input-with-button.tsx new file mode 100644 index 00000000..a40d10d5 --- /dev/null +++ b/app/components/stacks/creation/input-with-button.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useFormStatus } from 'react-dom'; +import { IoSend } from 'react-icons/io5'; +import tw from 'tailwind-styled-components'; + +// import SearchStacks from './search-stacks'; +import { StackDescription } from '../utils/stack-db'; + +export const SubmitButton = () => { + const { pending } = useFormStatus(); + + return ( + + ); +}; + +interface InputWithButtonProps { + setBrief: React.Dispatch>; + formAction: (payload: FormData) => void; + stackDB: Record; +} + +const InputWithButton: React.FC = ({ + setBrief, + formAction, + stackDB, +}) => { + const router = useRouter(); + + const handleSubmit = (e) => { + const inputValue = e.target.elements.stack.value; + setBrief(inputValue); + }; + + const handleLuckyClick = () => { + const randomEntry = + Object.keys(stackDB)[ + Math.floor(Math.random() * Object.keys(stackDB).length) + ]; + router.push(`/stacks/${randomEntry}`); + }; + + return ( + + {/*
+
+ + +
+
*/} + {/* */} + + Take me to a random stack -{'>'} + +
+ ); +}; +const FormWrapper = tw.div` + md:w-3/4 + w-full + flex + flex-col + items-center +`; + +export const Form = tw.form` + flex + items-center + justify-center + lg:w-1/2 + md:w-3/4 + w-full + mb-2 +`; + +const LuckyButton = tw.button` + mt-3 + font-bold + border-b + transition + duration-300 + ease-in-out + transform + hover:scale-110 + text-lg +`; + +export default InputWithButton; diff --git a/app/components/stacks/creation/main-content.tsx b/app/components/stacks/creation/main-content.tsx new file mode 100644 index 00000000..8cd44e25 --- /dev/null +++ b/app/components/stacks/creation/main-content.tsx @@ -0,0 +1,47 @@ +'use server'; + +import tw from 'tailwind-styled-components'; + +import Content from './content'; + +export default async function MainContent({ stackDB }) { + return ( + + +
+ +
+ The open source AI app collection. +
+ + + +
+ ); +} + +const Container = tw.div` + flex + flex-col + justify-end + //pt-60 + pt-24 + pb-5 + //pb-10 +`; + +const TitleContainer = tw.div` + text-center +`; + +const Subtitle = tw.p` + text-lg +`; + +const MainWrapper = tw.div` + w-full + flex + flex-col + justify-center + items-center +`; diff --git a/app/components/stacks/creation/outputs.tsx b/app/components/stacks/creation/outputs.tsx new file mode 100644 index 00000000..c034fee2 --- /dev/null +++ b/app/components/stacks/creation/outputs.tsx @@ -0,0 +1,56 @@ +import tw from 'tailwind-styled-components'; + +interface InputsProps { + state: string; + value: any; +} + +const Inputs: React.FC = ({ state, value }) => { + const renderContent = () => { + // Split the string by your placeholder pattern + const parts = state.split(/({[^}]+})/).filter(Boolean); + + return parts.map((part, index) => { + if (part.startsWith('{') && part.endsWith('}')) { + // Extract and evaluate the expression + const expression = part.slice(1, -1); + + // Check if the expression is trying to access a property of 'value' + if (expression.startsWith('value.')) { + const property = expression.slice(6); + + // Check if 'value' is an object and the property is not null/undefined + if (value && typeof value === 'object' && value[property] != null) { + return value[property].toString(); + } + return ''; // Return an empty string if the property is null/undefined + } + + // Handle the direct 'value' expression + if (expression === 'value') { + return value != null ? value.toString() : ''; + } + + return ''; // If the expression cannot be evaluated, return an empty string + } else { + // If part is not an expression, return it as is + return part; + } + }); + }; + + return ( + +
+ + ); +}; + +export default Inputs; + +const Outputs = tw.form` + w-1/2 + flex + justify-center + items-center +`; diff --git a/app/components/stacks/elevenlabs-tts.tsx b/app/components/stacks/elevenlabs-tts.tsx new file mode 100644 index 00000000..25f5c9b8 --- /dev/null +++ b/app/components/stacks/elevenlabs-tts.tsx @@ -0,0 +1,78 @@ +'use client'; + +import React, { useState } from 'react'; +import { IoSend } from 'react-icons/io5'; + +// Chat component +const Chat = () => { + const [inputValue, setInputValue] = useState(''); + const [generatedFileContents, setGeneratedFileContents] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + console.log('Submitting:', inputValue); + if (inputValue.trim()) { + console.log('Submitting:', inputValue); + playText(inputValue.trim()); + } + }; + + return ( +
+
+
+ setInputValue(e.target.value)} + placeholder="Ask anything..." + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + onKeyDown={(e) => { + if (e.key === 'Enter') + handleSubmit(e as unknown as React.FormEvent); + }} + /> + +
+
+
+ ); +}; + +async function playText(text: string) { + const response = await fetch('/api/stacks/elevenlabs-tts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ text }), + }); + + if (!response.ok) { + throw new Error('Speech generation failed'); + } + + const audioContext = new window.AudioContext(); + const audioData = await response.arrayBuffer(); + const audioBuffer = await audioContext.decodeAudioData(audioData); + + const source = audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(audioContext.destination); + source.start(); + + source.onended = () => { + audioContext.close(); + }; +} + +export default Chat; diff --git a/app/components/stacks/get-image-description-openai.tsx b/app/components/stacks/get-image-description-openai.tsx new file mode 100644 index 00000000..e73b36dd --- /dev/null +++ b/app/components/stacks/get-image-description-openai.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useState } from 'react'; +import { useChat } from 'ai/react'; +import { IoSend } from 'react-icons/io5'; + +export default function Chat() { + const [image, setImage] = useState( + 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3c/Field_sparrow_in_CP_%2841484%29_%28cropped%29.jpg/733px-Field_sparrow_in_CP_%2841484%29_%28cropped%29.jpg', + ); + + const { messages, input, handleInputChange, handleSubmit } = useChat({ + api: '/api/get-image-description-openai', + }); + + const handleImageChange = (e) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onloadend = () => { + if (typeof reader.result === 'string') { + setImage(reader.result); + } else { + // Handle the case where the result is not a string + console.error('File could not be converted to base64 string'); + } + }; + reader.readAsDataURL(file); + } + }; + + return ( +
+
{ + handleSubmit(e, { + data: { + imageUrl: image, + }, + }); + }} + className="flex w-3/4 flex-col items-center space-y-4 md:w-1/2 lg:w-2/5" + > + + {image && Preview} +
+ + +
+
+
+ {messages.length > 0 + ? messages.map((m) => ( +
+ {m.role === 'user' ? 'User: ' : 'AI: '} + {m.content} +
+ )) + : null} +
+
+ ); +} diff --git a/app/components/stacks/image-sharpener.tsx b/app/components/stacks/image-sharpener.tsx new file mode 100644 index 00000000..beaa0495 --- /dev/null +++ b/app/components/stacks/image-sharpener.tsx @@ -0,0 +1,132 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { + ReactCompareSlider, + ReactCompareSliderImage, +} from 'react-compare-slider'; + +export default function ImageToMusic() { + const [img, setImg] = useState(null); + const [sharpenedImage, setSharpenedImage] = useState(); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const fetchImage = async () => { + try { + const response = await fetch('/caesar.jpeg'); + const blob = await response.blob(); + const file = new File([blob], 'caesar.jpeg', { + type: 'image/webp', + }); + setImg(file); + } catch (error) { + console.error('Error fetching default image:', error); + } + }; + + fetchImage(); + }, []); + + const handleImageUpload = (e: React.ChangeEvent) => { + if (sharpenedImage) { + setSharpenedImage(''); + } + if (e.target.files && e.target.files[0]) { + setImg(e.target.files[0]); + } + }; + + const sharpenImage = async () => { + if (loading) return; + if (sharpenedImage) { + setSharpenedImage(''); + return; + } + + setLoading(true); + const formData = new FormData(); + if (img) { + formData.append('img', img); + } + const response = await fetch('/api/stacks/image-sharpener', { + method: 'POST', + body: formData, + }); + const data = await response.json(); + console.log(data); + setSharpenedImage(data); + setLoading(false); + }; + + return ( +
+
+ +
+
+ {sharpenedImage && img ? ( +
+

Before

+ + } + itemTwo={ + + } + /> +

After

+
+ ) : ( + img && ( + Preview + ) + )} +
+ +
+ ); +} diff --git a/app/components/stacks/image-to-music.tsx b/app/components/stacks/image-to-music.tsx new file mode 100644 index 00000000..394e83d5 --- /dev/null +++ b/app/components/stacks/image-to-music.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +export default function ImageToMusic() { + const [img, setImg] = useState(null); + const [llavaResponse, setLlavaResponse] = useState<{ + description: string; + prompt: string; + }>({ description: '', prompt: '' }); // Provide initial values here + const [audio, setAudio] = useState(''); + const [musicLength, setMusicLength] = useState('10'); + const [loading, setLoading] = useState(false); + const audioRef = useRef(null); + + useEffect(() => { + const fetchImage = async () => { + try { + const response = await fetch('/apocalyptic_car.png'); + const blob = await response.blob(); + const file = new File([blob], 'default_image.webp', { + type: 'image/webp', + }); + setImg(file); + } catch (error) { + console.error('Error fetching default image:', error); + } + }; + + fetchImage(); + }, []); + + const handleImageUpload = (e: React.ChangeEvent) => { + if (audio) { + setAudio(''); + } + if (e.target.files && e.target.files[0]) { + setImg(e.target.files[0]); + } + }; + + const createMusic = async () => { + if (loading) return; + if (audio) { + setAudio(''); + return; + } + + setLoading(true); + const formData = new FormData(); + if (img) { + formData.append('img', img); + } + formData.append('length', musicLength.toString()); + const response = await fetch('/api/stacks/image-to-music', { + method: 'POST', + body: formData, + }); + const data = await response.json(); + setAudio(data.audio); + setLlavaResponse(data.llavaResponse); + setLoading(false); + }; + + useEffect(() => { + if (audio) { + (audioRef.current as HTMLAudioElement | null)?.load(); + (audioRef.current as HTMLAudioElement | null)?.play(); + } + }, [audio]); + + const handleMusicLength = (e: React.ChangeEvent) => { + let value = parseInt(e.target.value, 10); + if (!isNaN(value)) { + value = Math.max(3, Math.min(30, value)); + } + setMusicLength(String(value)); + }; + + return ( +
+
+ + <> + Length (sec): + setMusicLength(e.target.value)} + onBlur={handleMusicLength} + type="number" + className="ml-1 h-8 rounded border pl-1" + /> + +
+
+ {img && ( + Preview + )} + {audio && ( +
+

+ Image description: + {llavaResponse.description} +

+

+ Inspired music: + {llavaResponse.prompt} +

+
+ )} +
+ +
+ ); +} diff --git a/app/components/stacks/instant-video-to-image.tsx b/app/components/stacks/instant-video-to-image.tsx new file mode 100644 index 00000000..788933d3 --- /dev/null +++ b/app/components/stacks/instant-video-to-image.tsx @@ -0,0 +1,155 @@ +import { useEffect, useRef, useState } from 'react'; +import * as fal from '@fal-ai/serverless-client'; + +fal.config({ + credentials: `${process.env.NEXT_PUBLIC_FAL_KEY_ID}:${process.env.NEXT_PUBLIC_FAL_KEY_SECRET}`, +}); + +const InstantVideoToImage: React.FC = () => { + const [image, setImage] = useState(''); + const [videoImage, setVideoImage] = useState(''); + const [imagePrompt, setImagePrompt] = useState('ryan reynolds'); + const [videoHeight, setVideoHeight] = useState(0); + const [strength, setStrength] = useState(4); + const videoRef = useRef(null); + const intervalRef = useRef(); + + const captureImage = () => { + if (videoRef.current) { + const scaleCanvas = document.createElement('canvas'); + scaleCanvas.width = 512; + scaleCanvas.height = 512; + const ctx = scaleCanvas.getContext('2d'); + if (ctx) { + ctx.drawImage(videoRef.current, 0, 0, 512, 512); + const dataUriImage = scaleCanvas.toDataURL('image/png'); + setVideoImage(dataUriImage); + } + } + }; + + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + setVideoHeight(entry.target.clientHeight); + } + }); + + if (videoRef.current) { + resizeObserver.observe(videoRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + useEffect(() => { + navigator.mediaDevices + .getUserMedia({ video: true }) + .then((stream) => { + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + }) + .catch((error) => { + console.error('Error accessing the camera: ', error); + }); + + intervalRef.current = setInterval(captureImage, 75); // Capture image every 0.1 seconds + + const timeoutRef = setTimeout(() => { + clearInterval(intervalRef.current); + if (videoRef.current?.srcObject instanceof MediaStream) { + videoRef.current.pause(); // Pause the video + + // Apply a blur effect to the video + videoRef.current.style.filter = 'blur(6px)'; + } + }, 15000); + + return () => { + clearInterval(intervalRef.current); + clearTimeout(timeoutRef); // Clear the timeout on unmount + // Stop video stream + if (videoRef.current?.srcObject instanceof MediaStream) { + videoRef.current.srcObject.getTracks().forEach((track) => track.stop()); + videoRef.current.style.filter = 'none'; + } + }; + }, []); + + const connection = fal.realtime.connect('110602490-lcm-sd15-i2i', { + connectionKey: 'fal-realtime-example', + clientOnly: false, + throttleInterval: 75, + onResult: (result) => { + if (result.images && result.images[0]) { + setImage(result.images[0].url); + } + }, + onError: (error) => { + console.error(error); + }, + }); + + const range = [1, 10]; + + useEffect(() => { + const scaledValue = + ((strength - 1) * (0.6 - 0.1)) / (range[1] - range[0]) + 0.1; + connection.send({ + prompt: imagePrompt, + sync_mode: true, + image_url: videoImage, + strength: scaledValue, + enable_safety_checks: false, + }); + }, [videoImage, imagePrompt, strength]); + + const handleStrengthChange = (e) => { + let value = parseInt(e.target.value, 10); + if (!isNaN(value)) { + value = Math.max(range[0], Math.min(range[1], value)); + } + setStrength(value); + }; + + return ( +
+
+ setImagePrompt(e.target.value)} + value={imagePrompt} + placeholder="Enter prompt..." + className="mb-2 w-full rounded-full border border-gray-300 p-2 pl-4 sm:w-3/4 md:w-1/2" + /> + + +
+

+ For cost reasons this will only run for 10 seconds +

+
+
+
+ ); +}; + +export default InstantVideoToImage; diff --git a/app/components/stacks/rag-pdf-with-langchain.tsx b/app/components/stacks/rag-pdf-with-langchain.tsx new file mode 100644 index 00000000..c67ed2d3 --- /dev/null +++ b/app/components/stacks/rag-pdf-with-langchain.tsx @@ -0,0 +1,257 @@ +// File path: ui/app/components/RAGPDFWithLangchain.tsx + +import { useEffect, useRef, useState } from 'react'; +import { IoSend } from 'react-icons/io5'; + +const chatHistoryDelimiter = `||~||`; + +interface ChatHistoryProps { + chatHistory: string[]; + handleCancel: () => void; +} + +const placeholderAnswering = 'A: Answering…'; + +const ChatHistory: React.FC = ({ + chatHistory, + handleCancel, +}) => ( +
0 && 'border-t' + } border-gray-200`} + > + {chatHistory.length > 0 && ( +

Chat History

+ )} +
    + {chatHistory.map((entry, index) => ( +
  • + + {entry} + {entry === placeholderAnswering && ( + + )} + +
  • + ))} +
+
+); + +const RAGPDFWithLangchain = () => { + const [pdfFile, setPdfFile] = useState(null); + const [loading, setLoading] = useState(false); + const [question, setQuestion] = useState(''); + const [error, setError] = useState(''); + const [pdfUploaded, setPdfUploaded] = useState(false); + const [chatHistory, setChatHistory] = useState([]); + const abortControllerRef = useRef(null); + const questionInputRef = useRef(null); + const pdfInputRef = useRef(null); + const prevQuestionRef = useRef(question); + + useEffect(() => { + return () => { + // Clean up the fetch request if the component is unmounted during a request + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, []); + + useEffect(() => { + // Compare the previous question value with the current one + if (prevQuestionRef.current && !question) { + // The input has just been cleared + handleClearQuestion(); + } + // Update the ref to the current question for the next render + prevQuestionRef.current = question; + }, [question]); // Only re-run if question changes + + const handlePDFUpload = async ( + event: React.ChangeEvent, + ) => { + const file = event.target.files?.[0]; + if (file) { + setLoading(true); + setError(''); + setPdfFile(file); + setPdfUploaded(true); + setLoading(false); + // Reset chat history and related states if chat history is not empty + if (chatHistory.length > 0) { + setChatHistory([]); + setQuestion(''); + } + } + }; + + const handleCancel = () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); // Abort the fetch request + } + setLoading(false); + setPdfFile(null); + setPdfUploaded(false); + setQuestion(''); + setError(''); + }; + + const handleTotalReset = () => { + // Reset all state to initial values + handleCancel(); + setChatHistory([]); + // Clear the file input if needed + if (questionInputRef?.current) { + questionInputRef.current.value = ''; + } + // Focus the file input after reset + questionInputRef.current?.focus(); + if (pdfInputRef.current) { + pdfInputRef.current.value = ''; + } + }; + + const handleClearQuestion = () => { + // Clear the question input field + setQuestion(''); + // Focus the question input after clearing + questionInputRef.current?.focus(); + }; + + const getChatHistoryString = () => { + return chatHistory.join(chatHistoryDelimiter); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (!pdfFile || !question) { + setError('Please upload a PDF and enter a question.'); + return; + } + setError(''); + setLoading(true); + + // Create a new AbortController and store its reference + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + const formData = new FormData(); + formData.append('pdf', pdfFile); + formData.append('question', question); + formData.append('chatHistory', getChatHistoryString()); + + // Temporarily add the question and "Answering..." message to the chat history + setChatHistory((prev) => [...prev, `Q: ${question}`, placeholderAnswering]); + + try { + const response = await fetch('/api/stacks/rag-pdf-with-langchain', { + method: 'POST', + body: formData, + signal: signal, + }); + if (signal.aborted) return; + + const data = await response.json(); + if (data.error) { + setError(data.error); + // Remove the temporary question and "Answering..." message if there's an error + setChatHistory((prev) => prev.slice(0, -2)); + } else { + // Replace the "Answering..." message with the actual answer + setChatHistory((prev) => [...prev.slice(0, -1), `A: ${data.answer}`]); + } + } catch (error) { + setError('An error occurred while fetching the data.'); + // Remove the temporary question and "Answering..." message if there's an error + setChatHistory((prev) => prev.slice(0, -2)); + } finally { + setLoading(false); + setQuestion(''); // Clear the question input + abortControllerRef.current = null; + } + }; + + return ( +
+
+
+ + {pdfFile && ( + <> + {pdfFile.name} + + + )} +
+ +
+ setQuestion(e.target.value)} + placeholder="What would you like to ask?" + className="focus:shadow-outline w-full rounded-full border border-gray-400 py-2 pl-4 pr-10 focus:outline-none" + /> + +
+ + {error &&

{error}

} +
+ ); +}; + +export default RAGPDFWithLangchain; diff --git a/app/components/stacks/stable-video-diffusion.tsx b/app/components/stacks/stable-video-diffusion.tsx new file mode 100644 index 00000000..e5c5f197 --- /dev/null +++ b/app/components/stacks/stable-video-diffusion.tsx @@ -0,0 +1,240 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +const StableVideoDiffusion = () => { + const draftCanvasRef = useRef(null); + const [isDrawing, setIsDrawing] = useState(false); + const [lastPosition, setLastPosition] = useState<{ + x: number; + y: number; + } | null>(null); + const [imgSrc, setImgSrc] = useState('/boat_example.webp'); + const [loading, setLoading] = useState(false); + const [degreeOfMotion, setDegreeOfMotion] = useState(40); + const [animatedPicture, setAnimatedPicture] = useState(''); + const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); + const imageRef = useRef(new Image()); + + useEffect(() => { + const loadImage = () => { + const image = imageRef.current; + image.onload = () => resizeCanvas(image); // Resize canvas when image is loaded + image.src = imgSrc; // Set the image source + }; + + loadImage(); + + // Add a throttled resize event listener + window.addEventListener('resize', loadImage); + + // Cleanup the event listener on component unmount + return () => window.removeEventListener('resize', loadImage); + }, [imgSrc]); + + const draw = useCallback( + (clientX: number, clientY: number) => { + const draftCanvas = draftCanvasRef.current; + if (!draftCanvas) return; + + const rect = draftCanvas.getBoundingClientRect(); + const ctx = draftCanvas.getContext('2d'); + if (!ctx) return; + + const x = clientX - rect.left; + const y = clientY - rect.top; + + if (lastPosition) { + ctx.strokeStyle = 'rgb(0,0,0)'; // Set stroke color + ctx.lineWidth = 40; // Set stroke width + ctx.lineCap = 'round'; // Smooth line endings + + ctx.beginPath(); + ctx.moveTo(lastPosition.x, lastPosition.y); + ctx.lineTo(x, y); + ctx.stroke(); + } + + setLastPosition({ x, y }); + }, + [lastPosition], + ); + + const handleMouseMove = useCallback( + (e: React.MouseEvent) => { + if (!isDrawing) return; + draw(e.clientX, e.clientY); + }, + [draw, isDrawing], + ); + + const handleMouseDown = useCallback((e: React.MouseEvent) => { + setIsDrawing(true); + const draftCanvas = draftCanvasRef.current; + const rect = draftCanvas?.getBoundingClientRect(); + if (rect) { + setLastPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }); + } + }, []); + + const handleMouseUp = useCallback(() => { + setIsDrawing(false); + setLastPosition(null); + }, []); + + const handleImageUpload = (e: React.ChangeEvent) => { + if (animatedPicture) { + setAnimatedPicture(''); + } + if (e.target.files && e.target.files[0]) { + const newImgSrc = URL.createObjectURL(e.target.files[0]); + setImgSrc(newImgSrc); // Update the imgSrc state + + const image = imageRef.current; + image.onload = () => resizeCanvas(image); // Use the ref's current image for resizing + image.src = newImgSrc; // Update the image source in the ref + } + }; + + const handleDegreeOfMotionChange = (e) => { + let value = parseInt(e.target.value, 10); + // Ensure the value is between 1 and 255 + if (!isNaN(value)) { + value = Math.max(1, Math.min(255, value)); + } + setDegreeOfMotion(value); + }; + + const resizeCanvas = (img) => { + const parentDiv = document.querySelector('.canvas-img'); + if (!parentDiv || !draftCanvasRef.current) return; + + const parentWidth = parentDiv.clientWidth; + const parentHeight = parentDiv.clientHeight; + const imgRatio = img.naturalWidth / img.naturalHeight; + let newWidth, newHeight; + + if (parentWidth / parentHeight > imgRatio) { + // Parent is wider than image aspect ratio + newHeight = parentHeight; + newWidth = newHeight * imgRatio; + } else { + // Parent is narrower than image aspect ratio + newWidth = parentWidth; + newHeight = newWidth / imgRatio; + } + + draftCanvasRef.current.width = newWidth; + draftCanvasRef.current.height = newHeight; + + setCanvasSize({ width: newWidth, height: newHeight }); + }; + + const handleSubmit = async () => { + // Reset and setup code + if (animatedPicture) { + setAnimatedPicture(''); + resizeCanvas(imageRef.current); + return; + } + setLoading(true); + + // Convert the drawing on the main canvas to a Blob + const mainCanvasBlob = await new Promise((resolve) => { + if (draftCanvasRef.current) { + draftCanvasRef.current.toBlob(resolve, 'image/png'); + } else { + resolve(null); + } + }); + + try { + // Fetch the background image from the imgSrc URL and convert it to a Blob + const response = await fetch(imgSrc); + const backgroundImageBlob = await response.blob(); + + // Prepare FormData + const formData = new FormData(); + formData.append('img', backgroundImageBlob, 'background.png'); + formData.append('mask', mainCanvasBlob as Blob, 'mask.png'); + const ensureMotion = isNaN(degreeOfMotion) ? 40 : degreeOfMotion; + formData.append('degreeOfMotion', ensureMotion.toString()); + + // Call the API with FormData + const apiResponse = await fetch('/api/stacks/stable-video-diffusion', { + method: 'POST', + body: formData, // FormData is used directly here + }); + + const resultData = await apiResponse.json(); + + if (!apiResponse.ok) throw new Error(resultData.error); + + // Update state with the returned GIF URL + setAnimatedPicture(resultData.image.url); + } catch (error) { + console.error('Error during API call:', error); + } + + setLoading(false); + }; + + return ( +
+
+ + + +
+
+ Animated image + +
+ + +
+ ); +}; + +export default StableVideoDiffusion; diff --git a/app/components/stacks/text-to-qr.tsx b/app/components/stacks/text-to-qr.tsx new file mode 100644 index 00000000..32e40534 --- /dev/null +++ b/app/components/stacks/text-to-qr.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useState } from 'react'; + +export default function ImageToMusic() { + const [qrPrompt, setQrPrompt] = useState('a city view with clouds'); + const [url, setUrl] = useState(''); + const [loading, setLoading] = useState(false); + const [img, setImg] = useState(''); + + const createMusic = async () => { + if (loading) return; + if (img) { + setImg(''); + return; + } + + setLoading(true); + + const response = await fetch('/api/stacks/text-to-qr', { + method: 'POST', + body: JSON.stringify({ qrPrompt, url }), + }); + const data = await response.json(); + setImg(data.img); + setLoading(false); + }; + + return ( +
+
+ {img ? ( + + ) : ( + <> + setUrl(e.target.value)} + value={url} + placeholder="Enter message or website link..." + className="mb-2 w-full rounded-lg border border-gray-300 p-2 pl-4 sm:w-3/4 md:w-1/2" + /> +