diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..ca85fb28 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,19 @@ +{ + "permissions": { + "allow": [ + "Bash(git lfs push:*)", + "Bash(git push:*)", + "Bash(find:*)", + "Bash(git lfs pointer:*)", + "Bash(git add:*)", + "Bash(git rm:*)", + "Bash(git commit:*)", + "Bash(ls:*)", + "Bash(git checkout:*)", + "Bash(git fetch:*)", + "Bash(git filter-repo:*)", + "Bash(git remote add:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 016e34ba..6edc5a20 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.17.0 +v22.17.0 diff --git a/Dockerfile b/Dockerfile index 43b860d5..506bbd4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,12 @@ -FROM oven/bun:canary-alpine +# Due to a bug in Bun or Next 🤷‍♂️ we are unable to build the app using only Bun +# to fix this, we use a Docker image that contains both Bun and Node + +# a lot of people have the same issue but the Bun team closed the ticket 🤷‍♂️ +# https://github.com/oven-sh/bun/issues/8725 +FROM imbios/bun-node:latest-current-alpine-git + +# if the Bun team decides to fix the bug one day 🤷‍♂️ then we will be able to use this instead: +# FROM oven/bun:alpine ARG DEBIAN_FRONTEND=noninteractive @@ -18,13 +26,13 @@ RUN apk add font-arabic-misc font-inconsolata font-dejavu font-awesome RUN apk add ttf-opensans # For Puppeteer -# DISABLEd - we don't actually need Puppeteer on Clapper right now +# DISABLED: we don't actually need Puppeteer on Clapper right now # RUN apk add build-base gcompat udev chromium RUN apk add --no-cache ffmpeg -# Set up a new user named "user" with user ID 1000 -RUN adduser --disabled-password --uid 1001 user +# Set up a new user named "user" with user ID 1002 +RUN adduser --disabled-password --uid 1002 user # Switch to the "user" user USER user @@ -50,9 +58,19 @@ COPY --chown=user . /app RUN bun i -# this step can be done by the developer, -# and the file uploaded using HF CLI tools -# RUN bun run build +# we can use this all-in-one command, but it might timeout +#RUN bun run build + +# this allows for a finer grained control +RUN bun run build:clap +RUN bun run build:timeline +RUN bun run build:client +RUN bun run build:io +RUN bun run build:colors +RUN bun run build:engine +RUN bun run build:broadway +RUN bun run build:clapper-services +RUN bun run build:app EXPOSE 3000 diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..ff7b282c --- /dev/null +++ b/bun.lock @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4be85e228b5faa279e8663802af2b843a23ccde812bbbdbe47891b2483c5c888 +size 542238 diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 9b891e76..00000000 --- a/bun.lockb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f047fdb2fc29c8bc2eddf944107e42fc113ac477f4202552dfaf7564cb3c2deb -size 694900 diff --git a/package.json b/package.json index 163a44db..4e9ec107 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,26 @@ { "name": "@aitube/clapper-monorepo", - "version": "0.2.4", + "version": "0.2.5", "description": "A monorepo for the Clapper project. Individual packages are in the packages directory.", "engines": { "bun": ">=1.0.0" }, - "packageManager": "bun@1.0.25", + "packageManager": "bun@1.2.17", "private": true, "scripts": { "dev": "bun run --cwd packages/app dev", "start": "bun run --cwd packages/app start", "start:prod": "bun run --cwd packages/app start:prod", + "clean:all": "bun run clean:clap && bun run clean:timeline && bun run clean:client && bun run clean:io && bun run clean:colors && bun run clean:engine && bun run clean:broadway && bun run clean:clapper-services && bun run clean:app", + "clean:clap": "bun run --cwd packages/clap clean", + "clean:timeline": "bun run --cwd packages/timeline clean", + "clean:client": "bun run --cwd packages/client clean", + "clean:io": "bun run --cwd packages/io clean", + "clean:colors": "bun run --cwd packages/colors clean", + "clean:engine": "bun run --cwd packages/engine clean", + "clean:broadway": "bun run --cwd packages/broadway clean", + "clean:clapper-services": "bun run --cwd packages/clapper-services clean", + "clean:app": "bun run --cwd packages/app clean", "build": "bun run build:all", "build:all": "bun run build:clap && bun run build:timeline && bun run build:client && bun run build:io && bun run build:colors && bun run build:engine && bun run build:broadway && bun run build:clapper-services && bun run build:app", "build:clap": "bun run --cwd packages/clap build", @@ -28,8 +38,18 @@ }, "trustedDependencies": [ "@aitube/clapper", + "@tailwindcss/oxide", + "bufferutil", + "electron", + "electron-winstaller", + "esbuild", + "fs-xattr", + "macos-alias", + "msw", "onnxruntime-node", - "protobufjs" + "protobufjs", + "sharp", + "unrs-resolver" ], "workspaces": [ "packages/clap", diff --git a/packages/app/next.config.js b/packages/app/next.config.js index 427c1281..33ac5839 100644 --- a/packages/app/next.config.js +++ b/packages/app/next.config.js @@ -1,12 +1,31 @@ /** @type {import('next').NextConfig} */ +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file +const __dirname = dirname(__filename); // get the name of the directory + +process.on('unhandledRejection', error => { + console.log('unhandledRejection', error); +}); + const nextConfig = { output: 'standalone', + // this includes files from the monorepo base two directories up + // see: https://nextjs.org/docs/pages/api-reference/config/next-config-js/output#caveats + outputFileTracingRoot: join(__dirname, '../../'), + experimental: { serverActions: { // a clap file can be quite large - but that's OK bodySizeLimit: '32mb' - } + }, + + + // https://nextjs.org/docs/app/guides/memory-usage#try-experimentalwebpackmemoryoptimizations + // I suspect this might sometimes crash the build of the app + webpackMemoryOptimizations: false // let's set it to false }, images: { // temporary fix for: @@ -62,4 +81,4 @@ const nextConfig = { } } -module.exports = nextConfig +export default nextConfig; \ No newline at end of file diff --git a/packages/app/package.json b/packages/app/package.json index 8da9b8af..cd1d478a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@aitube/clapper", - "version": "0.2.4", + "version": "0.2.5", "private": true, "description": "🎬 Clapper", "license": "GPL-3.0-only", @@ -14,9 +14,13 @@ }, "scripts": { "dev": "bun run checks && next dev", - "build": "bun run prepare && bun run checks && rm -Rf out && next build && bun run build:copyassets", + "clean": "rm -Rf .next && rm -Rf node_modules", + "build": "bun run prepare && bun run checks && rm -Rf out && next build -d && bun run build:copyassets", + "build:faster": "bun run prepare && rm -Rf out && next build && bun run build:copyassets", "build:copyassets": "cp -R public .next/standalone/packages/app/public && cp -R .next/static .next/standalone/packages/app/.next/static", - "prepare": "cp -R ../../node_modules/mediainfo.js/dist/MediaInfoModule.wasm public/wasm/", + "prepare": "bun run prepare:prod", + "prepare:dev": "cp ../../node_modules/mediainfo.js/dist/MediaInfoModule.wasm src/services/io/", + "prepare:prod": "cp ../../node_modules/mediainfo.js/dist/MediaInfoModule.wasm public/wasm/", "start": "next start", "start:prod": "bun .next/standalone/packages/app/server.js", "checks": "bun run format:fix && bun run lint", @@ -41,21 +45,21 @@ "@aitube/clapper-services": "workspace:*", "@aitube/engine": "workspace:*", "@aitube/timeline": "workspace:*", - "@fal-ai/serverless-client": "0.14.2", - "@ffmpeg/ffmpeg": "0.12.10", - "@ffmpeg/util": "0.12.1", - "@gradio/client": "1.8.0", - "@huggingface/hub": "0.15.1", - "@huggingface/inference": "2.8.1", - "@huggingface/transformers": "3.0.2", - "@langchain/anthropic": "0.3.8", - "@langchain/cohere": "0.3.1", - "@langchain/core": "0.3.18", - "@langchain/google-vertexai": "0.1.2", - "@langchain/groq": "0.1.2", - "@langchain/mistralai": "0.2.0", - "@langchain/openai": "0.3.14", - "@monaco-editor/react": "4.6.0", + "@fal-ai/serverless-client": "0.15.0", + "@ffmpeg/ffmpeg": "0.12.15", + "@ffmpeg/util": "0.12.2", + "@gradio/client": "1.15.4", + "@huggingface/hub": "2.2.0", + "@huggingface/inference": "4.3.0", + "@huggingface/transformers": "3.6.1", + "@langchain/anthropic": "0.3.23", + "@langchain/cohere": "0.3.4", + "@langchain/core": "0.3.61", + "@langchain/google-vertexai": "0.2.14", + "@langchain/groq": "0.2.3", + "@langchain/mistralai": "0.2.1", + "@langchain/openai": "0.5.16", + "@monaco-editor/react": "4.7.0", "@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-avatar": "1.0.4", "@radix-ui/react-checkbox": "1.0.4", @@ -76,123 +80,123 @@ "@radix-ui/react-tabs": "1.0.4", "@radix-ui/react-toast": "1.1.5", "@radix-ui/react-tooltip": "1.0.7", - "@react-spring/three": "9.7.5", - "@react-spring/types": "9.7.5", - "@react-three/drei": "9.117.3", - "@react-three/fiber": "8.17.10", + "@react-spring/three": "10.0.1", + "@react-spring/types": "10.0.1", + "@react-three/drei": "10.4.2", + "@react-three/fiber": "9.1.4", "@react-three/uikit": "0.8.4", "@react-three/uikit-lucide": "0.8.4", - "@saintno/comfyui-sdk": "0.2.29", + "@saintno/comfyui-sdk": "0.2.47", "@tailwindcss/container-queries": "0.1.1", - "@types/dom-speech-recognition": "0.0.4", + "@types/dom-speech-recognition": "0.0.6", "@types/pngjs": "6.0.5", - "@xyflow/react": "12.3.5", - "autoprefixer": "10.4.20", + "@xyflow/react": "12.8.1", "base64-arraybuffer": "1.0.2", - "bellhop-iframe": "3.5.0", + "bellhop-iframe": "3.6.0", "civitai": "0.1.15", - "class-variance-authority": "0.7.0", + "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "cmdk": "0.2.1", - "comfydeploy": "0.0.21", - "date-fns": "3.6.0", - "dotenv": "16.4.5", + "cmdk": "1.1.1", + "comfydeploy": "2.2.1", + "date-fns": "4.1.0", + "dotenv": "17.0.1", "fflate": "0.8.2", "fluent-ffmpeg": "2.1.3", - "framer-motion": "11.3.31", - "fs-extra": "11.2.0", + "framer-motion": "12.23.0", + "fs-extra": "11.3.0", "is-hotkey": "0.2.0", - "lucide-react": "0.438.0", - "lumaai": "1.0.2", + "lucide-react": "0.525.0", + "lumaai": "1.15.0", "mediainfo.js": "0.3.2", "mlt-xml": "2.0.2", - "monaco-editor": "0.51.0", - "next": "14.2.10", - "next-themes": "0.3.0", + "monaco-editor": "0.52.2", + "next": "15.3.4", + "next-themes": "0.4.6", "pngjs": "7.0.0", - "qs": "6.13.0", - "query-string": "9.1.0", - "react": "18.3.1", + "qs": "6.14.0", + "query-string": "9.2.1", + "react": "*", "react-device-frameset": "1.3.4", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", - "react-dom": "18.3.1", - "react-drag-drop-files": "2.3.10", - "react-error-boundary": "4.0.13", + "react-dom": "*", + "react-drag-drop-files": "3.0.1", + "react-error-boundary": "6.0.0", "react-hook-consent": "3.5.3", - "react-hotkeys-hook": "4.5.0", - "react-icons": "5.2.1", - "react-markdown": "9.0.1", - "react-reflex": "4.2.6", - "react-speakup": "1.0.0", - "remark-gfm": "4.0.0", - "replicate": "0.32.0", - "sharp": "0.33.4", - "sonner": "1.5.0", - "tailwind-merge": "2.5.2", + "react-hotkeys-hook": "5.1.0", + "react-icons": "5.5.0", + "react-markdown": "10.1.0", + "react-reflex": "4.2.7", + "react-speakup": "1.1.2", + "remark-gfm": "4.0.1", + "replicate": "1.0.1", + "sharp": "0.34.2", + "sonner": "2.0.5", + "tailwind-merge": "3.3.1", "tailwindcss-animate": "1.0.7", "three": "0.164.1", "ts-node": "10.9.2", - "use-file-picker": "2.1.2", + "use-file-picker": "2.1.4", "usehooks-ts": "2.14.0", "uuid": "9.0.1", - "web-audio-beat-detector": "8.2.12", - "yaml": "2.6.1", - "zustand": "4.5.2", - "zx": "8.1.4" + "web-audio-beat-detector": "8.2.29", + "yaml": "2.8.0", + "zustand": "5.0.6", + "zx": "8.6.1" }, "devDependencies": { - "@electron-forge/cli": "7.4.0", - "@electron-forge/maker-deb": "7.4.0", - "@electron-forge/maker-dmg": "7.4.0", - "@electron-forge/maker-rpm": "7.4.0", - "@electron-forge/maker-squirrel": "7.4.0", - "@electron-forge/maker-zip": "7.4.0", - "@electron-forge/plugin-auto-unpack-natives": "7.4.0", - "@electron-forge/publisher-github": "7.4.0", - "@playwright/test": "1.45.1", - "@testing-library/react": "16.0.0", + "@electron-forge/cli": "7.8.1", + "@electron-forge/maker-deb": "7.8.1", + "@electron-forge/maker-dmg": "7.8.1", + "@electron-forge/maker-rpm": "7.8.1", + "@electron-forge/maker-squirrel": "7.8.1", + "@electron-forge/maker-zip": "7.8.1", + "@electron-forge/plugin-auto-unpack-natives": "7.8.1", + "@electron-forge/publisher-github": "7.8.1", + "@playwright/test": "1.53.2", + "@tailwindcss/postcss": "4.1.11", + "@testing-library/react": "16.3.0", "@types/fluent-ffmpeg": "2.1.24", "@types/is-hotkey": "0.1.10", "@types/lodash": "4.17.7", - "@types/node": "20", - "@types/react": "18", - "@types/react-dom": "18", + "@types/node": "22", + "@types/react": "*", + "@types/react-dom": "*", "@types/uuid": "9.0.8", - "@vitejs/plugin-react": "4.3.1", - "@webgpu/types": "0.1.44", - "electron": "31.2.1", - "eslint": "8", - "eslint-config-next": "14.2.5", - "eslint-config-prettier": "9.1.0", - "jsdom": "24.1.0", - "node-gyp": "10.2.0", - "postcss": "8", - "prettier": "3.3.3", - "prettier-plugin-tailwindcss": "0.6.5", - "tailwind-scrollbar": "3.1.0", - "tailwindcss": "3.4.3", - "typescript": "5.5.2", - "vitest": "2.0.2" + "@vitejs/plugin-react": "4.6.0", + "@webgpu/types": "0.1.63", + "electron": "37.1.0", + "eslint": "9.30.1", + "eslint-config-next": "15.3.4", + "eslint-config-prettier": "10.1.5", + "jsdom": "26.1.0", + "node-gyp": "11.2.0", + "postcss": "8.5.6", + "prettier": "3.6.2", + "prettier-plugin-tailwindcss": "0.6.13", + "tailwind-scrollbar": "4.0.2", + "tailwindcss": "4.1.11", + "typescript": "5.8.3", + "vitest": "3.2.4" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-libvips-win32-ia32": "1.0.2", - "@img/sharp-libvips-win32-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4" + "@img/sharp-darwin-arm64": "0.34.2", + "@img/sharp-darwin-x64": "0.34.2", + "@img/sharp-libvips-darwin-arm64": "1.1.0", + "@img/sharp-libvips-darwin-x64": "1.1.0", + "@img/sharp-libvips-linux-arm": "1.1.0", + "@img/sharp-libvips-linux-arm64": "1.1.0", + "@img/sharp-libvips-linux-x64": "1.1.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", + "@img/sharp-libvips-linuxmusl-x64": "1.1.0", + "@img/sharp-libvips-win32-ia32": "1.1.0", + "@img/sharp-libvips-win32-x64": "1.1.0", + "@img/sharp-linux-arm": "0.34.2", + "@img/sharp-linux-arm64": "0.34.2", + "@img/sharp-linux-x64": "0.34.2", + "@img/sharp-linuxmusl-arm64": "0.34.2", + "@img/sharp-linuxmusl-x64": "0.34.2", + "@img/sharp-win32-ia32": "0.34.2", + "@img/sharp-win32-x64": "0.34.2" } } diff --git a/packages/app/postcss.config.js b/packages/app/postcss.config.js index 33ad091d..52b9b4ba 100644 --- a/packages/app/postcss.config.js +++ b/packages/app/postcss.config.js @@ -1,6 +1,5 @@ module.exports = { plugins: { - tailwindcss: {}, - autoprefixer: {}, + '@tailwindcss/postcss': {}, }, } diff --git a/packages/app/src/app/api/assistant/askAnyAssistant.ts b/packages/app/src/app/api/assistant/askAnyAssistant.ts index 1ba08902..7d33bbdd 100644 --- a/packages/app/src/app/api/assistant/askAnyAssistant.ts +++ b/packages/app/src/app/api/assistant/askAnyAssistant.ts @@ -92,7 +92,7 @@ export async function askAnyAssistant({ builtinProviderCredentialsGroq, settings.clapperApiKey ), - modelName, + model: modelName, // temperature: 0.7, }) : provider === ClapWorkflowProvider.OPENAI diff --git a/packages/app/src/app/api/resolve/providers/gradio/index.ts b/packages/app/src/app/api/resolve/providers/gradio/index.ts index cc08cbbd..58d42752 100644 --- a/packages/app/src/app/api/resolve/providers/gradio/index.ts +++ b/packages/app/src/app/api/resolve/providers/gradio/index.ts @@ -9,6 +9,8 @@ export async function resolveSegment( ): Promise { const segment = request.segment + console.log('calling a Gradio API for a segment') + if (request.segment.category === ClapSegmentCategory.IMAGE) { segment.assetUrl = await callGradioApi({ url: request.settings.gradioApiUrlForImage, diff --git a/packages/app/src/app/api/resolve/providers/huggingface/generateImage.ts b/packages/app/src/app/api/resolve/providers/huggingface/generateImage.ts index f577cc85..b7062a9d 100644 --- a/packages/app/src/app/api/resolve/providers/huggingface/generateImage.ts +++ b/packages/app/src/app/api/resolve/providers/huggingface/generateImage.ts @@ -1,4 +1,4 @@ -import { HfInference, HfInferenceEndpoint } from '@huggingface/inference' +import { InferenceClient } from '@huggingface/inference' import { decodeOutput } from '@/lib/utils/decodeOutput' import { ResolveRequest } from '@aitube/clapper-services' @@ -40,29 +40,34 @@ export async function generateImage(request: ResolveRequest): Promise { } } - const hf: HfInferenceEndpoint = new HfInference(apiKey) + const hf = new InferenceClient(apiKey) - const blob: Blob = await hf.textToImage({ - model: request.settings.imageGenerationWorkflow.data, - inputs: request.prompts.image.positive, - parameters: { - height: request.meta.height, - width: request.meta.width, + const blob: Blob = await hf.textToImage( + { + model: request.settings.imageGenerationWorkflow.data, + inputs: request.prompts.image.positive, + parameters: { + height: request.meta.height, + width: request.meta.width, - // this triggers the following exception: - // Error: __call__() got an unexpected keyword argument 'negative_prompt' - // negative_prompt: request.prompts.image.negative || '', + // this triggers the following exception: + // Error: __call__() got an unexpected keyword argument 'negative_prompt' + // negative_prompt: request.prompts.image.negative || '', - /** - * The number of denoising steps. More denoising steps usually lead to a higher quality image at the expense of slower inference. - */ - // num_inference_steps?: number; - /** - * Guidance scale: Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, usually at the expense of lower image quality. - */ - // guidance_scale?: number; + /** + * The number of denoising steps. More denoising steps usually lead to a higher quality image at the expense of slower inference. + */ + // num_inference_steps?: number; + /** + * Guidance scale: Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, usually at the expense of lower image quality. + */ + // guidance_scale?: number; + }, }, - }) + { + outputType: 'blob', + } + ) // console.log('output from Hugging Face Inference API:', blob) diff --git a/packages/app/src/app/api/resolve/providers/huggingface/generateMusic.ts b/packages/app/src/app/api/resolve/providers/huggingface/generateMusic.ts index 5373cb9a..7ba08e1c 100644 --- a/packages/app/src/app/api/resolve/providers/huggingface/generateMusic.ts +++ b/packages/app/src/app/api/resolve/providers/huggingface/generateMusic.ts @@ -1,4 +1,4 @@ -import { HfInference, HfInferenceEndpoint } from '@huggingface/inference' +import { InferenceClient } from '@huggingface/inference' import { ResolveRequest } from '@aitube/clapper-services' import { @@ -37,7 +37,7 @@ export async function generateMusic(request: ResolveRequest): Promise { } } - const hf: HfInferenceEndpoint = new HfInference(apiKey) + const hf = new InferenceClient(apiKey) /* hf.textToMusic doesn't exist yet! diff --git a/packages/app/src/app/api/resolve/providers/huggingface/generateVideo.ts b/packages/app/src/app/api/resolve/providers/huggingface/generateVideo.ts index f60d1151..d6f8d447 100644 --- a/packages/app/src/app/api/resolve/providers/huggingface/generateVideo.ts +++ b/packages/app/src/app/api/resolve/providers/huggingface/generateVideo.ts @@ -4,6 +4,8 @@ import { builtinProviderCredentialsHuggingface, clapperApiKeyToUseBuiltinCredentials, } from '@/app/api/globalSettings' +import { ClapInputCategory } from '@aitube/clap' +import { getWorkflowInputValues } from '../getWorkflowInputValues' export async function generateVideo(request: ResolveRequest): Promise { if (!request.settings.videoGenerationWorkflow.data) { @@ -11,13 +13,6 @@ export async function generateVideo(request: ResolveRequest): Promise { `HuggingFace.generateVideo: cannot generate without a valid videoGenerationWorkflow.data` ) } - - if (!request.prompts.video.image) { - throw new Error( - `HuggingFace.generateVideo: cannot generate without a valid input image prompt` - ) - } - let apiKey = request.settings.huggingFaceApiKey if (!apiKey) { @@ -36,10 +31,81 @@ export async function generateVideo(request: ResolveRequest): Promise { } } + const { workflowDefaultValues, workflowValues } = getWorkflowInputValues( + request.settings.videoGenerationWorkflow + ) + + console.log('HuggingFace.generateVideo prompts: ', request.prompts) + + console.log( + 'HuggingFace.generateVideo: workflowDefaultValues:', + workflowDefaultValues + ) + + console.log('HuggingFace.generateVideo: workflowValues:', workflowValues) + + const supportsTextPrompt = + request.settings.videoGenerationWorkflow.inputFields.some( + (field) => field.category === ClapInputCategory.PROMPT + ) + const supportsImagePrompt = + request.settings.videoGenerationWorkflow.inputFields.some((field) => + [ClapInputCategory.IMAGE_URL, ClapInputCategory.IMAGE_URLS].includes( + field.category + ) + ) + + if (supportsTextPrompt) { + console.log( + 'HuggingFace.generateVideo: this workflow supports text prompts' + ) + request.settings.videoGenerationWorkflow.inputValues.prompt = + request.prompts.image.positive + } else { + console.log( + 'HuggingFace.generateVideo: this workflow does NOT support text prompts' + ) + } + + if (supportsImagePrompt) { + console.log( + 'HuggingFace.generateVideo: this workflow supports image prompts' + ) + // TODO: replace the workflowValues.prompt or something? + } else { + console.log( + 'HuggingFace.generateVideo: this workflow does NOT support image prompts' + ) + } + + if (!request.prompts.video.image) { + console.log( + 'HuggingFace.generateVideo: request.settings.videoGenerationWorkflow:', + request.settings.videoGenerationWorkflow + ) + + if (!supportsImagePrompt && supportsTextPrompt) { + // the provider only works on text anyway so we're good + } else { + // note: actually we should check if the field is mandatory or not + throw new Error( + `HuggingFace.generateVideo: cannot generate without a valid input image prompt` + ) + } + } + + console.log( + 'HuggingFace.generateVideo: calling gradio API with inputs: ', + request.settings.videoGenerationWorkflow.inputValues + ) // TODO pass a type to the template function const assetUrl = await callGradioApi({ url: request.settings.videoGenerationWorkflow.data, - inputs: request.prompts.video, + + // there is a small incompatibility here, + // we should check if it's possible some fields are string[] + inputs: request.settings.videoGenerationWorkflow.inputValues as any, + apiKey, }) diff --git a/packages/app/src/app/api/resolve/providers/huggingface/generateVideoStream.ts b/packages/app/src/app/api/resolve/providers/huggingface/generateVideoStream.ts new file mode 100644 index 00000000..5ae21e78 --- /dev/null +++ b/packages/app/src/app/api/resolve/providers/huggingface/generateVideoStream.ts @@ -0,0 +1,52 @@ +import { ResolveRequest } from '@aitube/clapper-services' +import { callGradioApi } from '@/lib/hf/callGradioApi' +import { + builtinProviderCredentialsHuggingface, + clapperApiKeyToUseBuiltinCredentials, +} from '@/app/api/globalSettings' + +/** + * For now this is just an experiment, to try to see what we can do with TS streams + */ +export async function EXPERIMENTAL_generateVideoStream( + request: ResolveRequest +): Promise { + if (!request.settings.videoGenerationWorkflow.data) { + throw new Error( + `HuggingFace.EXPERIMENTAL_generateVideoStream: cannot generate without a valid videoGenerationWorkflow.data` + ) + } + + if (!request.prompts.video.image) { + throw new Error( + `HuggingFace.EXPERIMENTAL_generateVideoStream: cannot generate without a valid input image prompt` + ) + } + + let apiKey = request.settings.huggingFaceApiKey + + if (!apiKey) { + if (clapperApiKeyToUseBuiltinCredentials) { + if ( + request.settings.clapperApiKey !== clapperApiKeyToUseBuiltinCredentials + ) { + throw new Error(`Missing API key for "Hugging Face"`) + } else { + // user has a valid Clapper API key, so they are allowed to use the built-in credentials + apiKey = builtinProviderCredentialsHuggingface + } + } else { + // no Clapper API key is defined, so we give free access to the built-in credentials + apiKey = builtinProviderCredentialsHuggingface + } + } + + // TODO pass a type to the template function + const assetUrl = await callGradioApi({ + url: request.settings.videoGenerationWorkflow.data, + inputs: request.prompts.video, + apiKey, + }) + + return assetUrl +} diff --git a/packages/app/src/app/api/resolve/providers/huggingface/generateVoice.ts b/packages/app/src/app/api/resolve/providers/huggingface/generateVoice.ts index 171812d1..9136480a 100644 --- a/packages/app/src/app/api/resolve/providers/huggingface/generateVoice.ts +++ b/packages/app/src/app/api/resolve/providers/huggingface/generateVoice.ts @@ -1,4 +1,4 @@ -import { HfInference, HfInferenceEndpoint } from '@huggingface/inference' +import { InferenceClient } from '@huggingface/inference' import { ResolveRequest } from '@aitube/clapper-services' import { @@ -37,7 +37,7 @@ export async function generateVoice(request: ResolveRequest): Promise { } } - const hf: HfInferenceEndpoint = new HfInference(apiKey) + const hf = new InferenceClient(apiKey) const blob: Blob = await hf.textToSpeech({ model: request.settings.voiceGenerationWorkflow.data, diff --git a/packages/app/src/app/api/resolve/providers/huggingface/index.ts b/packages/app/src/app/api/resolve/providers/huggingface/index.ts index 319dd1a6..7fb05424 100644 --- a/packages/app/src/app/api/resolve/providers/huggingface/index.ts +++ b/packages/app/src/app/api/resolve/providers/huggingface/index.ts @@ -1,4 +1,4 @@ -import { HfInference, HfInferenceEndpoint } from '@huggingface/inference' +import { InferenceClient } from '@huggingface/inference' import { ResolveRequest } from '@aitube/clapper-services' import { ClapSegmentCategory } from '@aitube/clap' diff --git a/packages/app/src/app/api/resolve/providers/lumalabs/index.ts b/packages/app/src/app/api/resolve/providers/lumalabs/index.ts index 0baf37dd..69a9e6b8 100644 --- a/packages/app/src/app/api/resolve/providers/lumalabs/index.ts +++ b/packages/app/src/app/api/resolve/providers/lumalabs/index.ts @@ -57,6 +57,7 @@ export async function resolveSegment( aspect_ratio: aspectRatio, prompt: request.prompts.image.positive || '', + model: 'ray-flash-2', } // Luma crashes (and spends our money :/) if keyframes is an empty array diff --git a/packages/app/src/app/api/resolve/route.ts b/packages/app/src/app/api/resolve/route.ts index 9d094e54..4915b94f 100644 --- a/packages/app/src/app/api/resolve/route.ts +++ b/packages/app/src/app/api/resolve/route.ts @@ -159,7 +159,11 @@ export async function POST(req: NextRequest) { */ } } catch (err) { - console.error(`failed to generate a segment: ${err}`) + if (typeof err?.['message'] === 'string') { + console.error(`failed to generate a segment: ${err?.['message']}`) + } else { + console.error(`failed to generate a segment:`, err) + } segment.assetUrl = '' segment.assetSourceType = ClapAssetSource.EMPTY segment.assetDurationInMs = 0 diff --git a/packages/app/src/app/embed/EmbeddedPlayer.tsx b/packages/app/src/app/embed/EmbeddedPlayer.tsx index 7227b24b..dd7cff0b 100644 --- a/packages/app/src/app/embed/EmbeddedPlayer.tsx +++ b/packages/app/src/app/embed/EmbeddedPlayer.tsx @@ -16,7 +16,7 @@ export function EmbeddedPlayer() { const [isOverlayVisible, setOverlayVisible] = useState(true) - const overlayTimerRef = useRef() + const overlayTimerRef = useRef(null) // const videoLayerRef = useRef(null) // const segmentationLayerRef = useRef(null) @@ -24,12 +24,12 @@ export function EmbeddedPlayer() { isPlayingRef.current = isPlaying const scheduleOverlayInvisibility = () => { - clearTimeout(overlayTimerRef.current) + clearTimeout(overlayTimerRef.current!) overlayTimerRef.current = setTimeout(() => { if (isPlayingRef.current) { setOverlayVisible(!isPlayingRef.current) } - clearTimeout(overlayTimerRef.current) + clearTimeout(overlayTimerRef.current!) }, 3000) } @@ -38,7 +38,7 @@ export function EmbeddedPlayer() { {/* content overlay, with the gradient, buttons etc */}
{ diff --git a/packages/app/src/app/embed/StaticOrInteractive.tsx b/packages/app/src/app/embed/StaticOrInteractive.tsx index 4371643b..460f91e6 100644 --- a/packages/app/src/app/embed/StaticOrInteractive.tsx +++ b/packages/app/src/app/embed/StaticOrInteractive.tsx @@ -18,8 +18,8 @@ export function StaticOrInteractiveTag({ className={cn( `flex flex-none flex-row items-center justify-center border font-medium uppercase`, { - 'rounded-xs space-x-0.5 py-0.5 pl-0.5 pr-1 text-2xs': size === 'sm', - 'space-x-1 rounded py-1 pl-1 pr-2 text-xs': size === 'md', + 'text-2xs space-x-0.5 rounded-xs py-0.5 pr-1 pl-0.5': size === 'sm', + 'space-x-1 rounded-sm py-1 pr-2 pl-1 text-xs': size === 'md', 'border-yellow-600 text-yellow-600': isInteractive, // " text-red-500 border-red-500": isLive, 'border-neutral-600 text-neutral-600': isStatic, diff --git a/packages/app/src/app/main.tsx b/packages/app/src/app/main.tsx index 34c32233..1705390b 100644 --- a/packages/app/src/app/main.tsx +++ b/packages/app/src/app/main.tsx @@ -278,7 +278,7 @@ function MainContent({ mode }: { mode: ClapperIntegrationMode }) { >
@@ -291,7 +291,7 @@ function MainContent({ mode }: { mode: ClapperIntegrationMode }) {
diff --git a/packages/app/src/app/styles/globals.css b/packages/app/src/app/styles/globals.css index fd81e885..5dc16836 100644 --- a/packages/app/src/app/styles/globals.css +++ b/packages/app/src/app/styles/globals.css @@ -1,6 +1,4 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; :root { --foreground-rgb: 0, 0, 0; diff --git a/packages/app/src/app/styles/react-reflex-custom.css b/packages/app/src/app/styles/react-reflex-custom.css index 14641550..8b7dd164 100644 --- a/packages/app/src/app/styles/react-reflex-custom.css +++ b/packages/app/src/app/styles/react-reflex-custom.css @@ -1,7 +1,3 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - /* This extends the original react-reflex.css (from react-reflex@4.1.0), with some custom colors and sizes @@ -11,31 +7,43 @@ body .reflex-splitter { opacity: 0.2; } -@layer components { - body .reflex-container > .reflex-splitter { - @apply bg-neutral-400 transition-all duration-200 ease-in-out; - } +body .reflex-container > .reflex-splitter { + background-color: #a3a3a3; /* bg-neutral-400 */ + transition: all 200ms ease-in-out; +} - body .reflex-container > .reflex-splitter.active, - body .reflex-container > .reflex-splitter:hover { - @apply bg-neutral-200 transition-all duration-200 ease-in-out; - } +body .reflex-container > .reflex-splitter.active, +body .reflex-container > .reflex-splitter:hover { + background-color: #e5e5e5; /* bg-neutral-200 */ + transition: all 200ms ease-in-out; +} - body .horizontal > .reflex-splitter { - @apply h-[3px] border-b-neutral-400 border-t-neutral-400 bg-neutral-400; - } +body .horizontal > .reflex-splitter { + height: 3px; + border-top: 1px solid #a3a3a3; /* border-t-neutral-400 */ + border-bottom: 1px solid #a3a3a3; /* border-b-neutral-400 */ + background-color: #a3a3a3; /* bg-neutral-400 */ +} - body .reflex-container.horizontal > .reflex-splitter:hover, - body .reflex-container.horizontal > .reflex-splitter.active { - @apply h-[3px] border-b-neutral-200 border-t-neutral-200 bg-neutral-200; - } +body .reflex-container.horizontal > .reflex-splitter:hover, +body .reflex-container.horizontal > .reflex-splitter.active { + height: 3px; + border-top: 1px solid #e5e5e5; /* border-t-neutral-200 */ + border-bottom: 1px solid #e5e5e5; /* border-b-neutral-200 */ + background-color: #e5e5e5; /* bg-neutral-200 */ +} - body .reflex-container.vertical > .reflex-splitter { - @apply w-[3px] border-l-neutral-400 border-r-neutral-400 bg-neutral-400; - } +body .reflex-container.vertical > .reflex-splitter { + width: 3px; + border-right: 1px solid #a3a3a3; /* border-r-neutral-400 */ + border-left: 1px solid #a3a3a3; /* border-l-neutral-400 */ + background-color: #a3a3a3; /* bg-neutral-400 */ +} - body .reflex-container.vertical > .reflex-splitter:hover, - body .reflex-container.vertical > .reflex-splitter.active { - @apply w-[3px] border-l-neutral-200 border-r-neutral-200 bg-neutral-200; - } +body .reflex-container.vertical > .reflex-splitter:hover, +body .reflex-container.vertical > .reflex-splitter.active { + width: 3px; + border-right: 1px solid #e5e5e5; /* border-r-neutral-200 */ + border-left: 1px solid #e5e5e5; /* border-l-neutral-200 */ + background-color: #e5e5e5; /* bg-neutral-200 */ } diff --git a/packages/app/src/components/assistant/ChatBubble.tsx b/packages/app/src/components/assistant/ChatBubble.tsx index c1dc74ad..87328509 100644 --- a/packages/app/src/components/assistant/ChatBubble.tsx +++ b/packages/app/src/components/assistant/ChatBubble.tsx @@ -27,7 +27,11 @@ export function ChatBubble({ )} >
- - {message} - + {message}
diff --git a/packages/app/src/components/core/timeline/Slider.tsx b/packages/app/src/components/core/timeline/Slider.tsx index 7ed8cb1c..69b49328 100644 --- a/packages/app/src/components/core/timeline/Slider.tsx +++ b/packages/app/src/components/core/timeline/Slider.tsx @@ -13,14 +13,14 @@ const Slider = React.forwardRef< ({
  • @@ -56,7 +56,7 @@ export function Node({ className={cn( 'group flex flex-row items-center space-x-2 border-[1.5px] border-transparent', isFocusable && - 'focus-within:border-transparent group-focus:border-gray-900/0', + 'group-focus:border-gray-900/0 focus-within:border-transparent', /* isSelected ? 'bg-gray-700/100 text-gray-200' @@ -71,9 +71,9 @@ export function Node({ {node.children?.length ? ( <> {showArrows ? ( - + ) : null} -
    +
    {node.icon ? (
    @@ -86,7 +86,7 @@ export function Node({ ) : (
    @@ -148,7 +148,9 @@ export function Node({ }, }} {...treeGroupProps} - className={cn('pl-3')} + // we can't use a className here work for some reason, + // sqo I've moved this to line 166 + // className={cn('pl-3')} > ({ // let's just hid it for flex-nowrap `opacity-0`, - `absolute bottom-0 left-3.5 top-[31px] z-[-1] h-[calc(100%-30px)] -translate-x-1/2 transform` + `ml-3`, + + `absolute top-[31px] bottom-0 left-3.5 z-[-1] h-[calc(100%-30px)] -translate-x-1/2 transform` )} key={node.id + 'line'} stroke="currentColor" diff --git a/packages/app/src/components/core/tree/roving.tsx b/packages/app/src/components/core/tree/roving.tsx index 73d71cf1..4125ee33 100644 --- a/packages/app/src/components/core/tree/roving.tsx +++ b/packages/app/src/components/core/tree/roving.tsx @@ -95,7 +95,7 @@ export function RovingTabindexRoot({ { + onFocus={(e: any) => { if (e.target !== e.currentTarget) return if (isShiftTabbing) return const orderedItems = getOrderedItems() @@ -110,8 +110,8 @@ export function RovingTabindexRoot({ focusFirst(candidates) }} onBlur={() => setIsShiftTabbing(false)} - ref={rootRef} - {...props} + ref={rootRef as any} + {...(props as any)} > {children} diff --git a/packages/app/src/components/dialogs/iframe-warning/index.tsx b/packages/app/src/components/dialogs/iframe-warning/index.tsx index 43085f36..8e587b2c 100644 --- a/packages/app/src/components/dialogs/iframe-warning/index.tsx +++ b/packages/app/src/components/dialogs/iframe-warning/index.tsx @@ -5,6 +5,14 @@ import { cn } from '@/lib/utils' import { useTheme } from '@/services' import { useEffect, useState } from 'react' +/** + * When Clapper is hosted in a space, using the fingers to scroll in the timeline + * make the page go back to the previous one, so the user loses all their data + * + * obviously people aren't happy about this, so with a heavy heart I took the decision + * to disable clapper on Hugging Face (I'll try to see of there are alternative solutions, maybe using the original subdomain) + * @returns + */ export function IframeWarning() { const [showWarning, setShowWarning] = useState(false) const theme = useTheme() @@ -13,12 +21,11 @@ export function IframeWarning() { setShowWarning(window.self !== window.top) return () => {} }, []) - // TODO: read our global state return (
    Entities
    -
    +
    {panels[showSettings]}
    - + {children} diff --git a/packages/app/src/components/toolbars/top-menu/ToggleWindowLayout/index.tsx b/packages/app/src/components/toolbars/top-menu/ToggleWindowLayout/index.tsx index b1f45a55..b0d0ebae 100644 --- a/packages/app/src/components/toolbars/top-menu/ToggleWindowLayout/index.tsx +++ b/packages/app/src/components/toolbars/top-menu/ToggleWindowLayout/index.tsx @@ -26,7 +26,7 @@ export function ToggleWindowLayout({
    { @@ -59,7 +59,7 @@ export function ToggleWindowLayout({
    - + Toggle layout diff --git a/packages/app/src/components/toolbars/top-menu/index.tsx b/packages/app/src/components/toolbars/top-menu/index.tsx index aff162c7..85063928 100644 --- a/packages/app/src/components/toolbars/top-menu/index.tsx +++ b/packages/app/src/components/toolbars/top-menu/index.tsx @@ -117,7 +117,7 @@ export function TopMenu() { */} @@ -149,7 +149,7 @@ export function TopMenu() {
    - + Community @@ -162,7 +162,7 @@ export function TopMenu() {
    - + Code diff --git a/packages/app/src/components/toolbars/top-menu/view/index.tsx b/packages/app/src/components/toolbars/top-menu/view/index.tsx index f47a5984..4b72bd91 100644 --- a/packages/app/src/components/toolbars/top-menu/view/index.tsx +++ b/packages/app/src/components/toolbars/top-menu/view/index.tsx @@ -65,7 +65,7 @@ export function TopMenuView() { >
    @@ -103,7 +103,7 @@ export function TopMenuView() { >
    diff --git a/packages/app/src/components/ui/accordion.tsx b/packages/app/src/components/ui/accordion.tsx index 6a41e175..fe86fb0b 100644 --- a/packages/app/src/components/ui/accordion.tsx +++ b/packages/app/src/components/ui/accordion.tsx @@ -47,12 +47,12 @@ const AccordionContent = React.forwardRef< -
    {children}
    +
    {children}
    )) AccordionContent.displayName = AccordionPrimitive.Content.displayName diff --git a/packages/app/src/components/ui/alert.tsx b/packages/app/src/components/ui/alert.tsx index 05e51a6e..151bfb4b 100644 --- a/packages/app/src/components/ui/alert.tsx +++ b/packages/app/src/components/ui/alert.tsx @@ -39,7 +39,7 @@ const AlertTitle = React.forwardRef< >(({ className, ...props }, ref) => (
    )) diff --git a/packages/app/src/components/ui/badge.tsx b/packages/app/src/components/ui/badge.tsx index 482b487f..aa7335b1 100644 --- a/packages/app/src/components/ui/badge.tsx +++ b/packages/app/src/components/ui/badge.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils/cn' const badgeVariants = cva( - 'inline-flex items-center rounded-full border border-neutral-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 dark:border-neutral-800 dark:focus:ring-neutral-800', + 'inline-flex items-center rounded-full border border-neutral-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 dark:border-neutral-800 dark:focus:ring-neutral-800', { variants: { variant: { diff --git a/packages/app/src/components/ui/button.tsx b/packages/app/src/components/ui/button.tsx index 8098363c..584fa5a4 100644 --- a/packages/app/src/components/ui/button.tsx +++ b/packages/app/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils/cn' const buttonVariants = cva( - 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-800', + 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-800', { variants: { variant: { diff --git a/packages/app/src/components/ui/card.tsx b/packages/app/src/components/ui/card.tsx index 8aa87718..e68d78c1 100644 --- a/packages/app/src/components/ui/card.tsx +++ b/packages/app/src/components/ui/card.tsx @@ -9,7 +9,7 @@ const Card = React.forwardRef<
    { return ( - + {children} @@ -46,7 +46,7 @@ const CommandInput = React.forwardRef< (({ className, ...props }, ref) => ( )) @@ -117,7 +117,7 @@ const CommandItem = React.forwardRef< {children} - + Close @@ -89,7 +89,7 @@ const DialogTitle = React.forwardRef< ( // we let the underlying tint sink in 'font-mono', - 'focus-visible:ring-blue-[rgb(59,134,247)] flex h-full w-full rounded-md border border-neutral-50/80 bg-white px-3 py-2 text-xs ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-400/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-30 dark:border-neutral-100/30 dark:bg-black/20 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-800', + 'focus-visible:ring-blue-[rgb(59,134,247)] flex h-full w-full rounded-md border border-neutral-50/80 bg-white px-3 py-2 text-xs ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-400/80 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-30 dark:border-neutral-100/30 dark:bg-black/20 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-800', className )} ref={ref} diff --git a/packages/app/src/components/ui/menubar-legacy-react18.tsx b/packages/app/src/components/ui/menubar-legacy-react18.tsx new file mode 100644 index 00000000..1583a95b --- /dev/null +++ b/packages/app/src/components/ui/menubar-legacy-react18.tsx @@ -0,0 +1,452 @@ +'use client' + +import * as React from 'react' +import * as MenubarPrimitive from '@radix-ui/react-menubar' +import { + Check, + ChevronRight, + Circle, + Menu, + X, + ChevronLeft, + ChevronUp, + ChevronDown, +} from 'lucide-react' + +import { cn } from '@/lib/utils/cn' +import { useBreakpoints } from '@/lib/hooks/useBreakpoints' + +const MenubarMenu = MenubarPrimitive.Menu + +const MenubarGroup = MenubarPrimitive.Group + +const MenubarPortal = MenubarPrimitive.Portal + +const MenubarSub = MenubarPrimitive.Sub + +const MenubarRadioGroup = MenubarPrimitive.RadioGroup + +// Add new components for mobile menu +const MobileMenuTrigger = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +MobileMenuTrigger.displayName = 'MobileMenuTrigger' + +const MobileMenuDrawer = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { + isOpen: boolean + onClose: () => void + } +>(({ className, isOpen, onClose, children, ...props }, ref) => ( +
    + +
    {children}
    +
    +)) +MobileMenuDrawer.displayName = 'MobileMenuDrawer' + +interface MobileMenuProps { + items: React.ReactNode + onBack?: () => void + title?: string +} + +const MobileMenu: React.FC = ({ items, onBack, title }) => ( +
    + {(onBack || title) && ( +
    + {onBack && ( + + )} + {title &&

    {title}

    } +
    + )} +
    {items}
    +
    +) + +MobileMenu.displayName = 'MobileMenu' + +const MobileMenuItem: React.FC<{ + label: React.ReactNode + children?: React.ReactNode + onClick?: () => void +}> = ({ label, children, onClick }) => { + const [isOpen, setIsOpen] = React.useState(false) + + const handleClick = () => { + if (children) { + setIsOpen(!isOpen) + } else if (onClick) { + onClick() + } + } + + return ( +
    + + {children && ( +
    + {children} +
    + )} +
    + ) +} + +const Menubar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => { + const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false) + const { isMd } = useBreakpoints() + + const renderMobileMenuItems = ( + items: React.ReactNode, + depth = 0 + ): React.ReactNode => { + return React.Children.map(items, (child) => { + if (React.isValidElement(child)) { + if (child.type === MenubarMenu) { + const trigger = (child.props as any).children.find( + (c: React.ReactElement) => c.type === MenubarTrigger + ) + const content = (child.props as any).children.find( + (c: React.ReactElement) => c.type === MenubarContent + ) + + return ( + + {renderMobileMenuItems( + (content.props as any).children, + depth + 1 + )} + + ) + } else if (child.type === MenubarItem) { + return ( + { + if ((child.props as any).onClick) { + ;(child.props as any).onClick() + } + setIsMobileMenuOpen(false) + }} + /> + ) + } else if (child.type === MenubarSub) { + const subTrigger = (child.props as any).children.find( + (c: React.ReactElement) => c.type === MenubarSubTrigger + ) + const subContent = (child.props as any).children.find( + (c: React.ReactElement) => c.type === MenubarSubContent + ) + return ( + + {renderMobileMenuItems( + (subContent.props as any).children, + depth + 1 + )} + + ) + } else if (child.type === MenubarSeparator) { + return
    + } + } + return child + }) + } + + return ( + + {!isMd && ( + + )} + {!isMd && ( +
    +
    +
    +

    Menu

    + +
    +
    + {renderMobileMenuItems(children)} +
    +
    +
    + )} + {isMd && ( +
    + {children} +
    + )} +
    + ) +}) +Menubar.displayName = MenubarPrimitive.Root.displayName + +const MenubarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName + +const MenubarSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName + +const MenubarSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName + +const MenubarContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, align = 'start', alignOffset = -4, sideOffset = 8, ...props }, + ref + ) => ( + + + + ) +) +MenubarContent.displayName = MenubarPrimitive.Content.displayName + +const MenubarItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +MenubarItem.displayName = MenubarPrimitive.Item.displayName + +const MenubarCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName + +const MenubarRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName + +const MenubarLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +MenubarLabel.displayName = MenubarPrimitive.Label.displayName + +const MenubarSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName + +const MenubarShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +MenubarShortcut.displayname = 'MenubarShortcut' + +export { + Menubar, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarItem, + MenubarSeparator, + MenubarLabel, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarPortal, + MenubarSubContent, + MenubarSubTrigger, + MenubarGroup, + MenubarSub, + MenubarShortcut, +} diff --git a/packages/app/src/components/ui/menubar-legacy.tsx b/packages/app/src/components/ui/menubar-original-react18.tsx similarity index 71% rename from packages/app/src/components/ui/menubar-legacy.tsx rename to packages/app/src/components/ui/menubar-original-react18.tsx index 0e530d03..680cb67d 100644 --- a/packages/app/src/components/ui/menubar-legacy.tsx +++ b/packages/app/src/components/ui/menubar-original-react18.tsx @@ -38,7 +38,7 @@ const MenubarTrigger = React.forwardRef< +>(({ className, ...props }, ref) => ( + +)) +MobileMenuTrigger.displayName = 'MobileMenuTrigger' + +const MobileMenuDrawer = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { + isOpen: boolean + onClose: () => void + } +>(({ className, isOpen, onClose, children, ...props }, ref) => ( +
    + +
    {children}
    +
    +)) +MobileMenuDrawer.displayName = 'MobileMenuDrawer' + +interface MobileMenuProps { + items: React.ReactNode + onBack?: () => void + title?: string +} + +const MobileMenu: React.FC = ({ items, onBack, title }) => ( +
    + {(onBack || title) && ( +
    + {onBack && ( + + )} + {title &&

    {title}

    } +
    + )} +
    {items}
    +
    +) + +MobileMenu.displayName = 'MobileMenu' + +const MobileMenuItem: React.FC<{ + label: React.ReactNode + children?: React.ReactNode + onClick?: () => void +}> = ({ label, children, onClick }) => { + const [isOpen, setIsOpen] = React.useState(false) + + const handleClick = () => { + if (children) { + setIsOpen(!isOpen) + } else if (onClick) { + onClick() + } + } + + return ( +
    + + {children && ( +
    + {children} +
    + )} +
    + ) +} + +function Menubar({ + className, + children, + ...props +}: React.ComponentProps) { + const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false) + const { isMd } = useBreakpoints() + + const renderMobileMenuItems = ( + items: React.ReactNode, + depth = 0 + ): React.ReactNode => { + return React.Children.map(items, (child) => { + if (React.isValidElement(child)) { + if (child.type === MenubarMenu) { + const trigger = (child.props as any).children.find( + (c: React.ReactElement) => c.type === MenubarTrigger + ) + const content = (child.props as any).children.find( + (c: React.ReactElement) => c.type === MenubarContent + ) + + return ( + + {renderMobileMenuItems( + (content.props as any).children, + depth + 1 + )} + + ) + } else if (child.type === MenubarItem) { + return ( + { + if ((child.props as any).onClick) { + ;(child.props as any).onClick() + } + setIsMobileMenuOpen(false) + }} + /> + ) + } else if (child.type === MenubarSub) { + const subTrigger = (child.props as any).children.find( + (c: React.ReactElement) => c.type === MenubarSubTrigger + ) + const subContent = (child.props as any).children.find( + (c: React.ReactElement) => c.type === MenubarSubContent + ) + return ( + + {renderMobileMenuItems( + (subContent.props as any).children, + depth + 1 + )} + + ) + } else if (child.type === MenubarSeparator) { + return
    + } + } + return child + }) + } + + return ( + + {!isMd && ( + + )} + {!isMd && ( +
    +
    +
    +

    Menu

    + +
    +
    + {renderMobileMenuItems(children)} +
    +
    +
    + )} + {isMd && ( +
    + {children} +
    + )} +
    + ) +} + +function MenubarTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function MenubarSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarContent({ + className, + align = 'start', + alignOffset = -4, + sideOffset = 8, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function MenubarItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: 'default' | 'destructive' +}) { + return ( + + ) +} + +function MenubarCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function MenubarRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function MenubarLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function MenubarSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarShortcut({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ) +} + +export { + Menubar, + MenubarPortal, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarGroup, + MenubarSeparator, + MenubarLabel, + MenubarItem, + MenubarShortcut, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSub, + MenubarSubTrigger, + MenubarSubContent, +} diff --git a/packages/app/src/components/ui/menubar.tsx b/packages/app/src/components/ui/menubar.tsx index 23acd2dd..1583a95b 100644 --- a/packages/app/src/components/ui/menubar.tsx +++ b/packages/app/src/components/ui/menubar.tsx @@ -34,7 +34,7 @@ const MobileMenuTrigger = React.forwardRef< @@ -147,40 +147,46 @@ const Menubar = React.forwardRef< return React.Children.map(items, (child) => { if (React.isValidElement(child)) { if (child.type === MenubarMenu) { - const trigger = child.props.children.find( + const trigger = (child.props as any).children.find( (c: React.ReactElement) => c.type === MenubarTrigger ) - const content = child.props.children.find( + const content = (child.props as any).children.find( (c: React.ReactElement) => c.type === MenubarContent ) return ( - - {renderMobileMenuItems(content.props.children, depth + 1)} + + {renderMobileMenuItems( + (content.props as any).children, + depth + 1 + )} ) } else if (child.type === MenubarItem) { return ( { - if (child.props.onClick) { - child.props.onClick() + if ((child.props as any).onClick) { + ;(child.props as any).onClick() } setIsMobileMenuOpen(false) }} /> ) } else if (child.type === MenubarSub) { - const subTrigger = child.props.children.find( + const subTrigger = (child.props as any).children.find( (c: React.ReactElement) => c.type === MenubarSubTrigger ) - const subContent = child.props.children.find( + const subContent = (child.props as any).children.find( (c: React.ReactElement) => c.type === MenubarSubContent ) return ( - - {renderMobileMenuItems(subContent.props.children, depth + 1)} + + {renderMobileMenuItems( + (subContent.props as any).children, + depth + 1 + )} ) } else if (child.type === MenubarSeparator) { @@ -245,7 +251,7 @@ const MenubarTrigger = React.forwardRef< (({ className, ...props }, ref) => ( )) @@ -81,7 +81,7 @@ const SelectItem = React.forwardRef< (({ className, ...props }, ref) => ( {children} - + Close diff --git a/packages/app/src/components/ui/slider.tsx b/packages/app/src/components/ui/slider.tsx index 005ba8a7..1052ad5f 100644 --- a/packages/app/src/components/ui/slider.tsx +++ b/packages/app/src/components/ui/slider.tsx @@ -12,7 +12,7 @@ const Slider = React.forwardRef< - + )) Slider.displayName = SliderPrimitive.Root.displayName diff --git a/packages/app/src/components/ui/switch.tsx b/packages/app/src/components/ui/switch.tsx index 51744cc3..c7923ad6 100644 --- a/packages/app/src/components/ui/switch.tsx +++ b/packages/app/src/components/ui/switch.tsx @@ -11,7 +11,7 @@ const Switch = React.forwardRef< >(({ className, ...props }, ref) => ( ( return (