Compare commits

...
Author SHA1 Message Date
Brandon Thomas dde547ad1c remove lots of old code from repo 2025-08-24 15:24:54 -04:00
Brandon Thomas f0e8bec162 cleanup titlebar setup 2025-08-24 15:01:31 -04:00
7312 changed files with 14 additions and 576215 deletions
-8
View File
@@ -1,8 +0,0 @@
image_width: 1024
image_height: 1024
#scheduler_steps: 15
# NB(bt): It looks like we can get down to 2 sampling steps, perhaps ?
scheduler_steps: 8
scheduler_samples: 1
cfg_scale: 15.0
seed: 42
@@ -1,51 +1,39 @@
use tauri::{AppHandle, TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
use tauri::window::Color;
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
use errors::AnyhowResult;
use tauri::window::Color;
use tauri::{AppHandle, TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
pub async fn setup_main_window(
app: &AppHandle,
) -> AnyhowResult<()> {
let win_builder =
WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title("Artcraft")
//.decorations(false) // NB: This breaks Mac!
WebviewWindowBuilder::new(app, MAIN_WINDOW_NAME, WebviewUrl::default())
.title("ArtCraft")
.resizable(true)
.fullscreen(false)
.background_color(Color(0, 0, 0, 0))
.enable_clipboard_access()
.inner_size(800.0, 600.0);
.inner_size(2400.0, 1300.0);
// set transparent title bar only when building for macOS
#[cfg(target_os = "macos")]
let win_builder = win_builder
.decorations(true) // NB: Mac requires decorations. Tons of capabilities disappear without it.
.decorations(true) // NB: Mac requires decorations. Tons of capabilities disappear otherwise.
.title_bar_style(TitleBarStyle::Overlay)
.hidden_title(true)
.accept_first_mouse(true) // https://github.com/tauri-apps/tauri/issues/11605#issuecomment-2460112096
.focusable(true)
.focused(true);
let window = win_builder.build()?;
#[cfg(not(target_os = "macos"))]
let win_builder = win_builder
.decorations(false); // NB: This breaks Mac!
/*// set background color only when building for macOS
#[cfg(target_os = "macos")]
{
use cocoa::appkit::{NSColor, NSWindow};
use cocoa::base::{id, nil};
#[cfg(target_os = "windows")]
let win_builder = win_builder
.drag_and_drop(false); // TODO: Is this necessary on Windows?
let ns_window = window.ns_window().unwrap() as id;
unsafe {
let bg_color = NSColor::colorWithRed_green_blue_alpha_(
nil,
50.0 / 255.0,
158.0 / 255.0,
163.5 / 255.0,
0.0,
);
ns_window.setBackgroundColor_(bg_color);
}
}*/
let _window = win_builder.build()?;
Ok(())
}
@@ -11,15 +11,6 @@
},
"app": {
"windows": [
{
"title": "ArtCraft",
"width": 2400,
"height": 1300,
"resizable": true,
"fullscreen": false,
"decorations": true,
"dragDropEnabled": false
}
],
"security": {
"csp": {
-9
View File
@@ -11,15 +11,6 @@
},
"app": {
"windows": [
{
"title": "ArtCraft",
"width": 2400,
"height": 1300,
"resizable": true,
"fullscreen": false,
"decorations": false,
"dragDropEnabled": false
}
],
"security": {
"csp": {
@@ -1,9 +0,0 @@
BASE_API="http://localhost:3000"
GOOGLE_API="http://localhost:3000/google"
FUNNEL_API="http://localhost:3000/funnel"
CDN_API="http://localhost:3000/cdn"
GRAVATAR_API="https://www.gravatar.com"
DEPLOY_PRIME_URL="http://localhost:5173"
REACT_APP_PUBLIC_POSTHOG_KEY="phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
REACT_APP_PUBLIC_POSTHOG_UI="https://us.i.posthog.com"
CONTEXT="DEVELOPMENT"
@@ -1,9 +0,0 @@
BASE_API="https://api.storyteller.ai"
GOOGLE_API="https://storage.googleapis.com"
FUNNEL_API="https://style.storyteller.ai"
CDN_API="https://cdn.storyteller.ai"
GRAVATAR_API="https://www.gravatar.com"
DEPLOY_PRIME_URL="https://studio.storyteller.ai"
REACT_APP_PUBLIC_POSTHOG_KEY="phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
REACT_APP_PUBLIC_POSTHOG_UI="https://us.i.posthog.com"
CONTEXT="DEVELOPMENT"
@@ -1,97 +0,0 @@
/**
* This is intended to be a basic starting point for linting in your app.
* It relies on recommended configs out of the box for simplicity, but you can
* and should modify this configuration to best suit your team's needs.
*/
const SEVERITY = {
OFF: 0,
WARN: 1,
ERROR: 2,
};
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
commonjs: true,
es6: true,
},
// Base config
extends: ["eslint:recommended", "plugin:storybook/recommended"],
ignorePatterns: ["storybook-static/*"],
overrides: [
// React
{
files: ["**/*.{js,jsx,ts,tsx}"],
plugins: ["react", "jsx-a11y"],
extends: [
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
],
settings: {
react: {
version: "detect",
},
formComponents: ["Form"],
linkComponents: [
{ name: "Link", linkAttribute: "to" },
{ name: "NavLink", linkAttribute: "to" },
],
"import/resolver": {
typescript: {},
},
},
rules: {
// we are incapable to provide support for blind people atm
"jsx-a11y/click-events-have-key-events": SEVERITY.OFF,
"jsx-a11y/no-static-element-interactions": SEVERITY.OFF,
"jsx-a11y/media-has-caption": SEVERITY.OFF,
"jsx-a11y/no-autofocus": SEVERITY.OFF,
},
},
// Typescript
{
files: ["**/*.{ts,tsx}"],
plugins: ["@typescript-eslint", "import"],
parser: "@typescript-eslint/parser",
settings: {
"import/internal-regex": "^~/",
"import/resolver": {
node: {
extensions: [".ts", ".tsx"],
},
typescript: {
alwaysTryTypes: true,
},
},
},
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
},
// Node
{
files: [".eslintrc.js"],
env: {
node: true,
},
},
],
};
@@ -1,33 +0,0 @@
# This action syncs "studio-staging" with "main" by force pushing every time it is run.
# Adapted from:
# https://github.community/t/user-github-pages-site-auto-deploy-from-another-branch-on-push/17912/2
on:
push:
branches:
- main
jobs:
sync_studio_staging_branch:
runs-on: [ubuntu-latest]
steps:
- name: checkout main branch
uses: actions/checkout@v2
with:
persist-credentials: false
- name: sync main to studio-staging
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git checkout -b studio-staging
- name: push changes
uses: ad-m/github-push-action@0fafdd62b84042d49ec0cb92d9cac7f7ce4ec79e
with:
# Note: Github does not allow the default access token to push to
# protected branches. Using a PAT with permissions can bypass this,
# but it triggers recursive Github Actions calls on branch updates
# (which the default access token does not). Ultimately, it's better
# to leave the studio-staging branch unprotected.
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: studio-staging
force: true
-27
View File
@@ -1,27 +0,0 @@
.env
.eslintcache
.vite
.notes
node_modules
/~/
proxy/~/
/.cache
/public/build
/build
/backup
.idea
$
# VS Code Setting
.vscode
# Local Netlify folder
.netlify
# Mac
.DS_Store
# Assets to ignore
public/resources/movie/Big_Buck_Bunny_180_10s.webm
public/resources/models/room/room_with_girl.glb
*storybook.log
@@ -1 +0,0 @@
_
@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
-3
View File
@@ -1,3 +0,0 @@
@awesome.me:registry=https://npm.fontawesome.com/
@fortawesome:registry=https://npm.fontawesome.com/
//npm.fontawesome.com/:_authToken=104779DB-FD41-4BEA-BDB4-B1845D758A79
-11
View File
@@ -1,11 +0,0 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"useTabs": false,
"tabWidth": 2
}
@@ -1,23 +0,0 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../**/*.mdx", "../**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-vite",
options: {
builder: {
viteConfigPath: "sb-vite.config.ts",
},
},
},
docs: {
autodocs: "tag",
},
};
export default config;
@@ -1,34 +0,0 @@
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = function expressMiddleware(router) {
router.use(
"/v1",
createProxyMiddleware({
target: "https://api.storyteller.ai/v1",
changeOrigin: true,
on: {
proxyReq: async (req, res) => {
req.setHeader("Access-Control-Allow-Origin", "*");
req.setHeader("Origin", "http://localhost:5173");
req.setHeader("Referer", "http://localhost:5173");
},
},
logger: console,
}),
);
router.use(
"/cdn-cgi",
createProxyMiddleware({
target: "https://cdn.storyteller.ai/cdn-cgi",
changeOrigin: true,
on: {
proxyReq: async (req, res) => {
req.setHeader("Access-Control-Allow-Origin", "*");
req.setHeader("Origin", "http://localhost:5173");
req.setHeader("Referer", "http://localhost:5173");
},
},
logger: console,
}),
);
};
@@ -1,7 +0,0 @@
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
Roboto, "Helvetica Neue", Arial, sans-serif;
color: white;
}
</style>
@@ -1,6 +0,0 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet"
/>
@@ -1,23 +0,0 @@
import type { Preview } from "@storybook/react";
import "tailwindcss/tailwind.css";
import "../app/styles/tailwind.css";
import "../app/styles/normalize.css?url";
import "../app//styles/tailwind.css?url";
import "../app//styles/base.css?url";
import { MemoryRouter } from "react-router";
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [
(story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>,
],
};
export default preview;
-122
View File
@@ -1,122 +0,0 @@
# Storyteller GGF
## Coding Rules and Styles
```
Short Style Guide TS React and Engine:
No use of lets
No basic for loop operators.
Use the collection operators and array operators.
React Side / No modules over 200 lines. ( loose rule )
Don't do:
if (condition) return;
Do:
if (condition) {
return;
}
No use of any type.
If you can use chat gpt to create the interface.
```
## Branches
- `main` - The main branch is the production source of truth.
Changes to main will automatically build, but will not auto-deploy to production. You'll have to promote changes to production using the Netlify control panel.
Merge your code to main frequently. If you have to have a long-lived set of changes, ask yourself whether it might be better to feature flag your code off and
land it to main rather than fight rebasing against the whole team.
- `studio-staging` - The staging auto-deploy branch.
You do not ever need to interact with this branch. Changes to `main` will automatically be pushed to `studio-staging` and auto-deployed to
https://studio-staging.studio.storyteller.ai . You can test changes that have landed in `main` here before promoting them to production.
- `studio-testing` - The testing branch.
Anyone can push their changes to this branch for testing in a production-like environment. Since it is built and hosted on https://studio-testing.studio.storyteller.ai ,
you won't face CORS issues, same-site cookie issues, or have to use a local development proxy. (This doesn't obviate the need for a development
proxy for fast development iteration, but is a fantastic way to test your changes in a "production-like" environment.)
Force push to this branch by using `git push -f origin studio-testing`
## API Docs
Server API docs live here: https://storyteller-docs.netlify.app/
## Local Testing & Development
### Set up `.env` and the Proxy Server
Copy & paste the env settings from `.env-local-proxy` to `.env`, (if the file `.env` do not exist, simply create one at the root of this project's folder beside the other env settings files).
The settings from `.env-local-proxy` directs all requests to `localhost:3000`, this will relieve us from having to deal with CORS between various services.
Then, start the proxy server in the folder `/proxy`. Before running the proxy for the first time, run:
```
npm i
```
To start the proxy run:
```
npm run dev
```
If the proxy fails, make sure your node version is 18.18. If you need to change the node version
run `npm rebuild` once before re-running `npm run dev`
### Run the Code Locally
To setup and run this project's code locally:
```
npm install
npm run dev
```
## PostHog
PostHog is installed and working. To create a feature flag go to https://us.posthog.com/project/75284/feature_flags
Add the feature flag for all users.
To check the feature flag in the app use code similar to this
```ts
import { usePosthogFeatureFlag } from "~/hooks/usePosthogFeatureflag";
import { FeatureFlags } from "~/enums";
const Component = () => {
const flag = usePosthogFeatureFlag(FeaturesFlags.FLAG_NAME);
// if flag is true, the feature shoud be executed & showed;
// if flag is false, the feature should be disabled & hid;
// for example
return(
<>
{flag && <ComponentThatDependsOnTheFlag />}
</>
);
};
```
## Other Documentation
Here is the Swagger documentation for the backend endpoints
- https://storyteller-docs.netlify.app/
This project uses Remix for routing and Tailwind for styling:
- [Remix Docs](https://remix.run/docs)
- [Tailwind Docs](https://tailwindcss.com/docs)
On Netlify, we use functions:
- [Netlify Functions Overview](https://docs.netlify.com/functions/overview)
-3
View File
@@ -1,3 +0,0 @@
# This is to allow Storyteller Studio to use Web Workers to call the engine
/*
Cross-Origin-Embedder-Policy: require-corp
@@ -1,38 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
export class AnalyticsApi extends ApiManager {
public PostAnalytics({
maybeLastAction,
maybeLogToken,
}: {
maybeLastAction: string;
maybeLogToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/analytics/log_session`;
const body = {
maybe_last_action: maybeLastAction,
maybe_log_token: maybeLogToken,
};
return this.post<
{
maybe_last_action: string;
maybe_log_token: string;
},
{
success?: boolean;
log_token?: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.log_token,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,204 +0,0 @@
import environmentVariables from "../EnvironmentVariables";
type NonNullableObject<T extends object> = NonNullable<T>;
export interface ApiResponse<T, P = undefined> {
success: boolean;
errorMessage?: string;
data?: T;
pagination?: P;
}
export class ApiManager {
ApiTargets: Record<string, string> = {};
constructor() {
// look at the .env file
this.ApiTargets = {
BaseApi: environmentVariables.values.BASE_API as string,
GoggleApi: environmentVariables.values.GOOGLE_API as string,
FunnelApi: environmentVariables.values.FUNNEL_API as string,
CdnApi: environmentVariables.values.CDN_API as string,
GravatarApi: environmentVariables.values.GRAVATAR_API as string,
uploadApi: environmentVariables.values.UPLOAD_API_VIDEO as string,
};
}
public async fetch<B, T>(
endpoint: string,
{
method,
query,
body,
}: {
method: string;
query?: Record<string, string | boolean | number | undefined>;
body?: B;
},
): Promise<T> {
const queryInString =
query &&
Object.entries(query).reduce(
(allOptions, [key, value]) => {
if (!value) {
return allOptions;
}
allOptions[key] = value.toString();
return allOptions;
},
{} as Record<string, string>,
);
const endpointWithQueries = queryInString
? endpoint + "?" + new URLSearchParams(queryInString)
: endpoint;
const bodyInString = JSON.stringify(body);
const response = await fetch(endpointWithQueries, {
method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
credentials: "include",
body: bodyInString,
});
return response.json();
}
public async fetchMultipartFormData<T>(
endpoint: string,
{
method,
body,
}: {
method: string;
body: FormData;
},
): Promise<T> {
const response = await fetch(endpoint, {
method,
headers: {
Accept: "application/json",
},
credentials: "include",
body: body,
});
return response.json();
}
protected get<T>({
endpoint,
query,
}: {
endpoint: string;
query?: Record<string, string | boolean | number | undefined>;
}): Promise<T> {
return this.fetch<null, T>(endpoint, { method: "GET", query });
}
protected post<B, T>({
endpoint,
query,
body,
}: {
endpoint: string;
query?: Record<string, string | boolean | number | undefined>;
body?: B;
}): Promise<T> {
return this.fetch<B, T>(endpoint, {
method: "POST",
query,
body,
});
}
protected delete<B, T>({
endpoint,
query,
body,
}: {
endpoint: string;
query?: Record<string, string | boolean | number | undefined>;
body?: B;
}): Promise<T> {
return this.fetch<B, T>(endpoint, {
method: "DELETE",
query,
body,
});
}
protected async postForm<T>({
endpoint,
formRecord,
uuid,
blob,
blobFileName,
}: {
endpoint: string;
formRecord: Record<string, string>;
uuid: string;
blob?: Blob | File;
blobFileName?: string;
}): Promise<T> {
const formData = new FormData();
formData.append("uuid_idempotency_token", uuid);
Object.entries(formRecord).forEach(([key, value]) => {
formData.append(key, value);
});
if (blob && blobFileName) {
formData.append("file", blob, blobFileName);
} else if (blob) {
formData.append("file", blob);
}
console.log(`HELLO${formData}`);
return this.fetchMultipartFormData<T>(endpoint, {
method: "POST",
headers: {
Accept: "application/json",
},
credentials: "include",
body: formData,
});
}
protected camelToSnakeCase(str: string) {
return str.replace(/([a-z0])([A-Z])/g, "$1_$2").toLowerCase();
}
protected parseQueryValues(
params: Record<string, string | string[] | boolean | number | undefined>,
): Record<string, string> {
return Object.entries(params).reduce(
(allParams, [key, value]) => {
if (!value) {
return allParams;
}
const snakeKey = this.camelToSnakeCase(key);
if (Array.isArray(value)) {
return { ...allParams, [snakeKey]: value.join(",") };
}
return { ...allParams, [snakeKey]: value.toString() };
},
{} as Record<string, string>,
);
}
protected parseBodyValues<T extends object, B extends object>(
params: NonNullableObject<T>,
): B {
return Object.entries(params).reduce((allParams, [key, value]) => {
if (!value) {
return allParams;
}
const snakeKey = this.camelToSnakeCase(key);
if (Array.isArray(value)) {
return { ...allParams, [snakeKey]: value };
}
return { ...allParams, [snakeKey]: value };
}, {} as B);
}
}
@@ -1,167 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { BetaKey } from "~/models";
import { Pagination } from "~/pages/PageEnigma/models";
export class BetaKeysApi extends ApiManager {
public CreateBetaKey({
maybeNote,
maybeReferrerUsername,
numberOfKeys,
uuidIdempotencyToken,
}: {
maybeNote?: string;
maybeReferrerUsername?: string;
numberOfKeys: number;
uuidIdempotencyToken: string;
}): Promise<ApiResponse<string[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/create`;
const body = {
maybe_note: maybeNote,
maybe_referrer_username: maybeReferrerUsername,
number_of_keys: numberOfKeys,
uuid_idempotency_token: uuidIdempotencyToken,
};
return this.post<
{
maybe_note?: string;
maybe_referrer_username?: string;
number_of_keys: number;
uuid_idempotency_token: string;
},
{
success?: boolean;
beta_keys?: string[];
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.beta_keys,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListBetaKeys({
sortAscending,
pageSize,
pageIndex,
maybeReferrerUsername,
onlyListRemaining,
}: {
sortAscending?: boolean;
pageSize?: number;
pageIndex?: number;
maybeReferrerUsername?: string;
onlyListRemaining?: boolean;
}): Promise<ApiResponse<BetaKey[], Pagination>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/list`;
const query = {
sort_ascending: sortAscending,
page_size: pageSize,
page_index: pageIndex,
maybe_referrer_username: maybeReferrerUsername,
only_list_remaining: onlyListRemaining,
};
return this.get<{
success?: boolean;
beta_keys?: BetaKey[];
pagination?: Pagination;
BadInput?: string;
}>({ endpoint, query })
.then((response) => ({
success: response.success ?? false,
data: response.beta_keys,
pagination: response.pagination,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public RedeemBetaKey({
betaKey,
}: {
betaKey: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/redeem`;
return this.post<
{
beta_key: string;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { beta_key: betaKey } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public UpdateBetaKeyIsDistributed({
betaKey,
isDistributed,
}: {
betaKey: string;
isDistributed: boolean;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${betaKey}/distributed`;
return this.post<
{
is_distributed: boolean;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { is_distributed: isDistributed } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public UpdateBetaKeyNote({
token,
note,
}: {
token: string;
note?: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${token}/note`;
return this.post<
{
note?: string;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { note } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,33 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { Subscription } from "~/models/Billing";
import { LoyaltyProgram } from "~/enums";
export class BillingApi extends ApiManager {
public async ListActiveSubscriptions(): Promise<
ApiResponse<{
active_subscriptions: Subscription[];
maybe_loyalty_program?: LoyaltyProgram;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/billing/active_subscriptions`;
return await this.get<{
success: boolean;
active_subscriptions?: Subscription[];
maybe_loyalty_program?: LoyaltyProgram;
error_message?: string;
}>({ endpoint: endpoint })
.then((response) => ({
success: response.success,
data: {
active_subscriptions: response.active_subscriptions || [],
maybe_loyalty_program: response.maybe_loyalty_program,
},
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
}
@@ -1,91 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
export class CommentsApi extends ApiManager {
public CreateComment({
commentMarkdown,
entityToken,
entityType,
uuidIdempotencyToken,
}: {
commentMarkdown: string;
entityToken: string;
entityType: string;
uuidIdempotencyToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/new`;
const body = {
comment_markdown: commentMarkdown,
entity_token: entityToken,
entity_type: entityType,
uuid_idempotency_token: uuidIdempotencyToken,
};
return this.post<
{
comment_markdown: string;
entity_token: string;
entity_type: string;
uuid_idempotency_token: string;
},
{
success?: boolean;
comment_token?: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.comment_token,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteComment({
commentToken,
}: {
commentToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/delete/${commentToken}`;
return this.post<
{ as_mod: boolean },
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { as_mod: true } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListCommentsByEntity({
entityType,
entityToken,
}: {
entityType: string;
entityToken: string;
}): Promise<ApiResponse<string[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/list/${entityType}/${entityToken}`;
return this.get<{
success: boolean;
comments: string[];
}>({ endpoint })
.then((response) => ({
success: true,
data: response.comments,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,39 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
export class EngineApi extends ApiManager {
public async ConvertTbxToGltf({
mediaFileToken,
uuidIdempotencyToken,
}: {
mediaFileToken: string;
uuidIdempotencyToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/conversion/enqueue_fbx_to_gltf`;
const body = {
media_file_token: mediaFileToken,
uuid_idempotency_token: uuidIdempotencyToken,
};
return this.post<
{ media_file_token: string; uuid_idempotency_token: string },
{ success?: boolean; inference_job_token?: string; BadInput?: string }
>({
endpoint,
body: body,
})
.then((response) => {
return {
success: response.success ?? false,
data: response.inference_job_token,
errorMessage: response.BadInput,
};
})
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
}
@@ -1,96 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
export class FeaturedItemsApi extends ApiManager {
public CreateFeaturedItem({
entityToken,
entityType,
}: {
entityToken: string;
entityType: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/create`;
const body = {
entity_token: entityToken,
entity_type: entityType,
};
return this.post<
{
entity_token: string;
entity_type: string;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteFeaturedItem({
entityToken,
entityType,
}: {
entityToken: string;
entityType: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/delete`;
const body = {
entity_token: entityToken,
entity_type: entityType,
};
return this.delete<
{
entity_token: string;
entity_type: string;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public CheckFeaturedItem({
entityType,
entityToken,
}: {
entityType: string;
entityToken: string;
}): Promise<ApiResponse<boolean>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/is_featured/${entityType}/${entityToken}`;
return this.post<
undefined,
{
success?: boolean;
is_featured?: boolean;
BadInput?: string;
}
>({ endpoint })
.then((response) => ({
success: response.success ?? false,
data: response.is_featured,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,74 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { Job, JobState } from "~/models";
export class JobsApi extends ApiManager {
public GetJobByToken({
token,
}: {
token: string;
}): Promise<ApiResponse<JobState>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${token}`;
return this.get<{
success: boolean;
state: JobState;
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.state,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListJobs(): Promise<ApiResponse<JobState[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/batch`;
return this.get<{
success: boolean;
job_states: JobState[];
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.job_states,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListRecentJobs(): Promise<ApiResponse<Job[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session`;
return this.get<{
success: boolean;
jobs: Job[];
error_reason?: string;
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.jobs,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteJobByToken(jobToken: string): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${jobToken}`;
return this.delete<
undefined,
{
success: boolean;
}
>({ endpoint })
.then((response) => ({
success: response.success,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,369 +0,0 @@
import {
MediaFile,
MediaInfo,
Pagination,
PaginationInfinite,
} from "~/pages/PageEnigma/models";
import { ApiManager, ApiResponse } from "./ApiManager";
import { authentication } from "~/signals";
import type { Property } from "csstype";
type Visibility = Property.Visibility;
import {
FilterEngineCategories,
FilterMediaClasses,
FilterMediaType,
} from "~/enums";
interface ListMediaQuery {
sort_ascending?: boolean;
page_size?: number;
cursor?: string;
cursor_is_reversed?: boolean;
filter_media_classes?: FilterMediaClasses[];
filter_media_type?: FilterMediaType[];
filter_engine_categories?: FilterEngineCategories[];
}
interface ListUserMediaQuery {
sort_ascending?: boolean;
page_size?: number;
page_index?: number;
filter_media_classes?: FilterMediaClasses[];
filter_media_type?: FilterMediaType[];
filter_engine_categories?: FilterEngineCategories[];
}
interface SearchFeaturedMediaQuery {
search_term: string;
filter_media_classes?: FilterMediaClasses[];
filter_media_type?: FilterMediaType[];
filter_engine_categories?: FilterEngineCategories[];
}
export class MediaFilesApi extends ApiManager {
public async DeleteMediaFileByToken({
mediaFileToken,
asMod = true,
setDelete = true,
}: {
mediaFileToken: string;
asMod?: boolean;
setDelete?: boolean;
}): Promise<ApiResponse<MediaFile>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/file/${mediaFileToken}`;
const body = { as_mod: asMod, set_delete: setDelete };
return await this.delete<
{
as_mod: boolean;
set_delete: boolean;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async ListMediaFilesByTokens({
mediaTokens,
}: {
mediaTokens: string[];
}): Promise<ApiResponse<MediaFile[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/batch`;
return await this.get<{
success: boolean;
media_files: MediaFile[];
error_reason?: string;
}>({ endpoint, query: { tokens: mediaTokens.join(",") } })
.then((response) => ({
success: response.success,
data: response.media_files,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public async GetMediaFileByToken({
mediaFileToken,
}: {
mediaFileToken: string;
}): Promise<ApiResponse<MediaFile>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/file/${mediaFileToken}`;
return await this.get<{
success: boolean;
media_file: MediaFile;
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.media_file,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async ListMediaFiles(
query: ListMediaQuery,
): Promise<ApiResponse<MediaInfo[], PaginationInfinite>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list`;
const queryWithStrings = {
...query,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaInfo[];
pagination?: PaginationInfinite;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: response.success,
data: response.results ?? [],
pagination: response.pagination,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async ListFeaturedMediaFiles(
query: ListMediaQuery,
): Promise<ApiResponse<MediaInfo[], PaginationInfinite>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list_featured`;
const queryWithStrings = {
...query,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaInfo[];
pagination: PaginationInfinite;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async ListUserMediaFiles(
query: ListUserMediaQuery,
): Promise<ApiResponse<MediaInfo[], Pagination>> {
const userName = authentication.userInfo.value?.username;
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list/user/${userName}`;
const queryWithStrings = {
...query,
include_user_uploads: true,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaInfo[];
pagination?: Pagination;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: response.success,
data: response.results ?? [],
pagination: response.pagination,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async SearchFeaturedMediaFiles(
query: SearchFeaturedMediaQuery,
): Promise<ApiResponse<MediaInfo[], Pagination>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/search_featured`;
const queryWithStrings = {
search_term: query.search_term,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaInfo[];
pagination: Pagination;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => ({
success: false,
errorMessage: err.message,
}));
}
public async SearchUserMediaFiles(
query: SearchFeaturedMediaQuery,
): Promise<ApiResponse<MediaInfo[], Pagination>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/search_session`;
const queryWithStrings = {
search_term: query.search_term,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaInfo[];
pagination: Pagination;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => ({
success: false,
errorMessage: err.message,
}));
}
public async RenameMediaFileByToken({
mediaToken,
name,
}: {
mediaToken: string;
name: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/rename/${mediaToken}`;
const body = { name };
return await this.post<
{ name: string },
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public async UpdateCoverImage({
mediaFileToken,
imageToken,
}: {
mediaFileToken: string;
imageToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/cover_image/${mediaFileToken}`;
return await this.post<
{ cover_image_media_file_token: string },
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { cover_image_media_file_token: imageToken } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async UpdateVisibility({
mediaFileToken,
visibility,
}: {
mediaFileToken: string;
visibility: Visibility;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/visibility/${mediaFileToken}`;
return await this.post<
{ creator_set_visibility: string },
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { creator_set_visibility: visibility } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
}
@@ -1,336 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { FilterEngineCategories, Visibility } from "~/enums";
export class MediaUploadApi extends ApiManager {
private sessionToken: string = undefined;
private async getSessionTokenForUploadStudioShot() {
const endpoint = `${this.ApiTargets.BaseApi}/v1/session_token`;
return await this.get<{
maybe_signed_session: string;
}>({ endpoint })
.then((response) => {
return {
success: true,
data: response["maybe_signed_session"],
};
})
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async UploadStudioShot({
maybe_title,
uuid_idempotency_token,
blob,
fileName,
maybe_visibility,
}: {
maybe_title?: string;
uuid_idempotency_token: string;
blob: Blob | null;
fileName: string;
maybe_visibility?: Visibility;
}): Promise<ApiResponse<string>> {
// This function is a bit special it requires that
const endpoint = `${this.ApiTargets.uploadApi}/v1/media_files/upload/studio_shot`;
if (!this.sessionToken) {
console.log("Session Token");
const response = await this.getSessionTokenForUploadStudioShot();
if (response.success && response.data) {
this.sessionToken = response.data;
console.log(`Token ${this.sessionToken}`);
} else {
return {
success: false,
errorMessage: "Could not obtain session tokens.",
};
}
} else {
console.log(`Session Token: ${this.sessionToken}`);
}
const options: Record<string, string | number | undefined> = {
maybe_title,
maybe_frame_rate_fps: 60,
maybe_visibility: maybe_visibility?.toString(),
};
const formRecord = Object.entries(options).reduce(
(allOptions, [key, value]) => {
if (value === undefined) {
return allOptions;
}
return { ...allOptions, [key]: value.toString() };
},
{} as Record<string, string>,
);
const formData = new FormData();
formData.append("uuid_idempotency_token", uuid_idempotency_token);
Object.entries(formRecord).forEach(([key, value]) => {
formData.append(key, value);
});
if (blob && fileName) {
formData.append("file", blob, fileName);
} else if (blob) {
formData.append("file", blob);
}
const response = await fetch(endpoint, {
method: "POST",
headers: {
Accept: "application/json",
Session: this.sessionToken,
},
credentials: "include",
body: formData,
});
const result = await response.json();
if (result.success && result.media_file_token) {
return { success: true, data: result.media_file_token };
} else {
return { success: false, errorMessage: "Failed to Get Media Token" };
}
}
private async Upload({
endpoint,
uuid,
blob,
fileName,
options,
}: {
endpoint: string;
blob: Blob | File;
fileName: string;
uuid: string;
options: Record<string, string | number | undefined>;
}): Promise<ApiResponse<string>> {
const formRecord = Object.entries(options).reduce(
(allOptions, [key, value]) => {
if (value === undefined) {
return allOptions;
}
return { ...allOptions, [key]: value.toString() };
},
{} as Record<string, string>,
);
return await this.postForm<{
success: boolean;
media_file_token?: string;
BadInput?: string;
}>({ endpoint, formRecord, blob, blobFileName: fileName, uuid })
.then((response) => ({
success: Boolean(response.success ?? false),
data: response.media_file_token,
errorMessage: response.BadInput,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async UploadAudio({
blob,
fileName,
uuid,
maybe_title,
maybe_visibility = Visibility.Public,
}: {
blob: Blob;
fileName: string;
uuid: string;
maybe_title?: string | undefined;
maybe_visibility?: Visibility | undefined;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/audio`;
const options: Record<string, string | number | undefined> = {
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
public async UploadImage({
blob,
fileName,
uuid,
maybe_title,
maybe_visibility = Visibility.Public,
}: {
blob: Blob;
fileName: string;
uuid: string;
maybe_title?: string | undefined;
maybe_visibility?: Visibility | undefined;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/image`;
const options: Record<string, string | number | undefined> = {
is_intermediate_system_file: "true",
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
public async UploadNewEngineAsset({
file,
fileName,
uuid,
engine_category,
maybe_animation_type,
maybe_duration_millis,
maybe_title,
maybe_visibility = Visibility.Public,
}: {
file: File;
fileName: string;
uuid: string;
engine_category: FilterEngineCategories;
maybe_animation_type?: string;
maybe_duration_millis?: number;
maybe_title?: string;
maybe_visibility?: Visibility;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_engine_asset`;
const options: Record<string, string | number | undefined> = {
engine_category,
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
maybe_animation_type,
maybe_duration_millis,
};
return this.Upload({ endpoint, blob: file, fileName, uuid, options });
}
public async UploadNewScene({
blob,
fileName,
uuid,
maybe_title,
maybe_visibility = Visibility.Public,
}: {
blob: Blob;
fileName: string;
uuid: string;
maybe_title?: string;
maybe_visibility?: Visibility;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_scene`;
const options: Record<string, string | number | undefined> = {
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
public async UploadNewVideo({
blob,
fileName,
uuid,
maybe_title,
maybe_visibility = Visibility.Public,
maybe_style_name,
maybe_scene_source_media_file_token,
}: {
blob: Blob;
fileName: string;
uuid: string;
maybe_title?: string;
maybe_visibility?: Visibility;
maybe_style_name?: string;
maybe_scene_source_media_file_token?: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_video`;
const options: Record<string, string | number | undefined> = {
is_intermediate_system_file: "true",
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
maybe_style_name,
maybe_scene_source_media_file_token,
};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
public async UploadPmx({
file,
fileName,
uuid,
engine_category,
maybe_animation_type,
maybe_duration_millis,
maybe_title,
maybe_visibility = Visibility.Public,
}: {
file: File;
fileName: string;
uuid: string;
engine_category?: string;
maybe_animation_type?: string;
maybe_duration_millis?: number;
maybe_title?: string;
maybe_visibility?: Visibility;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/pmx`;
const options: Record<string, string | number | undefined> = {
is_intermediate_system_file: "true",
engine_category,
maybe_animation_type,
maybe_duration_millis,
maybe_title,
maybe_visibility,
};
return this.Upload({ endpoint, blob: file, fileName, uuid, options });
}
public async UploadSavedScene({
blob,
fileName,
uuid,
mediaToken,
}: {
blob: Blob;
fileName: string;
uuid: string;
mediaToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/saved_scene/${mediaToken}`;
const options: Record<string, string | number | undefined> = {};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
// this is used to send the scene snapshot to the vst upload endpoint this creates a snapshot of the scene where in we can remix from the og.
public async UploadSceneSnapshotMediaFileForm({
blob,
uuid,
maybe_title, // title of the scene at the time
maybe_scene_source_media_file_token, // original token that started it all
}: {
blob: Blob;
uuid: string;
maybe_title: string;
maybe_scene_source_media_file_token: string | undefined;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/scene_snapshot`;
const options: Record<string, string | number | undefined> = {
maybe_title: maybe_title,
maybe_scene_source_media_file_token: maybe_scene_source_media_file_token,
};
const fileName = `${maybe_title}-${uuid}`;
return this.Upload({ endpoint, blob, fileName, uuid, options }); // token comes back and send it VST
}
}
@@ -1,26 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { StatusAlert } from "~/models/StatusAlert";
export class MiscApi extends ApiManager {
public GetStatusAlertCheck(): Promise<ApiResponse<StatusAlert>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/session`;
return this.get<{
success: boolean;
maybe_alert?: Omit<StatusAlert, "refresh_interval_millis">;
refresh_interval_millis: number;
}>({ endpoint: endpoint })
.then((response) => ({
success: response.success,
data: {
...response.maybe_alert,
refresh_interval_millis: response.refresh_interval_millis,
},
}))
.catch((err) => {
return {
success: false,
errorMessage: err.mesasge,
};
});
}
}
@@ -1,42 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
export class ModerationApi extends ApiManager {
public async UpdateFeatureFlagsByToken({
usernameOrToken,
flags,
}: {
usernameOrToken: string;
flags: string[];
}): Promise<
ApiResponse<{
inference_job_token?: string;
inference_job_token_type?: string;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/moderation/user_feature_flags/${usernameOrToken}`;
const body = {
action: {
AddFlags: {
flags,
},
},
};
return this.post<
{ action: { AddFlags: { flags: string[] } } },
{ success?: boolean; BadInput?: string }
>({
endpoint,
body: body,
})
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => ({
success: false,
errorMessage: err.message,
}));
}
}
@@ -1,25 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { Prompts } from "~/pages/PageEnigma/models";
export class PromptsApi extends ApiManager {
public GetPromptsByToken({
token,
}: {
token: string;
}): Promise<ApiResponse<Prompts>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/prompts/${token}`;
return this.get<{
success: boolean;
prompt: Prompts;
error_reason?: string;
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.prompt,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,64 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { GenerateTtsAudioErrorType } from "~/pages/PageEnigma/enums";
import { Visibility } from "~/enums";
export interface GenerateTtsAudioRequest {
creator_set_visibility?: Visibility;
uuid_idempotency_token: string;
tts_model_token: string;
inference_text: string;
is_storyteller_demo?: boolean;
}
export interface GenerateTtsAudioResponse {
success?: boolean;
inference_job_token?: string;
inference_job_token_type?: string;
BadInput?: string;
}
export class TtsApi extends ApiManager {
public async GenerateTtsAudio(request: GenerateTtsAudioRequest): Promise<
ApiResponse<{
inference_job_token?: string;
inference_job_token_type?: string;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/tts/inference`;
const body = {
...request,
...(request.creator_set_visibility
? {}
: { creator_set_visibility: Visibility.Public }),
};
return this.post<GenerateTtsAudioRequest, GenerateTtsAudioResponse>({
endpoint,
body: body,
})
.then((response) => {
if (!response.success) {
return {
success: false,
errorMessage: response.BadInput,
data: {},
};
}
return {
success: true,
data: {
inference_job_token: response.inference_job_token,
inference_job_token_type: response.inference_job_token_type,
},
};
})
.catch(() => {
return {
success: false,
errorMessage: GenerateTtsAudioErrorType.UnknownError,
data: {},
};
});
}
}
@@ -1,202 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import {
Pagination,
UserBookmarkBatch,
UserBookmarkByEntity,
UserBookmarkByUser,
} from "~/pages/PageEnigma/models";
import { authentication } from "~/signals";
export enum ScopedEntityTypes {
USER = "user",
TTS_MODEL = "tts-model",
TTS_RESULT = "tts-result",
W2L_TEMPLATE = "w2l-template",
W2L_RESULT = "w2l-result",
MEDIA_FILE = "media_file",
MODEL_WEIGHT = "model_weight",
VOICE_CONVERSION_MODEL = "voice_conversion_model",
ZS_VOICE = "z_voice",
}
export enum ScopedWeightTypes {
HIFIGAN_TTl2 = "hifigan_tt2",
RVC_V2 = "rvc_v2",
SD_1_5 = "sd_1.5",
SDXL = "sdxl",
SO_VITS_SVC = "so_vits_svc",
TT2 = "tt2",
LORA = "loRA",
VALL_E = "vall_e",
COMFY_UI = "comfy_ui",
}
export enum ScopedMediaFileType {
IMAGE_GENERATION = "image_generation",
TEXT_TO_SPEECH = "text_to_speech",
VOCODER = "vocoder",
VOICE_CONVERSION = "voice_conversion",
WORKFLOW_CONFIG = "workflow_config",
}
interface ListUserBookmarksByUserRequest {
username?: string;
sort_ascending?: boolean;
page_size?: number;
page_index?: number;
maybe_scoped_entity_type?: ScopedEntityTypes[];
maybe_scoped_weight_type?: ScopedWeightTypes[];
maybe_scoped_media_file_type?: ScopedMediaFileType[];
}
export class UserBookmarksApi extends ApiManager {
public CreateUserBookmark({
entityToken,
entityType,
}: {
entityToken: string;
entityType: string;
}): Promise<
ApiResponse<{
new_bookmark_count_for_entity?: number;
user_bookmark_token?: string;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/create`;
const body = {
entity_token: entityToken,
entity_type: entityType,
};
return this.post<
{
entity_token: string;
entity_type: string;
},
{
success?: boolean;
new_bookmark_count_for_entity?: number;
user_bookmark_token: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: {
new_bookmark_count_for_entity: response.new_bookmark_count_for_entity,
user_bookmark_token: response.user_bookmark_token,
},
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteUserBookmark({
entityToken,
}: {
entityToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/delete/${entityToken}`;
return this.delete<
{ as_mod: boolean },
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { as_mod: true } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListUserBookmarks(): Promise<ApiResponse<UserBookmarkBatch[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/batch`;
return this.get<{
success?: boolean;
bookmarks?: UserBookmarkBatch[];
BadInput?: string;
}>({ endpoint })
.then((response) => ({
success: response.success ?? false,
data: response.bookmarks,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListUserBookmarksByUser({
username,
sort_ascending,
page_size,
page_index,
maybe_scoped_entity_type,
maybe_scoped_weight_type,
maybe_scoped_media_file_type,
}: ListUserBookmarksByUserRequest): Promise<
ApiResponse<UserBookmarkByUser[], Pagination>
> {
const user = username ?? authentication.userInfo.value?.username;
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${user}`;
const query = {
sort_ascending,
page_size,
page_index,
maybe_scoped_entity_type: maybe_scoped_entity_type
? maybe_scoped_entity_type.join(",")
: undefined,
maybe_scoped_weight_type: maybe_scoped_weight_type
? maybe_scoped_weight_type.join(",")
: undefined,
maybe_scoped_media_file_type: maybe_scoped_media_file_type
? maybe_scoped_media_file_type.join(",")
: undefined,
};
return this.get<{
success?: boolean;
pagination?: Pagination;
results: UserBookmarkByUser[];
}>({ endpoint, query })
.then((response) => ({
success: response.success ?? false,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListUserBookmarksByEntity({
entityType,
entityToken,
}: {
entityType: string;
entityToken: string;
}): Promise<ApiResponse<UserBookmarkByEntity[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${entityType}/${entityToken}`;
return this.get<{
success: boolean;
user_bookmarks: UserBookmarkByEntity[];
}>({ endpoint })
.then((response) => ({
success: true,
data: response.user_bookmarks,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,92 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
export interface UserRating {
entity_token: string;
entity_type: string;
rating_value: string;
}
export class UserRatingApi extends ApiManager {
public ListUserRatings(): Promise<ApiResponse<UserRating[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/batch`;
return this.get<{
success?: boolean;
ratings?: UserRating[];
BadInput?: string;
}>({ endpoint })
.then((response) => ({
success: response.success ?? false,
data: response.ratings,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public PostUserRating({
entityToken,
entityType,
ratingValue,
}: {
entityToken: string;
entityType: string;
ratingValue: string;
}): Promise<ApiResponse<number>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/rate`;
const body = {
entity_token: entityToken,
entity_type: entityType,
rating_value: ratingValue,
};
return this.post<
{
entity_token: string;
entity_type: string;
rating_value: string;
},
{
success?: boolean;
new_positive_rating_count_for_entity?: number;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.new_positive_rating_count_for_entity,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListUserRatingByEntity({
entityType,
entityToken,
}: {
entityType: string;
entityToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/view/${entityType}/${entityToken}`;
return this.get<{
success?: boolean;
maybe_rating_value?: string;
BadInput?: string;
}>({ endpoint })
.then((response) => {
return {
success: response.success ?? false,
data: response.maybe_rating_value,
errorMessage: response.BadInput,
};
})
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,161 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { UserInfo } from "~/models";
export class UsersApi extends ApiManager {
public GetSession(): Promise<
ApiResponse<{
loggedIn: boolean;
user?: UserInfo;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/session`;
return this.get<{
success: boolean;
logged_in: boolean;
user?: UserInfo;
error_message?: string;
}>({ endpoint: endpoint })
.then((response) => ({
success: response.success,
data: {
loggedIn: response.logged_in,
user: response.user,
},
}))
.catch((err) => {
return {
success: false,
errorMessage: err.mesasge,
};
});
}
public GetUserProfile(username: string): Promise<
ApiResponse<{
user?: UserInfo;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user/${username}/profile`;
return this.get<{
success: boolean;
user?: UserInfo;
error_message?: string;
}>({ endpoint: endpoint })
.then((response) => ({
success: response.success,
data: {
user: response.user,
},
}))
.catch((err) => {
return {
success: false,
error_message: err.message,
};
});
}
public async Login({
usernameOrEmail,
password,
}: {
usernameOrEmail: string;
password: string;
}): Promise<ApiResponse<{ signedSession?: string }>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/login`;
const body = {
username_or_email: usernameOrEmail,
password: password,
};
return await this.post<
{ username_or_email: string; password: string },
{
success: boolean;
signed_session?: string;
error_message?: string;
error_type?: string;
}
>({
endpoint: endpoint,
body: body,
})
.then((response) => ({
success: response.success,
data: { signedSession: response.signed_session },
errorMessage: response.error_message,
}))
.catch((err) => {
return {
success: false,
error_message: err.message,
};
});
}
public Logout(): Promise<ApiResponse<null>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/logout`;
return this.post<null, { success: boolean; error_message?: string }>({
endpoint: endpoint,
})
.then((response) => ({
success: response.success,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async Signup({
username,
email,
password,
passwordConfirmation,
}: {
username: string;
email: string;
password: string;
passwordConfirmation: string;
}): Promise<ApiResponse<{ signedSession?: string }>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/create_account`;
const body = {
email_address: email,
password,
password_confirmation: passwordConfirmation,
username,
};
return await this.post<
{
username: string;
email_address: string;
password: string;
password_confirmation: string;
},
{
success: boolean;
signed_session?: string;
error_fields?: Record<string, string>;
error_message?: string;
error_type?: string;
}
>({
endpoint: endpoint,
body: body,
})
.then((response) => {
return {
success: response.success,
data: { signedSession: response.signed_session },
errorMessage: Object.values(response.error_fields ?? {}).join(","),
};
})
.catch((err) => {
return {
success: false,
error_message: err.message,
};
});
}
}
@@ -1,70 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { Visibility } from "~/enums";
interface GenerateVideoStyleTransferRequest {
creator_set_visibility: string;
disable_lcm: boolean;
enable_lipsync: boolean;
frame_skip: number;
global_ipa_media_token: string;
input_depth_file: string;
input_file: string;
input_normal_file: string;
input_outline_file: string;
negative_prompt: string;
prompt: string;
remove_watermark: boolean;
style: string;
travel_prompt: string;
trim_end_millis: number;
trim_start_millis: number;
use_cinematic: boolean;
use_face_detailer: boolean;
use_strength: number;
use_upscaler: boolean;
uuid_idempotency_token: string;
}
export class VideoApi extends ApiManager {
public async EnqueueStudio({
enqueueVideo,
}: {
enqueueVideo: GenerateVideoStyleTransferRequest;
}): Promise<
ApiResponse<{
inference_job_token?: string;
inference_job_token_type?: string;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/workflows/enqueue_studio`;
const body = {
...enqueueVideo,
...(enqueueVideo.creator_set_visibility
? {}
: { creator_set_visibility: Visibility.Public }),
};
return await this.post<
GenerateVideoStyleTransferRequest,
{
success?: boolean;
inference_job_token?: string;
inference_job_token_type?: string;
BadInput?: string;
}
>({ endpoint, body: body })
.then((response) => ({
success: Boolean(response.success ?? false),
data: {
inference_job_token: response.inference_job_token,
inference_job_token_type: response.inference_job_token_type,
},
errorMessage: response.BadInput,
}))
.catch((err) => {
console.log(err.message);
return { success: false, error_reason: err.message };
});
}
}
@@ -1,53 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
interface VoiceConversionParams {
autoPredictF0?: boolean;
creatorSetVisibility?: string;
isStorytellerDemo?: boolean;
overrideF0Method?: string;
sourceMediaUploadToken: string;
transpose?: number;
uuidIdempotencyToken: string;
voiceConversionModelToken: string;
}
interface VoiceConversionRequest {
auto_predict_f0: boolean;
creator_set_visibility: string;
is_storyteller_demo: boolean;
override_f0_method: string;
source_media_upload_token: string;
transpose: number;
uuid_idempotency_token: string;
voice_conversion_model_token: string;
}
export class VoiceConversionApi extends ApiManager {
public ConvertVoice(
params: VoiceConversionParams,
): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_conversion/inference`;
const body = this.parseBodyValues<
VoiceConversionParams,
VoiceConversionRequest
>(params);
return this.post<
VoiceConversionRequest,
{
success?: boolean;
inference_job_token?: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.inference_job_token,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,78 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { Pagination } from "~/pages/PageEnigma/models";
import { authentication } from "~/signals";
import { ZsDataset } from "~/models/Dataset";
interface VoiceDesignerParams {
text: string;
uuidIdempotencyToken: string;
voiceToken: string;
}
interface VoiceDesignerRequest {
text: string;
uuid_idempotency_token: string;
voice_token: string;
}
export class VoiceDesignerApi extends ApiManager {
public EnqueueTts(
params: VoiceDesignerParams,
): Promise<
ApiResponse<{ inference_job_token?: string; voice_token?: string }>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_designer/enqueue_tts`;
const body = this.parseBodyValues<
VoiceDesignerParams,
VoiceDesignerRequest
>(params);
return this.post<
VoiceDesignerRequest,
{
success?: boolean;
inference_job_token?: string;
voice_token: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
...(response.success
? {
data: {
inference_job_token: response.inference_job_token,
voice_token: response.voice_token,
},
}
: {}),
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListDatasetsByUser({
username,
}: {
username?: string;
}): Promise<ApiResponse<ZsDataset[]>> {
const user = username ?? authentication.userInfo.value?.username;
const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_designer/user/${user}/list`;
return this.get<{
success: boolean;
datasets: ZsDataset[];
pagination: Pagination;
}>({ endpoint })
.then((response) => ({
success: true,
data: response.datasets,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,296 +0,0 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { authentication } from "~/signals";
import { Weight } from "~/models";
import { Pagination, PaginationInfinite } from "~/pages/PageEnigma/models";
import {
FilterEngineCategories,
FilterMediaClasses,
FilterMediaType,
Visibility,
} from "~/enums";
export enum ScopedWeightType {
HIFIGAN_TT2 = "hifigan_tt2",
RVC_V2 = "rvc_v2",
SD_1_5 = "sd_1.5",
SDXL = "sdxl",
SO_VITS_SVC = "so_vits_svc",
TT2 = "tt2",
LORA = "loRA",
VALL_E = "vall_e",
COMFY_UI = "comfy_ui",
}
export enum ScopedWeightCategory {
IMAGE_GENERATION = "image_generation",
TEXT_TO_SPEECH = "text_to_speech",
VOCODER = "vocoder",
VOICE_CONVERSION = "voice_conversion",
WORKFLOW_CONFIG = "workflow_config",
}
interface WeightsRequest {
username?: string;
pageSize?: number;
weightType?: ScopedWeightType[];
weightCategory?: ScopedWeightCategory[];
//TODO: what is scoped??
scopedWeightType?: ScopedWeightType[];
scopedWeightCategory?: ScopedWeightCategory[];
}
export interface ListWeightsByUserRequest extends WeightsRequest {
pageIndex?: number;
sortAscending?: boolean;
}
export interface ListWeightsRequest extends WeightsRequest {
cursor?: string;
cursorIsReversed?: boolean;
}
export interface ListFeaturedWeightsRequest {
sortAscending?: boolean;
pageSize?: number;
cursor?: string;
cursorIsReversed?: boolean;
filterMediaClasses?: FilterMediaClasses[];
filterMediaType?: FilterMediaType[];
filterEngineCategories?: FilterEngineCategories[];
}
export interface SearchWeightParams {
ietfLanguageSubtag: string;
searchTerm: string;
weightCategory: ScopedWeightCategory;
weightType: ScopedWeightType;
}
export interface SearchWeightRequest {
ietf_language_subtag: string;
search_term: string;
weight_category: string;
weight_type: string;
}
interface GetWeightByTokenResponse extends Weight {
success: boolean;
}
export interface UpdateWeightByTokenParams {
weightToken: string;
coverImageMediaFileToken: string;
descriptionMarkdown: string;
title: string;
visibility: Visibility;
}
export interface UpdateWeightByTokenRequest {
cover_image_media_file_token: string;
description_markdown: string;
title: string;
visibility: Visibility;
}
export class WeightsApi extends ApiManager {
public ListWeightsByUser({
username,
...params
}: ListWeightsByUserRequest): Promise<ApiResponse<Weight[], Pagination>> {
const user = username ?? authentication.userInfo.value?.username;
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/by_user/${user}`;
const query = this.parseQueryValues(params);
return this.get<{
success: boolean;
results: Weight[];
pagination: Pagination;
}>({ endpoint, query })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListWeights({
...params
}: ListWeightsRequest): Promise<ApiResponse<Weight[], PaginationInfinite>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list`;
const query = this.parseQueryValues(params);
return this.get<{
success: boolean;
results: Weight[];
pagination: PaginationInfinite;
}>({ endpoint, query })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListWeightsFeatured({
...params
}: ListFeaturedWeightsRequest): Promise<
ApiResponse<Weight[], PaginationInfinite>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list_featured`;
const query = this.parseQueryValues(params);
return this.get<{
success: boolean;
results: Weight[];
pagination: PaginationInfinite;
}>({ endpoint, query })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListWeightsPinned(): Promise<ApiResponse<Weight[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list_pinned`;
return this.get<{
success: boolean;
results: Weight[];
}>({ endpoint })
.then((response) => ({
success: true,
data: response.results,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public SearchWeights(
params: SearchWeightParams,
): Promise<ApiResponse<Weight[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/search`;
const body = this.parseBodyValues<SearchWeightParams, SearchWeightRequest>(
params,
);
return this.post<
SearchWeightRequest,
{
success: boolean;
weights: Weight[];
}
>({ endpoint, body })
.then((response) => ({
success: true,
data: response.weights,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public GetWeightByToken({
weightToken,
}: {
weightToken: string;
}): Promise<ApiResponse<Weight>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`;
return this.get<GetWeightByTokenResponse>({ endpoint })
.then(({ success, ...mediaFile }) => ({
success: success,
data: mediaFile,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public UpdateWeightByToken({
weightToken,
...params
}: UpdateWeightByTokenParams): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`;
const body = this.parseBodyValues<
Omit<UpdateWeightByTokenParams, "weightToken">,
UpdateWeightByTokenRequest
>(params);
return this.post<
UpdateWeightByTokenRequest,
{ success?: boolean; BadInput?: string }
>({ endpoint, body })
.then(({ success, BadInput }) => ({
success: success ?? false,
errorMessage: BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteWeightByToken({
weightToken,
}: {
weightToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`;
const body = {
as_mod: true,
set_delete: true,
};
return this.delete<
{ as_mod: boolean; set_delete: boolean },
{ success?: boolean; BadInput?: string }
>({ endpoint, body })
.then(({ success, BadInput }) => ({
success: success ?? false,
errorMessage: BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public UpdateWeightCoverImageByToken({
weightToken,
coverImageMediaFileToken,
}: {
weightToken: string;
coverImageMediaFileToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/${weightToken}/cover_image`;
const body = {
cover_image_media_file_token: coverImageMediaFileToken,
};
return this.post<
{ cover_image_media_file_token: string },
{ success?: boolean; BadInput?: string }
>({ endpoint, body })
.then(({ success, BadInput }) => ({
success: success ?? false,
errorMessage: BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -1,18 +0,0 @@
// https://storyteller-docs.netlify.app/#/
export * from "./AnalyticsApi";
export * from "./BetaKeysApi";
export * from "./BillingApi";
export * from "./CommentsApi";
export * from "./EngineApi";
export * from "./FeaturedItemsApi";
export * from "./JobsApi";
export * from "./MediaFilesApi";
export * from "./MediaUploadApi";
export * from "./MiscApi";
export * from "./ModerationApi";
export * from "./PromptsApi";
export * from "./TtsApi";
export * from "./UserBookmarksApi";
export * from "./UsersApi";
export * from "./VideoApi";
export * from "./WeightsApi";
@@ -1,10 +0,0 @@
class EnvironmentVariables {
values: Record<string, string | boolean> = {};
initialize(values: Record<string, string | boolean>) {
this.values = values;
}
}
const environmentVariables = new EnvironmentVariables();
export default environmentVariables;
@@ -1,57 +0,0 @@
import environmentVariables from "~/Classes/EnvironmentVariables";
class BucketConfig {
isLocalDev: boolean;
constructor() {
this.isLocalDev =
document.location.host.includes("localhost") ||
document.location.host.includes("jungle.horse") ||
document.location.host.startsWith("dev.");
}
// TODO: Prevent callers with undefined/null paths
// TODO pipeline works make more robust....
getGcsUrl(bucketRelativePath: string | undefined | null): string {
const bucket = this.getBucket();
let path = bucketRelativePath;
if (path !== undefined && path !== null && !path.startsWith("/")) {
path = "/" + path;
}
const media_api_base_url = environmentVariables.values.GOOGLE_API;
return `${media_api_base_url}/${bucket}${path}`;
}
private getBucket(): string {
return this.isLocalDev ? "dev-vocodes-public" : "vocodes-public";
}
getCdnUrl(
bucketRelativePath: string,
width?: number,
quality?: number,
): string {
const cndBasePath = environmentVariables.values.CDN_API;
const path = bucketRelativePath?.startsWith("/")
? bucketRelativePath
: "/" + bucketRelativePath;
let resizeParams = "";
if (width || quality) {
resizeParams = "cdn-cgi/image/";
if (width) {
resizeParams += `width=${width},`;
}
if (quality) {
resizeParams += `quality=${quality},`;
}
resizeParams = resizeParams.slice(0, -1);
}
if (resizeParams) {
return `${cndBasePath}/${resizeParams}${path}`;
} else {
return `${cndBasePath}${path}`;
}
}
}
export { BucketConfig };
@@ -1,10 +0,0 @@
export const apiHost = "https://api.storyteller.ai";
// Upload File
export const uploadMedia = `${apiHost}/v1/media_files/upload`;
// Scenes
export const uploadNewScene = `${apiHost}/v1/media_files/upload/new_scene`;
export const uploadThumbnail = `${apiHost}/v1/media_files/cover_image/`;
export const updateExistingScene = (sceneToken: string) =>
`${apiHost}/v1/media_files/upload/saved_scene/${sceneToken}`;
@@ -1,2 +0,0 @@
export * from "./reusable";
export * from "./signaled";
@@ -1,20 +0,0 @@
import { twMerge } from "tailwind-merge";
interface BadgeProps {
label: string;
color?: string;
className?: string;
}
export const Badge = ({ label, className }: BadgeProps) => {
return (
<div
className={twMerge(
"rounded-[6px] bg-ui-controls/45 px-[4px] py-[1px] text-[10px] shadow-sm",
className,
)}
>
{label}
</div>
);
};
@@ -1 +0,0 @@
export { Badge } from "./Badge";
@@ -1,20 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';
<Meta of={ButtonStories} />
# Button
A button denotes a possible action to be taken on behalf of the user.
There are three types of Button: primary, secondary, action.
<Canvas of={ButtonStories.Primary} />
<Canvas of={ButtonStories.Secondary} />
<Canvas of={ButtonStories.Action} />
It can be shown with an optional icon. If "iconFlip" is true, the icon shows after the "children", if false it shows before.
<Canvas of={ButtonStories.WithIconFlip} />
<Canvas of={ButtonStories.WithIconNoFlip} />
@@ -1,46 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
import { withActions } from "@storybook/addon-actions/decorator";
import { faAngleLeft } from "@fortawesome/pro-solid-svg-icons";
const meta: Meta<typeof Button> = {
component: Button,
parameters: {
actions: {
handles: ["click"],
},
},
decorators: [withActions],
};
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
render: () => <Button variant="primary">Primary</Button>,
};
export const Secondary: Story = {
render: () => <Button variant="secondary">Secondary</Button>,
};
export const Action: Story = {
render: () => <Button variant="action">Action</Button>,
};
export const WithIconFlip: Story = {
render: () => (
<Button variant="secondary" iconFlip icon={faAngleLeft}>
Icon Flip
</Button>
),
};
export const WithIconNoFlip: Story = {
render: () => (
<Button variant="secondary" icon={faAngleLeft}>
Icon
</Button>
),
};
export default meta;
@@ -1,107 +0,0 @@
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { ButtonHTMLAttributes } from "react";
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
icon?: IconDefinition;
iconClassName?: string;
iconFlip?: boolean;
htmlFor?: string;
variant?: "primary" | "secondary" | "action";
loading?: boolean;
}
export const Button = ({
icon,
iconClassName,
children,
className: propsClassName,
htmlFor,
variant: propsVariant = "primary",
disabled,
iconFlip = false,
loading,
...rest
}: ButtonProps) => {
function getVariantClassNames(variant: string) {
switch (variant) {
case "secondary": {
return "bg-brand-secondary hover:bg-brand-secondary-900 text-white focus-visible:outline-brand-secondary";
}
case "action": {
return " bg-action hover:bg-action-900 text-white focus-visible:outline-action ";
}
case "primary":
default: {
return "bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600";
}
}
}
const disabledClass = twMerge(
disabled || loading ? "opacity-50 pointer-events-none" : "",
);
const className = twMerge(
"text-sm font-medium rounded-lg px-3 py-2 border border-[#3F3F3F] shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-150 flex gap-2 items-center justify-center",
getVariantClassNames(propsVariant),
propsClassName,
disabledClass,
);
// const ButtonType = htmlFor ? "label" : "button";
if (htmlFor) {
return (
<label className={className} htmlFor={htmlFor} style={rest.style}>
{loading && !iconFlip ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<>
{icon && !iconFlip ? (
<FontAwesomeIcon icon={icon} className={iconClassName} />
) : null}
</>
)}
{children}
{loading && iconFlip ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<>
{icon && iconFlip ? (
<FontAwesomeIcon icon={icon} className={iconClassName} />
) : null}
</>
)}
</label>
);
}
return (
<button
className={className}
disabled={disabled || loading}
{...{ ...rest, htmlFor }}
>
{loading && !iconFlip ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<>
{icon && !iconFlip ? (
<FontAwesomeIcon icon={icon} className={iconClassName} />
) : null}
</>
)}
{children}
{loading && iconFlip ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<>
{icon && iconFlip ? (
<FontAwesomeIcon icon={icon} className={iconClassName} />
) : null}
</>
)}
</button>
);
};
@@ -1 +0,0 @@
export * from "./Button";
@@ -1,81 +0,0 @@
import { useState } from "react";
import { TransitionDialogue, Button, ButtonProps } from "~/components";
type UnionedButtonProps = { label?: string } & ButtonProps;
interface ButtonDialoguePropsI {
buttonProps?: UnionedButtonProps;
confirmButtonProps?: UnionedButtonProps;
closeButtonProps?: UnionedButtonProps;
dialogProps?: {
className?: string;
};
title?: React.ReactNode;
children: React.ReactNode;
showClose?: boolean;
}
export const ButtonDialogue = ({
dialogProps = {},
buttonProps: unionedButtonProps,
confirmButtonProps,
closeButtonProps: unionedCloseButtonProps,
title,
children,
showClose = true,
}: ButtonDialoguePropsI) => {
const [isOpen, setIsOpen] = useState(false);
const closeModal = () => setIsOpen(false);
const openModal = () => setIsOpen(true);
const { label: buttonLabel, ...buttonProps } = unionedButtonProps || {
label: "Open",
};
const { label: closeButtonLabel, ...closeButtonProps } =
unionedCloseButtonProps || { label: "Close" };
return (
<>
<Button type="button" onClick={openModal} {...buttonProps}>
{buttonLabel}
</Button>
<TransitionDialogue
title={title}
isOpen={isOpen}
onClose={closeModal}
className={dialogProps.className}
>
<div className="mt-2 h-full">{children}</div>
<div className="mt-6 flex justify-end gap-2">
{showClose && (
<Button
type="button"
onClick={closeModal}
{...closeButtonProps}
variant="secondary"
>
{closeButtonLabel}
</Button>
)}
{confirmButtonProps && (
<Button
type="button"
{...confirmButtonProps}
onClick={(e) => {
if (confirmButtonProps.onClick) {
confirmButtonProps.onClick(e);
}
closeModal();
}}
>
{confirmButtonProps.label ? confirmButtonProps.label : "Confirm"}
</Button>
)}
</div>
</TransitionDialogue>
</>
);
};
@@ -1 +0,0 @@
export { ButtonDialogue } from './ButtonDialogue';
@@ -1,15 +0,0 @@
import { Canvas, Meta } from "@storybook/blocks";
import * as ButtonDropdownStories from "./ButtonDropdown.stories";
<Meta of={ButtonDropdownStories} />
# Button for dropdown actions
Dropdown options that open up a dialog
<Canvas of={ButtonDropdownStories.OpenDialog} />
Dropdown options with onClick event
<Canvas of={ButtonDropdownStories.ClickEvent} />
@@ -1,77 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ButtonDropdown } from "./ButtonDropdown";
import { withActions } from "@storybook/addon-actions/decorator";
import { faAngleLeft, faFile } from "@fortawesome/pro-solid-svg-icons";
const meta: Meta<typeof ButtonDropdown> = {
component: ButtonDropdown,
parameters: {
actions: {
handles: ["click"],
},
},
decorators: [withActions],
};
type Story = StoryObj<typeof ButtonDropdown>;
export const OpenDialog: Story = {
render: () => (
<ButtonDropdown
label="Dialog Options"
icon={faFile}
options={[
{
label: "Open Dialog",
description: "Desc 1",
onDialogOpen: () => {
//console.log("Opened Dialog");
},
dialogProps: {
title: "Dialog Title",
content: <div className="text-white">Dialog content here...</div>,
confirmButtonProps: {
label: "Create",
disabled: false,
onClick: () => {
//console.log("Action on Dialog");
},
},
closeButtonProps: {
label: "Cancel",
},
showClose: true,
onClose: () => {
//console.log("Closed Dialog");
},
},
},
{
label: "Disabled",
description: "Desc 2",
disabled: true,
},
]}
/>
),
};
export const ClickEvent: Story = {
render: () => (
<ButtonDropdown
label="Event Options"
icon={faFile}
options={[
{
label: "Click Event",
description: "Desc",
onClick: () => {
//console.log("Clicked");
},
},
]}
/>
),
};
export default meta;
@@ -1,212 +0,0 @@
import { ButtonHTMLAttributes, Fragment, useState } from "react";
import { Menu, Transition } from "@headlessui/react";
import {
IconDefinition,
faChevronDown,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, ButtonProps, TransitionDialogue } from "~/components";
import { twMerge } from "tailwind-merge";
type UnionedButtonProps = { label?: string } & ButtonProps;
interface ButtonDropdownProps extends ButtonHTMLAttributes<HTMLButtonElement> {
label: string;
icon?: IconDefinition;
align?: "left" | "right";
showSelected?: boolean;
options: Array<{
label: string;
className?: string;
icon?: IconDefinition;
selected?: boolean;
description?: string;
onClick?: () => void;
disabled?: boolean;
divider?: boolean;
onDialogOpen?: () => void;
dialogProps?: {
title: string;
content: React.ReactNode;
className?: string;
confirmButtonProps?: UnionedButtonProps;
closeButtonProps?: UnionedButtonProps;
showClose?: boolean;
onClose?: () => void;
};
}>;
}
export const ButtonDropdown = ({
className,
label,
options,
icon,
align = "left",
showSelected,
}: ButtonDropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOptionIndex, setSelectedOptionIndex] = useState<number | null>(
null,
);
const closeModal = () => {
setIsOpen(false);
options[selectedOptionIndex!].dialogProps?.onClose?.();
};
const handleOptionClick = (index: number) => {
const option = options[index];
if (option.onClick) {
option.onClick();
}
if (option.onDialogOpen) {
option.onDialogOpen();
}
if (option.dialogProps) {
setSelectedOptionIndex(index);
setIsOpen(true);
}
};
const currentDialogProps =
selectedOptionIndex !== null
? options[selectedOptionIndex].dialogProps
: null;
return (
<div className="relative">
<Menu as="div" className="inline-block text-left">
<Menu.Button as="div">
<Button
className={className}
icon={faChevronDown}
iconFlip={true}
variant="secondary"
>
{icon ? <FontAwesomeIcon icon={icon} /> : null}
{label}
</Button>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
static
className={twMerge(
"absolute z-20 mt-1 w-max divide-y divide-gray-100 overflow-hidden rounded-lg bg-brand-secondary py-1.5 shadow-xl focus:outline-none",
align === "left" ? "left-0" : "right-0",
)}
>
<div>
{options.map((option, index) => (
<Fragment key={index}>
{option.divider && (
<div className="my-1.5 border-t border-ui-divider" />
)}
<Menu.Item>
{({ active }) => (
<button
disabled={option.disabled}
className={twMerge(
"duration-50 bg-brand-secondary font-medium text-white transition-all",
active ? "bg-brand-secondary-800" : "",
option.disabled
? "pointer-events-none opacity-40"
: "",
"group flex w-full items-center py-1.5 pl-7 pr-4 text-sm",
option.className,
)}
onClick={() => handleOptionClick(index)}
>
<div className="flex w-full items-center">
{option.icon && (
<FontAwesomeIcon
icon={option.icon}
className="mr-2"
/>
)}
<div className="grow text-start">{option.label}</div>
<div className="ml-10 font-normal text-white/75">
{option.description && option.description}
</div>
{showSelected && (
<>
{option.selected ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="ml-3 flex h-5"
>
<path
opacity="1"
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
fill="#FC6B68"
/>
<path
d="M369 175c-9.4 9.4-9.4 24.6 0 33.9L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0z"
fill="#FFFFFF"
/>
</svg>
) : (
<div className="w-8" />
)}
</>
)}
</div>
</button>
)}
</Menu.Item>
</Fragment>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
{currentDialogProps && (
<TransitionDialogue
title={currentDialogProps.title}
isOpen={isOpen}
onClose={closeModal}
className={currentDialogProps.className}
>
{currentDialogProps.content}
<div className="mt-6 flex justify-end gap-2">
{currentDialogProps.showClose !== false &&
currentDialogProps.closeButtonProps && (
<Button
variant="secondary"
{...currentDialogProps.closeButtonProps}
onClick={closeModal}
>
{currentDialogProps.closeButtonProps.label}
</Button>
)}
{currentDialogProps.confirmButtonProps && (
<Button
{...currentDialogProps.confirmButtonProps}
onClick={(e) => {
if (currentDialogProps.confirmButtonProps?.onClick) {
currentDialogProps.confirmButtonProps?.onClick(e);
}
closeModal();
}}
>
{currentDialogProps.confirmButtonProps.label || "Confirm"}
</Button>
)}
</div>
</TransitionDialogue>
)}
</div>
);
};
@@ -1 +0,0 @@
export * from "./ButtonDropdown";
@@ -1,13 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as ButtonIconStories from './ButtonIcon.stories';
<Meta of={ButtonIconStories} />
# ButtonIcon
A button denotes a possible action to be taken on behalf of the user.
This button shows only a single icon.
<Canvas of={ButtonIconStories.Default} />
@@ -1,22 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ButtonIcon } from "./ButtonIcon";
import { withActions } from "@storybook/addon-actions/decorator";
import { faAngleLeft } from "@fortawesome/pro-solid-svg-icons";
const meta: Meta<typeof ButtonIcon> = {
component: ButtonIcon,
parameters: {
actions: {
handles: ["click"],
},
},
decorators: [withActions],
};
type Story = StoryObj<typeof ButtonIcon>;
export const Default: Story = {
render: () => <ButtonIcon icon={faAngleLeft} onClick={() => {}} />,
};
export default meta;
@@ -1,39 +0,0 @@
import {
FontAwesomeIcon,
FontAwesomeIconProps,
} from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
interface ButtonIconProps extends FontAwesomeIconProps {
icon: IconDefinition;
onClick: () => void;
className?: string;
bgFill?: boolean;
disabled?: boolean;
}
export const ButtonIcon = ({
icon,
size,
onClick,
className: propsClassName,
bgFill = false,
disabled,
...rest
}: ButtonIconProps) => {
const className = twMerge(
"box-content flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
bgFill
? "bg-ui-controls-button hover:bg-ui-controls-button/[0.75]"
: "bg-transparent hover:bg-ui-panel/[0.4]",
disabled && "opacity-50 hover:bg-transparent",
propsClassName,
);
return (
<button className={className} onClick={onClick} disabled={disabled}>
<FontAwesomeIcon icon={icon} size={size} {...rest} />
</button>
);
};
@@ -1 +0,0 @@
export * from "./ButtonIcon";
@@ -1,13 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as ButtonIconSelectStories from './ButtonIconSelect.stories';
<Meta of={ButtonIconSelectStories} />
# ButtonIconSelectStories
A button denotes a possible action to be taken on behalf of the user.
This button shows only a single icon.
<Canvas of={ButtonIconSelectStories.Default} />
@@ -1,38 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ButtonIconSelect } from "./ButtonIconSelect";
import { withActions } from "@storybook/addon-actions/decorator";
import { faAngleLeft, faAngleRight } from "@fortawesome/pro-solid-svg-icons";
const meta: Meta<typeof ButtonIconSelect> = {
component: ButtonIconSelect,
parameters: {
actions: {
handles: ["click"],
},
},
decorators: [withActions],
};
type Story = StoryObj<typeof ButtonIconSelect>;
export const Default: Story = {
render: () => (
<ButtonIconSelect
onOptionChange={() => {}}
options={[
{
value: "V1",
icon: faAngleLeft,
text: "Select 1",
},
{
value: "V2",
icon: faAngleRight,
text: "Select 2",
},
]}
/>
),
};
export default meta;
@@ -1,54 +0,0 @@
import { useState } from "react";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
interface Option {
value: string;
icon: IconDefinition;
text?: string;
}
interface ButtonIconSelectProps {
options: Option[];
onOptionChange?: (value: string) => void;
}
export function ButtonIconSelect({
options,
onOptionChange,
}: ButtonIconSelectProps) {
const [selectedOption, setSelectedOption] = useState<string>(
options[0].value,
);
const handleOptionChange = (value: string) => {
setSelectedOption(value);
if (onOptionChange) {
onOptionChange(value);
}
};
return (
<div className="flex space-x-1">
{options.map(({ value, icon, text }) => (
<button
key={value}
className={twMerge(
`flex h-8 items-center justify-center rounded-lg border text-sm transition-all duration-150`,
text ? "h-auto w-auto gap-2 px-2.5 py-1" : "w-8",
selectedOption === value
? "border-brand-primary bg-ui-panel/[0.3]"
: "border-transparent hover:bg-ui-panel/[0.4]",
)}
onClick={() => handleOptionChange(value)}
>
<FontAwesomeIcon icon={icon} />
{text && (
<span className="text-nowrap text-sm font-medium">{text}</span>
)}
</button>
))}
</div>
);
}
@@ -1 +0,0 @@
export * from "./ButtonIconSelect";
@@ -1,20 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as ButtonLinkStories from './ButtonLink.stories';
<Meta of={ButtonLinkStories} />
# ButtonLink
A button denotes a possible action to be taken on behalf of the user.
The button label comes via "children".
There are three types of Button: primary, secondary, action.
<Canvas of={ButtonLinkStories.Primary} />
<Canvas of={ButtonLinkStories.Secondary} />
It can be shown with an optional icon.
<Canvas of={ButtonLinkStories.WithIcon} />
@@ -1,42 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ButtonLink } from "./ButtonLink";
import { withActions } from "@storybook/addon-actions/decorator";
import { faAngleLeft } from "@fortawesome/pro-solid-svg-icons";
const meta: Meta<typeof ButtonLink> = {
component: ButtonLink,
parameters: {
actions: {
handles: ["click"],
},
},
decorators: [withActions],
};
type Story = StoryObj<typeof ButtonLink>;
export const Primary: Story = {
render: () => (
<ButtonLink to="google.com" variant="primary">
Primary
</ButtonLink>
),
};
export const Secondary: Story = {
render: () => (
<ButtonLink to="google.com" variant="secondary">
Secondary
</ButtonLink>
),
};
export const WithIcon: Story = {
render: () => (
<ButtonLink to="google.com" variant="secondary" icon={faAngleLeft}>
Icon
</ButtonLink>
),
};
export default meta;
@@ -1,44 +0,0 @@
import { Link, LinkProps } from "@remix-run/react";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
interface LinkButtonProps extends LinkProps {
icon?: IconDefinition;
variant?: "primary" | "secondary";
}
export const ButtonLink = ({
icon,
children,
className: propsClassName,
variant = "primary",
...rest
}: LinkButtonProps) => {
//TODO: Duplicated from Button.tsx
function getVariantClassNames(variant: string) {
switch (variant) {
case "secondary": {
return "bg-brand-secondary hover:bg-brand-secondary-900 text-white focus-visible:outline-brand-secondary";
}
case "primary":
default: {
return "bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600";
}
}
}
const baseClassName =
"text-sm font-medium whitespace-nowrap rounded-lg px-3 py-2 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-150";
const variantClassNames = getVariantClassNames(variant);
const className = twMerge(baseClassName, variantClassNames, propsClassName);
// END TODO
return (
<Link {...rest}>
<button className={className}>
{icon && <FontAwesomeIcon className="mr-2" icon={icon} size="sm" />}
{children}
</button>
</Link>
);
};
@@ -1 +0,0 @@
export * from "./ButtonLink";
@@ -1,111 +0,0 @@
import { Fragment, useCallback } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { DoNotShow } from "~/constants";
import { Button } from "~/components";
interface Props {
text: string;
title: string;
open: boolean;
onClose: () => void;
onOk?: () => void;
okText?: string;
okColor?: string;
onCancel?: () => void;
cancelText?: string;
canHide?: boolean;
}
export const ConfirmationModal = ({
text,
title,
open,
onClose,
onOk,
okText = "OK",
okColor = "bg-brand-success",
onCancel,
cancelText = "Cancel",
canHide,
}: Props) => {
const handleOk = useCallback(() => {
const element = document.getElementById("hide-dialog");
if (element) {
if ((element as HTMLInputElement).checked) {
localStorage.setItem(title.replace(" ", "-"), DoNotShow);
}
}
onOk!();
}, [onOk, title]);
return (
<Transition appear show={open} as={Fragment}>
<Dialog open={open} onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/40" />
</Transition.Child>
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel p-5 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h4"
className="mb-4 text-xl font-bold text-white"
>
{title}
</Dialog.Title>
<div className="mt-2">{text}</div>
{canHide && (
<div className="mt-2">
<label>
<input id="hide-dialog" type="checkbox" />
&nbsp;&nbsp;Do not show this again
</label>
</div>
)}
<div className="mt-6 flex justify-end gap-2">
{!!onCancel && (
<Button
type="button"
onClick={onCancel}
className="rounded-lg px-3 py-2"
variant="secondary"
>
{cancelText}
</Button>
)}
{!!onOk && (
<Button
type="button"
onClick={handleOk}
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}
>
{okText}
</Button>
)}
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
);
};
@@ -1,52 +0,0 @@
import { faFileArrowUp, faFileAudio } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { P } from "~/components";
export const DragAndDropZone = ({
file,
fileTypes,
}: {
file?: File;
fileTypes: string[];
}) => {
if (!file) {
return (
<div className="flex cursor-pointer items-center gap-3.5 rounded-lg border border-dashed border-[#3F3F3F] bg-brand-secondary p-3">
<FontAwesomeIcon icon={faFileArrowUp} className="text-4xl" />
<div className="flex flex-col gap-0">
<P className="font-medium">
<u>Upload a file</u> or drop it here
</P>
<P className="flex items-center gap-2 text-sm font-normal opacity-50">
{fileTypes.join(", ").toString()} supported
</P>
</div>
</div>
);
}
const fileName = file.name.split(".")[0].toUpperCase();
const fileSize =
file.size >= 1024 * 1024
? (file.size / 1024 / 1024).toFixed(2) + " MB"
: `${Math.floor(file.size / 1024)} KB`;
return (
<div className="flex cursor-pointer items-center justify-between gap-3.5 rounded-lg border border-dashed border-[#3F3F3F] bg-brand-secondary p-3">
<FontAwesomeIcon icon={faFileAudio} className="text-4xl" />
<div className="flex grow flex-col gap-0">
<P className="font-medium">
{file.name.slice(0, file.name.lastIndexOf("."))}
</P>
<P className="flex items-center gap-2 text-sm font-normal">
<span className="opacity-50">
{`${fileName} file size: ${fileSize} `}
</span>
<u className="transition-all hover:text-white/80">Change File</u>
</P>
</div>
</div>
);
};
@@ -1,23 +0,0 @@
import { FileUploader as DragDropFileUploader } from "react-drag-drop-files";
// Usage refer to https://github.com/KarimMokhtar/react-drag-drop-files
import { DragAndDropZone } from "./DragAndDropZone";
export const FileUploader = ({
file,
fileTypes,
handleChange,
}: {
file: File | undefined;
handleChange: (file: File) => void;
fileTypes: string[];
}) => (
<DragDropFileUploader
handleChange={handleChange}
name="file"
maxSize={50}
types={fileTypes}
>
<DragAndDropZone file={file} fileTypes={fileTypes} />
</DragDropFileUploader>
);
@@ -1 +0,0 @@
export * from "./FileUploader";
@@ -1,14 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as FilterButtonsStories from './FilterButtons.stories';
<Meta of={FilterButtonsStories} />
# Three buttons which appear at the top of some of the media tabs.
The value can be set to Featured
<Canvas of={FilterButtonsStories.Featured} />
Mine
<Canvas of={FilterButtonsStories.Mine} />
or Bookmarked
<Canvas of={FilterButtonsStories.Bookmarked} />
@@ -1,36 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FilterButtons } from "./FilterButtons";
import { withActions } from "@storybook/addon-actions/decorator";
import { AssetFilterOption } from "~/enums";
const meta: Meta<typeof FilterButtons> = {
component: FilterButtons,
parameters: {
actions: {
handles: ["click"],
},
},
decorators: [withActions],
};
type Story = StoryObj<typeof FilterButtons>;
export const Featured: Story = {
render: () => (
<FilterButtons onClick={() => null} value={AssetFilterOption.FEATURED} />
),
};
export const Mine: Story = {
render: () => (
<FilterButtons onClick={() => null} value={AssetFilterOption.MINE} />
),
};
export const Bookmarked: Story = {
render: () => (
<FilterButtons onClick={() => null} value={AssetFilterOption.BOOKMARKED} />
),
};
export default meta;
@@ -1,43 +0,0 @@
import { AssetFilterOption } from "~/enums";
interface Props {
onClick: (button: AssetFilterOption) => void;
value: AssetFilterOption;
}
const FILTERS = [
{
text: "Featured",
option: AssetFilterOption.FEATURED,
},
{
text: "Mine",
option: AssetFilterOption.MINE,
},
{
text: "Bookmarked",
option: AssetFilterOption.BOOKMARKED,
},
];
export function FilterButtons({ onClick, value }: Props) {
return (
<div>
<div className="flex gap-2 overflow-x-auto overflow-y-hidden px-4">
{FILTERS.map((filter) => {
const isBookmarks = filter.option === AssetFilterOption.BOOKMARKED;
return (
<button
key={filter.option}
className={`filter-tab${value === filter.option ? " active" : ""}`}
disabled={isBookmarks}
onClick={() => onClick(filter.option)}
>
{filter.text}
</button>
);
})}
</div>
</div>
);
}
@@ -1 +0,0 @@
export * from "./FilterButtons";
@@ -1,107 +0,0 @@
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import EnvironmentVariables from "~/Classes/EnvironmentVariables";
interface Props {
size: number;
email_hash?: string;
username?: string;
avatarIndex?: number;
backgroundIndex?: number;
className?: string;
}
function Gravatar(props: Props) {
//url to the local fallback image
const localDefaultUrl =
props.avatarIndex === undefined
? "/resources/avatars/0.webp"
: `/resources/avatars/${props.avatarIndex}.webp`;
// submitted fallback need to be a url served from our server for gravatar to grab it
const defaultImageUrl =
props.avatarIndex === undefined
? "https://storyteller.ai/images/avatars/2000x2000/0.webp"
: `https://storyteller.ai/images/avatars/2000x2000/${props.avatarIndex}.webp`;
// NB: Gravatar suggests URI encoding these:
// https://en.gravatar.com/site/implement/images/
const encondedDefaultImage = encodeURIComponent(defaultImageUrl);
const gravatarUrl = `${EnvironmentVariables.values.GRAVATAR_API}/avatar/${props.email_hash}?s=${props.size}&d=${encondedDefaultImage}`;
const [{ imgUrl, showLoader }, setState] = useState<{
imgUrl: string;
showLoader: boolean;
}>({
imgUrl: gravatarUrl,
showLoader: true,
});
const altText = props.username ? `${props.username}'s gravatar` : "gravatar";
function getBackgroundColor(backgroundIndex?: number): string {
switch (backgroundIndex) {
case 0:
return "#E66462";
case 1:
return "#FD881B";
case 2:
return "#E7C13C";
case 3:
return "#4BA905";
case 4:
return "#25B8A0";
case 5:
return "#0078D1";
case 6:
return "#7F52C1";
case 7:
return "#FF66AC";
case 8:
return "#259FEC";
default:
return `#1a1a27`;
}
}
return (
<div
className={twMerge(
"relative aspect-square overflow-hidden rounded-full border border-white",
props.className,
)}
>
{showLoader && (
<div className="absolute flex h-full w-full items-center justify-center bg-brand-secondary">
<FontAwesomeIcon icon={faSpinnerThird} spin size={"lg"} />
</div>
)}
<img
crossOrigin="anonymous"
alt={altText}
src={imgUrl}
height={props.size}
width={props.size}
style={{ backgroundColor: getBackgroundColor(props.backgroundIndex) }}
onLoad={() => {
setState((curr) => ({
imgUrl: curr.imgUrl,
showLoader: false,
}));
}}
onError={() => {
setState((curr) => ({
imgUrl:
curr.imgUrl !== localDefaultUrl ? localDefaultUrl : curr.imgUrl,
showLoader: false,
}));
}}
/>
</div>
);
}
export { Gravatar };
@@ -1 +0,0 @@
export * from "./Gravatar";
@@ -1,19 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as InputStories from './Input.stories';
<Meta of={InputStories} />
# Input control
Here is the input with placeholder
<Canvas of={InputStories.Default} />
Here is the input with icon
<Canvas of={InputStories.WithIcon} />
Here is the input with label
<Canvas of={InputStories.WithLabel} />
Here is the input with error message
<Canvas of={InputStories.WithError} />
@@ -1,50 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Input } from "./Input";
import { withActions } from "@storybook/addon-actions/decorator";
import { faUser } from "@fortawesome/pro-solid-svg-icons";
const meta: Meta<typeof Input> = {
component: Input,
parameters: {
actions: {
handles: ["change", "blur", "focus"],
},
},
decorators: [withActions],
};
type Story = StoryObj<typeof Input>;
export const Default: Story = {
render: () => (
<div className="bg-action p-8">
<Input placeholder="field placeholder" />
</div>
),
};
export const WithIcon: Story = {
render: () => (
<div className="bg-action p-8">
<Input icon={faUser} />
</div>
),
};
export const WithLabel: Story = {
render: () => (
<div className="bg-action p-8">
<Input label="Input label" />
</div>
),
};
export const WithError: Story = {
render: () => (
<div className="bg-action p-8">
<Input errorMessage="Error message" />
</div>
),
};
export default meta;
@@ -1,83 +0,0 @@
import React from "react";
import { twMerge } from "tailwind-merge";
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Label, H6 } from "~/components";
import { kebabCase } from "~/utilities";
import {
disableHotkeyInput,
enableHotkeyInput,
DomLevels,
} from "~/pages/PageEnigma/signals";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
inputClassName?: string;
iconClassName?: string;
label?: string;
icon?: IconDefinition;
isError?: boolean;
errorMessage?: string;
}
export const Input = React.forwardRef(
(
{
label,
icon,
inputClassName,
iconClassName,
className,
id,
isError,
onBlur,
onFocus,
errorMessage,
...rest
}: InputProps,
ref: React.ForwardedRef<HTMLInputElement>,
) => {
return (
<div className={twMerge("flex flex-col", className)}>
{label && <Label htmlFor={id ? id : kebabCase(label)}>{label}</Label>}
<div className="relative w-full">
{icon && (
<FontAwesomeIcon
icon={icon}
className={twMerge("text-md absolute pl-3 pt-3", iconClassName)}
/>
)}
<input
ref={ref}
id={id ? id : label ? kebabCase(label) : undefined}
className={twMerge(
"h-10 w-full rounded-lg bg-brand-secondary px-3 py-2.5 text-white placeholder-white/50 outline-none",
"border border-[#3F3F3F] transition-all duration-150 ease-in-out hover:border-brand-primary/60 focus:border-brand-primary focus:!outline-none",
icon && "pl-10",
isError && "outline-red focus:outline-red",
inputClassName,
)}
onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
if (onFocus) {
onFocus(e);
}
disableHotkeyInput(DomLevels.INPUT);
}}
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
if (onBlur) {
onBlur(e);
}
enableHotkeyInput(DomLevels.INPUT);
}}
{...rest}
/>
{errorMessage && (
<H6 className="absolute z-10 text-red">{errorMessage}</H6>
)}
</div>
</div>
);
},
);
Input.displayName = "Input";
@@ -1 +0,0 @@
export * from "./Input";
@@ -1,10 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as InputVectorStories from './InputVector.stories';
<Meta of={InputVectorStories} />
# 3 Input controls: one for x, y, and z
It allows entry os plus or minus numbers with decimals. It can also be adjusted via dragging the mouse left or right.
<Canvas of={InputVectorStories.Default} />
@@ -1,44 +0,0 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { InputVector } from "./InputVector";
import { withActions } from "@storybook/addon-actions/decorator";
const meta: Meta<typeof InputVector> = {
component: InputVector,
parameters: {
actions: {
handles: ["change", "blur", "focus"],
},
},
decorators: [withActions],
};
type Story = StoryObj<typeof InputVector>;
const InputVectorRender = () => {
// Sets the hooks for both the label and primary props
const [xyz, setXyz] = useState<Record<string, string>>({
x: "0",
y: "0",
z: "0",
});
return (
<InputVector
x={xyz.x}
y={xyz.y}
z={xyz.z}
onChange={(value) => setXyz(value)}
/>
);
};
export const Default: Story = {
render: () => (
<div className="bg-action p-8">
<InputVectorRender />
</div>
),
};
export default meta;
@@ -1,232 +0,0 @@
import React, { ChangeEvent, RefObject, useRef } from "react";
import { twMerge } from "tailwind-merge";
import {
disableHotkeyInput,
DomLevels,
enableHotkeyInput,
} from "~/pages/PageEnigma/signals";
interface InputVectorProps {
x: string;
y: string;
z: string;
onChange: (v: Record<string, string>) => void;
increment?: number;
disabled?: boolean;
}
export const InputVector = ({
x,
y,
z,
onChange,
increment = 0.1,
disabled,
}: InputVectorProps) => {
const xRef = useRef<HTMLInputElement>(null);
const yRef = useRef<HTMLInputElement>(null);
const zRef = useRef<HTMLInputElement>(null);
const isDragging = useRef(false);
const previousMousePosition = useRef(0);
const oldX = useRef("");
const oldY = useRef("");
const oldZ = useRef("");
oldX.current = x;
oldY.current = y;
oldZ.current = z;
const inputCommonClasses =
"relative h-6 rounded-r-lg bg-brand-secondary p-2 text-sm text-white transition-all duration-100 ease-in-out outline-none -outline-offset-2 text-end w-full hover:cursor-e-resize hover:bg-brand-secondary-900";
const wrapperCommonClasses =
"relative flex items-center before:inline-block before:h-6 before:bg-brand-primary before:text-white before:rounded-l-lg before:w-1.5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs";
const lockedCommonClasses =
"hover:cursor-not-allowed hover:bg-brand-secondary";
function validateInput(newValue: string, oldValue: string) {
if (newValue === "" || newValue === "-" || newValue === ".") {
return newValue;
}
return /^-?\d*(\.\d{0,2})?$/.test(newValue) ? newValue : oldValue;
}
function handleOnChange(event: ChangeEvent<HTMLInputElement>) {
const newVector: Record<string, string> = {
x: oldX.current,
y: oldY.current,
z: oldZ.current,
};
newVector[event.target.name] = validateInput(
event.target.value,
newVector[event.target.name],
);
onChange(newVector);
}
const blurAllInputs = () => {
xRef.current?.blur();
yRef.current?.blur();
zRef.current?.blur();
};
// For dragging the input value to increment/decrement
const handleMouseDown = (
e: React.MouseEvent,
ref: RefObject<HTMLInputElement>,
) => {
e.stopPropagation();
blurAllInputs();
previousMousePosition.current = e.clientX;
isDragging.current = true;
const mouseMoveHandler = (e: MouseEvent) => {
e.stopPropagation();
const currentMousePosition = e.clientX;
const direction =
currentMousePosition === previousMousePosition.current
? 0
: currentMousePosition > previousMousePosition.current
? 1
: -1;
previousMousePosition.current = currentMousePosition;
const newVector = {
x: oldX.current,
y: oldY.current,
z: oldZ.current,
} as Record<string, string>;
const newValue =
parseFloat(newVector[ref.current?.name ?? ""]) + direction * increment;
newVector[ref.current?.name ?? ""] = (
Math.round(newValue * 1000) / 1000
).toString();
onChange(newVector);
};
const mouseUpHandler = () => {
document.removeEventListener("mousemove", mouseMoveHandler);
document.removeEventListener("mouseup", mouseUpHandler);
isDragging.current = false;
};
document.addEventListener("mousemove", mouseMoveHandler);
document.addEventListener("mouseup", mouseUpHandler);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
(e.currentTarget as HTMLInputElement).blur(); // Unfocus the input field on pressing Enter
}
};
return (
<div className="flex w-full flex-col justify-between gap-1.5">
<span
className={twMerge(
wrapperCommonClasses,
"before:bg-axis-x",
disabled && "opacity-60",
)}
>
<div className="absolute left-3.5 z-10 font-semibold">X</div>
<input
className={twMerge(
inputCommonClasses,
"focus:outline-axis-x",
disabled && lockedCommonClasses,
)}
type="text"
name="x"
onChange={handleOnChange}
ref={xRef}
value={x}
disabled={disabled}
onFocus={() => {
disableHotkeyInput(DomLevels.INPUT);
}}
onBlur={() => {
enableHotkeyInput(DomLevels.INPUT);
}}
onMouseDown={(e) => {
handleMouseDown(e, xRef);
}}
onKeyDown={handleKeyDown}
/>
</span>
<span
className={twMerge(
wrapperCommonClasses,
"before:bg-axis-y",
disabled && "opacity-60",
)}
>
<div className="absolute left-3.5 z-10 font-semibold">Y</div>
<input
className={twMerge(
inputCommonClasses,
"focus:outline-axis-y",
disabled && lockedCommonClasses,
)}
type="text"
name="y"
onChange={handleOnChange}
ref={yRef}
value={y}
disabled={disabled}
onFocus={() => {
disableHotkeyInput(DomLevels.INPUT);
}}
onBlur={() => {
enableHotkeyInput(DomLevels.INPUT);
}}
onMouseDown={(e) => {
handleMouseDown(e, yRef);
}}
onKeyDown={handleKeyDown}
/>
</span>
<span
className={twMerge(
wrapperCommonClasses,
"before:bg-axis-z",
disabled && "opacity-60",
)}
>
<div className="absolute left-3.5 z-10 font-semibold">Z</div>
<input
className={twMerge(
inputCommonClasses,
"focus:outline-axis-z",
disabled && lockedCommonClasses,
)}
type="text"
name="z"
onChange={handleOnChange}
ref={zRef}
value={z}
disabled={disabled}
onFocus={() => {
disableHotkeyInput(DomLevels.INPUT);
}}
onBlur={() => {
enableHotkeyInput(DomLevels.INPUT);
}}
onMouseDown={(e) => {
handleMouseDown(e, zRef);
}}
onKeyDown={handleKeyDown}
/>
</span>
</div>
);
};
@@ -1 +0,0 @@
export * from "./InputVector";
@@ -1,66 +0,0 @@
import { Fragment, useEffect, useState } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { faCheck, faChevronDown } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
interface ListDropdownProps {
list: { [key: string]: string }[];
onSelect: (val: string) => void;
}
export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => {
const [selected, setSelected] = useState(list[0]);
useEffect(() => {
onSelect(Object.values(selected)[0]);
}, [selected]);
return (
<Listbox value={selected} onChange={setSelected}>
<div className="relative mt-1">
<Listbox.Button className="relative h-10 w-full cursor-pointer rounded-md bg-brand-secondary py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-all duration-150 ease-in-out focus:outline-brand-primary sm:text-sm">
<span className="block truncate">{Object.keys(selected)[0]}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<FontAwesomeIcon icon={faChevronDown} aria-hidden="true" />
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-brand-secondary py-1 text-base shadow-lg focus:outline-none sm:text-sm">
{list.map((item, itemIdx) => (
<Listbox.Option
key={itemIdx}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 text-white ${
active ? "text-white" : "text-gray-400"
}`
}
value={item}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? "font-medium" : "font-normal"
}`}
>
{Object.values(item)[0]}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<FontAwesomeIcon icon={faCheck} aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
);
};
@@ -1,100 +0,0 @@
import { Transition } from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import { LoadingSpinner } from "~/components";
interface LoadingBarProps {
id?: string;
wrapperClassName?: string;
innerWrapperClassName?: string;
progressBackgroundClassName?: string;
progressClassName?: string;
variant?: string;
show?: boolean;
hasSpinner?: boolean;
progressData: {
progress: number;
label?: string;
message?: string;
};
}
export const LoadingBar = ({
wrapperClassName: propsWrapperClassName,
innerWrapperClassName: propsInnerWrapperClassName,
progressBackgroundClassName: propsProgressBackgroundClassName,
progressClassName: propsProgressClassName,
progressData,
hasSpinner,
variant = "primary",
show = true,
...rest
}: LoadingBarProps) => {
function getVariantClassNames(variant: string) {
switch (variant) {
case "secondary": {
return " bg-brand-secondary text-white ";
}
case "primary":
default: {
return " bg-brand-primary text-white ";
}
}
}
const wrapperClassName = twMerge(
"w-full h-full relative bg-ui-background",
propsWrapperClassName,
);
const innerWrapperClassName = twMerge(
"w-full h-full p-4 gap-6 m-auto flex flex-col justify-center items-center",
propsInnerWrapperClassName,
);
const progressBackgroundClassName = twMerge(
"w-full bg-brand-secondary-800 rounded-full h-2.5",
propsProgressBackgroundClassName,
);
const progressClassName = twMerge(
"h-2.5 rounded-full transition-all duration-1000",
getVariantClassNames(variant),
propsProgressClassName,
);
return (
<Transition
className={wrapperClassName}
show={show}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-1000"
leaveFrom="opacity-100"
leaveTo="opacity-0"
{...rest}
>
<div className={innerWrapperClassName}>
<div className="flex flex-col items-center gap-3">
<div className="flex items-center gap-2.5">
{hasSpinner && <LoadingSpinner className="h-5" />}
{progressData.label && (
<label className="text-lg font-medium">
{progressData.label}
</label>
)}
</div>
</div>
<div className={progressBackgroundClassName}>
<div
className={progressClassName}
style={{ width: progressData.progress + "%" }}
/>
</div>
{progressData.message && (
<p className="text-center text-sm opacity-75">
{progressData.message}
</p>
)}
</div>
</Transition>
);
};
@@ -1,61 +0,0 @@
import { Transition } from '@headlessui/react';
import { twMerge } from 'tailwind-merge';
import { H4 } from '.';
interface LoadingDotsProps {
className?: string;
isShowing?: boolean;
};
interface LoadingDotsInnerProps {
className?: string;
isShowing?: boolean;
type?: 'typing'|'bricks';
message?: string;
};
export const LoadingDotsTyping = (props: LoadingDotsProps)=>{
return (
<LoadingDots {...props} />
)
}
export const LoadingDotsBricks = (props: LoadingDotsProps)=>{
return (
<LoadingDots {...props} type="bricks"/>
)
}
export function LoadingDots({
className,
isShowing=true,
type = 'typing',
message,
}: LoadingDotsInnerProps){
const classNames = twMerge("w-full h-full flex flex-col justify-center items-center bg-ui-background gap-6", className);
return(
<Transition
className={classNames}
show={isShowing}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-1000"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
{ type==='typing' &&
<div className="dot-typing"></div>
}
{ type==='bricks' &&
<div className="dot-bricks"></div>
}
{
message &&
<H4>{message}</H4>
}
</Transition>
);
};
@@ -1,27 +0,0 @@
import { twMerge } from "tailwind-merge";
interface LoadingSpinnerProps {
className?: string;
}
export const LoadingSpinner = ({ className }: LoadingSpinnerProps) => {
return (
<svg
className={twMerge("h-6 w-6 animate-spin", className)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
);
};
@@ -1,86 +0,0 @@
import React, { useEffect, useState } from "react";
import { Input } from "../Input";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
interface NumberInputProps {
value: number;
onChange: (value: number) => void;
}
export const NumberInput: React.FC<NumberInputProps> = ({
value,
onChange,
}) => {
const [inputValue, setInputValue] = useState<string>(value.toString());
useEffect(() => {
setInputValue(Math.round(value * 100) / 100 + "");
}, [value]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
if (
newValue === "" ||
(!isNaN(Number(newValue)) && Number(newValue) <= 100)
) {
setInputValue(newValue);
const numericValue = parseFloat(newValue);
if (newValue !== "") {
onChange(Math.round(numericValue * 100) / 100);
}
}
};
const handleBlur = () => {
if (inputValue === "") {
setInputValue(Math.round(value * 100) / 100 + "");
}
};
const handleIncrement = () => {
const newValue = Math.min(Number(inputValue) + 1, 100);
setInputValue(Math.round(newValue * 100) / 100 + "");
onChange(Math.round(newValue * 100) / 100);
};
const handleDecrement = () => {
const newValue = Math.max(Number(inputValue) - 1, 0);
setInputValue(Math.round(newValue * 100) / 100 + "");
onChange(Math.round(newValue * 100) / 100);
};
const incrementButtonStyle =
"flex h-[15px] w-6 items-center justify-center text-[10px] text-white/80 bg-ui-controls hover:bg-ui-controls-button outline-none focus:outline-none transition-all active:outline-none";
return (
<div className="relative">
<Input
type="text"
value={inputValue}
onChange={handleChange}
onBlur={handleBlur}
inputClassName="w-[70px] h-[30px] rounded-md text-sm"
/>
<div className="absolute right-0 top-0 flex h-[30px] flex-col rounded-r-md border-l border-white/15 bg-gray-800">
<button
className={twMerge(
incrementButtonStyle,
"rounded-tr-md border-b border-white/10",
)}
onClick={handleIncrement}
>
<FontAwesomeIcon icon={faChevronUp} />
</button>
<button
className={twMerge(incrementButtonStyle, "rounded-br-md")}
onClick={handleDecrement}
>
<FontAwesomeIcon icon={faChevronDown} />
</button>
</div>
</div>
);
};
@@ -1 +0,0 @@
export * from "./NumberInput";
@@ -1,16 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as PaginationStories from './Pagination.stories';
<Meta of={PaginationStories} />
# Controls for pagination.
Here is what it looks like when empty
<Canvas of={PaginationStories.Empty} />
First Page
<Canvas of={PaginationStories.FirstPage} />
Middle
<Canvas of={PaginationStories.Middle} />
Last Page
<Canvas of={PaginationStories.LastPage} />
@@ -1,69 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Pagination } from "./Pagination";
import { withActions } from "@storybook/addon-actions/decorator";
const meta: Meta<typeof Pagination> = {
component: Pagination,
parameters: {
actions: {
handles: ["pageChange"],
},
},
decorators: [withActions],
};
type Story = StoryObj<typeof Pagination>;
export const Empty: Story = {
render: () => (
<div className="w-[300px] bg-brand-secondary-200">
<Pagination
className=""
currentPage={0}
totalPages={0}
onPageChange={() => {}}
/>
</div>
),
};
export const FirstPage: Story = {
render: () => (
<div className="w-[300px] bg-brand-secondary-200">
<Pagination
className=""
currentPage={0}
totalPages={5}
onPageChange={() => {}}
/>
</div>
),
};
export const Middle: Story = {
render: () => (
<div className="w-[300px] bg-brand-secondary-200">
<Pagination
className=""
currentPage={2}
totalPages={5}
onPageChange={() => {}}
/>
</div>
),
};
export const LastPage: Story = {
render: () => (
<div className="w-[300px] bg-brand-secondary-200">
<Pagination
className=""
currentPage={4}
totalPages={5}
onPageChange={() => {}}
/>
</div>
),
};
export default meta;
@@ -1,99 +0,0 @@
import {
faChevronRight,
faChevronLeft,
} from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { ButtonIcon } from "~/components";
export const Pagination = ({
className,
currentPage,
totalPages,
onPageChange,
onFetchMorePages,
}: {
className: string;
currentPage: number; // index starts from 0
totalPages: number;
onPageChange: (newPage: number) => void;
onFetchMorePages?: () => void;
}) => {
const pageGroupSize = 5;
const pageGroup = Math.floor(currentPage / pageGroupSize);
const totalPagesInPageGroup =
(pageGroup + 1) * pageGroupSize > totalPages
? totalPages % pageGroupSize
: pageGroupSize;
const pageGroupOffset = pageGroup * pageGroupSize;
const handleSetPreviousPage = () => {
onPageChange(currentPage - 1);
};
const handleSetNextPage = () => {
onPageChange(currentPage + 1);
};
return (
<nav
className={twMerge(
"flex items-center justify-between border-gray-200 px-4",
className,
)}
>
<ButtonIcon
className="-ml-2 pt-2 text-gray-400"
icon={faChevronLeft}
onClick={handleSetPreviousPage}
disabled={currentPage === 0}
aria-hidden
/>
<div className="hidden md:flex">
{[...Array(totalPagesInPageGroup)].map((e, i) => {
const pageIndex = i + pageGroupOffset;
if (pageIndex === currentPage) {
return (
<button
key={pageIndex}
className="border-indigo-500 text-indigo-600 inline-flex items-center border-t-2 px-4 pt-2 text-sm font-medium"
aria-current="page"
onClick={() => onPageChange(pageIndex)}
>
{pageIndex + 1}
</button>
);
} else {
return (
<button
key={pageIndex}
className="inline-flex items-center border-t-2 border-transparent px-4 pt-2 text-sm font-medium text-gray-500 hover:border-brand-primary hover:text-brand-primary"
onClick={() => onPageChange(pageIndex)}
>
{pageIndex + 1}
</button>
);
}
})}
</div>
<span>
{currentPage + 1 !== totalPages ? (
<ButtonIcon
className="-mr-2 pt-2 text-gray-500 hover:text-brand-primary"
icon={faChevronRight}
onClick={handleSetNextPage}
aria-hidden
/>
) : (
onFetchMorePages && (
<button
className="inline-flex items-center border-t-2 border-transparent pl-2 pr-4 pt-2 text-sm font-medium text-gray-500 hover:text-brand-primary"
key={totalPages}
onClick={onFetchMorePages}
>
More...
</button>
)
)}
</span>
</nav>
);
};
@@ -1 +0,0 @@
export * from "./Pagination";
@@ -1,11 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as PillStories from './Pill.stories';
<Meta of={PillStories} />
# Pill
A pill is a container around the children.
<Canvas of={PillStories.Default} />
@@ -1,15 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Pill } from "./Pill";
const meta: Meta<typeof Pill> = {
component: Pill,
parameters: {},
};
type Story = StoryObj<typeof Pill>;
export const Default: Story = {
render: () => <Pill>A Pill</Pill>,
};
export default meta;

Some files were not shown because too many files have changed in this diff Show More