Compare commits

...
33 changed files with 463 additions and 51 deletions
@@ -1,8 +1,9 @@
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use crate::state::app_dir::AppDataRoot;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use errors::AnyhowResult;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, ImageReader};
@@ -10,6 +11,7 @@ use log::{error, info};
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::sentinel_refresh::refresh_sentinel;
use openai_sora_client::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::upload::upload_media_from_file::SoraMediaUploadRequest;
use serde_derive::Deserialize;
@@ -40,11 +42,11 @@ pub async fn sora_image_remix_command(
_app: AppHandle,
request: SoraImageRemixCommand,
app_data_root: State<'_, AppDataRoot>,
sora_creds_holder: State<'_, SoraCredentialHolder>,
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> Result<String, String> {
info!("image_generation_command called; processing image...");
generate_image(request, &app_data_root, &sora_creds_holder)
generate_image(request, &app_data_root, &sora_creds_manager)
.await
.map_err(|err| {
error!("error: {:?}", err);
@@ -57,16 +59,31 @@ pub async fn sora_image_remix_command(
pub async fn generate_image(
request: SoraImageRemixCommand,
app_data_root: &AppDataRoot,
sora_creds_holder: &SoraCredentialHolder,
sora_creds_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
let sora_credentials = read_sora_credentials_from_disk(app_data_root)
// TODO(bt,2025-04-21): Read from in-memory cache instead, but allow for desktop replacement.
let mut sora_creds = read_sora_credentials_from_disk(app_data_root)
.map_err(|err| {
error!("Failed to read Sora credentials from disk: {:?}", err);
err
})?;
//let sora_credentials = sora_creds_holder.get_credentials()?;
let no_sentinel = !app_data_root.get_sora_sentinel_file_path().is_file()
|| sora_creds.sentinel.is_none();
if no_sentinel {
info!("Refreshing credentials...");
sora_creds = sora_creds_manager.call_sentinel_refresh()
.await
.map_err(|err| {
error!("Failed to refresh: {:?}", err);
err
})?;
// TODO: Write to disk.
}
let sora_media_tokens = vec![];
@@ -74,13 +91,36 @@ pub async fn generate_image(
// Note: This is incredibly inefficient. We should keep a local cache.
// Also, if they've already been uploaded to OpenAI, we shouldn't continue to re-upload.
let response = sora_image_gen_remix(SoraImageGenRemixRequest {
let mut response = sora_image_gen_remix(SoraImageGenRemixRequest {
prompt: request.prompt.to_string(),
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &sora_credentials,
}).await
credentials: &sora_creds,
}).await;
if let Err(err) = &response {
error!("Error in generating image: {:?}", err);
sora_creds = sora_creds_manager.call_sentinel_refresh()
.await
.map_err(|err| {
error!("Failed to refresh: {:?}", err);
err
})?;
info!("Retrying request...");
response = sora_image_gen_remix(SoraImageGenRemixRequest {
prompt: request.prompt.to_string(),
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &sora_creds,
}).await;
}
let response = response
.map_err(|err| {
error!("Failed to call Sora image generation: {:?}", err);
err
+5 -11
View File
@@ -11,6 +11,7 @@ use crate::commands::sora::sora_image_generation_command::sora_image_generation_
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
use crate::state::app_config::AppConfig;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::threads::sora_session_login_thread::sora_session_login_thread;
use tauri_plugin_log::Target;
@@ -29,16 +30,9 @@ pub fn run() {
let app_data_root = config.app_data_root.clone();
let app_data_root2 = config.app_data_root.clone();
let sora_creds_holder = SoraCredentialHolder::new();
let sora_creds_holder2 = sora_creds_holder.clone();
println!("Attempting to read existing credentials...");
if let Ok(creds) = read_sora_credentials_from_disk(&app_data_root) {
sora_creds_holder.set_credentials(creds).expect("there shouldn't be locking errors");
} else {
println!("No credentials found on disk.");
}
let sora_creds_manager = SoraCredentialManager::initialize_from_disk_infallible(&app_data_root);
let sora_creds_manager2 = sora_creds_manager.clone();
println!("Initializing backend runtime...");
@@ -65,13 +59,13 @@ pub fn run() {
//}
let app = app.handle().clone();
tauri::async_runtime::spawn(sora_session_login_thread(app, app_data_root2, sora_creds_holder2));
tauri::async_runtime::spawn(sora_session_login_thread(app, app_data_root2, sora_creds_manager2));
Ok(())
})
.manage(config)
.manage(app_data_root)
.manage(sora_creds_holder)
.manage(sora_creds_manager)
.invoke_handler(tauri::generate_handler![
flip_image,
open_sora_login_command,
@@ -1,2 +1,3 @@
pub mod read_sora_credentials_from_disk;
pub mod sora_credential_holder;
pub mod sora_credential_holder;
pub mod sora_credential_manager;
@@ -1,3 +1,4 @@
use std::ops::Deref;
use std::sync::{Arc, RwLock};
use anyhow::anyhow;
use errors::AnyhowResult;
@@ -16,11 +17,11 @@ impl SoraCredentialHolder {
}
}
pub fn set_credentials(&self, credentials: SoraCredentials) -> AnyhowResult<()> {
pub fn set_credentials(&self, credentials: &SoraCredentials) -> AnyhowResult<()> {
match self.credentials.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut creds) => {
*creds = Some(credentials);
*creds = Some(credentials.clone());
Ok(())
}
}
@@ -44,4 +45,14 @@ impl SoraCredentialHolder {
}
}
}
pub fn get_credentials_required(&self) -> AnyhowResult<SoraCredentials> {
match self.credentials.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(creds) => match creds.deref() {
None => Err(anyhow!("Credentials not set")),
Some(creds) => Ok(creds.clone()),
}
}
}
}
@@ -0,0 +1,88 @@
use crate::state::app_dir::AppDataRoot;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use errors::AnyhowResult;
use log::{error, info, warn};
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::sentinel_refresh::refresh_sentinel;
use std::fs::OpenOptions;
use std::io::Write;
use openai_sora_client::sentinel_refresh::generate::token::generate_token;
#[derive(Clone)]
pub struct SoraCredentialManager {
holder: SoraCredentialHolder,
app_data_root: AppDataRoot,
}
impl SoraCredentialManager {
pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self {
let holder = SoraCredentialHolder::new();
match read_sora_credentials_from_disk(app_data_root) {
Err(err) => warn!("Failed to read credentials from disk: {:?}", err),
Ok(creds) => {
holder.set_credentials(&creds).expect("Failed to set credentials");
}
}
Self {
holder,
app_data_root: app_data_root.clone(),
}
}
pub fn set_credentials(&self, creds: &SoraCredentials) -> AnyhowResult<()> {
self.holder.set_credentials(creds)
}
pub fn clear_credentials(&self) -> AnyhowResult<()> {
self.holder.clear_credentials()
}
pub fn get_credentials(&self) -> AnyhowResult<Option<SoraCredentials>> {
self.holder.get_credentials()
}
pub fn get_credentials_required(&self) -> AnyhowResult<SoraCredentials> {
self.holder.get_credentials_required()
}
pub fn reset_from_disk(&self) -> AnyhowResult<SoraCredentials> {
let creds = read_sora_credentials_from_disk(&self.app_data_root)?;
self.holder.set_credentials(&creds)?;
Ok(creds)
}
pub async fn call_sentinel_refresh(&self) -> AnyhowResult<SoraCredentials> {
// NB(bt,2025-04-21): Technically we don't need credentials to get a sentinel.
let mut creds = self.holder.get_credentials_required()?;
info!("Generating token...");
let sentinel = generate_token()
.await
.map_err(|err| {
error!("Failed to refresh: {:?}", err);
err
})?;
info!("Token obtained.");
creds.sentinel = Some(sentinel.clone());
self.holder.set_credentials(&creds)?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(self.app_data_root.get_sora_sentinel_file_path())?;
file.write_all(sentinel.as_bytes())?;
file.flush()?;
Ok(creds)
}
}
@@ -8,6 +8,7 @@ use tauri::{AppHandle, Manager, Webview};
use errors::AnyhowResult;
use crate::state::app_dir::AppDataRoot;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::utils::sora_webview_cookies::get_all_sora_cookies_as_string;
pub const LOGIN_WINDOW_NAME: &str = "login_window";
@@ -27,7 +28,7 @@ pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
pub async fn sora_session_login_thread(
app: AppHandle,
app_data_root: AppDataRoot,
sora_creds_holder: SoraCredentialHolder
sora_creds_manager: SoraCredentialManager
) -> ! {
loop {
for (window_name, webview) in app.webviews() {
@@ -18,6 +18,9 @@ pub enum SoraError {
#[error("Sora too many concurrent tasks: {0}")]
TooManyConcurrentTasks(String),
#[error("Sora invalid JWT: {0}")]
InvalidJwt(String),
#[error("Sora API error: {0}")]
GenericError(String),
@@ -120,6 +123,7 @@ pub enum RawSoraError {
#[serde(rename_all = "snake_case")]
pub enum SoraErrorCode {
SentinelBlock,
InvalidJwt,
TokenExpired,
TooManyConcurrentTasks,
Unknown(String),
@@ -188,6 +192,9 @@ pub (crate) async fn image_gen_http_request(sora_request: RawSoraImageGenRequest
SoraErrorCode::Unknown(_) => {
return Err(SoraError::GenericError(error_response.error.message));
}
SoraErrorCode::InvalidJwt => {
return Err(SoraError::InvalidJwt(error_response.error.message));
}
}
}
@@ -1,7 +1,7 @@
import { useParams } from "react-router-dom";
import { withProtectionRoute } from "~/components/hoc";
import { useRenderCounter } from "~/hooks/useRenderCounter";
import { Resource, invoke } from '@tauri-apps/api/core';
import { Resource, invoke } from "@tauri-apps/api/core";
// Components of the page
import { ToolbarUserProfile } from "~/components/features";
@@ -103,7 +103,7 @@ export const Main = withProtectionRoute(() => {
}}
className="max-w-5xl"
>
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center justify-center gap-5">
<div className="flex flex-col items-center gap-3">
<h1 className="text-3xl font-bold">
<FontAwesomeIcon
@@ -125,7 +125,7 @@ export const Main = withProtectionRoute(() => {
<div className="aspect-video w-full overflow-hidden rounded-md">
<video autoPlay muted loop controls={false}>
<source src="/videos/demo_video.mp4" type="video/mp4" />
<source src="/videos/artcraft-canvas-demo.mp4" type="video/mp4" />
</video>
</div>
<Button
@@ -134,14 +134,14 @@ export const Main = withProtectionRoute(() => {
iconFlip={true}
onClick={async () => {
if (isTauri()) {
await invoke('open_sora_login_command');
await invoke("open_sora_login_command");
setFirstTimeDialogOpen(false);
} else {
console.error('Tauri is not available in this environment');
console.error("Tauri is not available in this environment");
}
}}
>
Signin to OpenAI to get Started
Sign in to OpenAI to get Started
</Button>
{/* <Button
className="font-semibold"
@@ -35,6 +35,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PopoverItem } from "~/components/reusable/Popover";
import { Api, ApiManager } from "~/KonvaApp/Api";
import { useJobContext } from "~/components/JobContext";
import { IsDesktopApp } from "@storyteller/tauri-utils";
interface ReferenceImage {
id: string;
url: string;
@@ -289,7 +291,10 @@ export const PromptBox = () => {
referenceImages,
);
if (isDesktopApp == true) {
const isDesktop = IsDesktopApp();
console.log('Is this a desktop app?', isDesktop);
if (isDesktop) {
handleTauriEnqueue();
} else {
handleWebEnqueue();
+3
View File
@@ -33,6 +33,9 @@
},
{
"path": "../../libs/components/button/tsconfig.lib.json"
},
{
"path": "../../libs/tauri-utils/tsconfig.lib.json"
}
]
}
+3
View File
@@ -7,6 +7,9 @@
{
"path": "../../libs/components/button"
},
{
"path": "../../libs/tauri-utils"
},
{
"path": "./tsconfig.app.json"
},
@@ -1300,17 +1300,6 @@ class Editor {
}
}
if (this.cam_obj == undefined) {
this.cam_obj = this.activeScene.get_object_by_name(this.camera_name);
if (this.outliner_feature_flag) {
const result = this.sceneManager?.render_outliner(
this.timeline.characters,
);
if (result) outlinerState.items.value = result.items;
}
}
if (this.clock == undefined || this.renderer == undefined) {
return;
}
@@ -1,6 +1,7 @@
import { EventDispatcher, Quaternion, Vector3, Vector2 } from "three";
import * as THREE from "three";
import { cameras, selectedCameraId } from "~/pages/PageEnigma/signals/camera";
import { isPromptBoxFocused } from "~/pages/PageEnigma/signals/promptBox";
class FreeCam extends EventDispatcher {
object: THREE.PerspectiveCamera;
@@ -70,7 +71,7 @@ class FreeCam extends EventDispatcher {
}
keydown(event: KeyboardEvent) {
if (!this.enabled) {
if (!this.enabled || isPromptBoxFocused.value) {
return;
}
@@ -115,7 +116,7 @@ class FreeCam extends EventDispatcher {
}
keyup(event: KeyboardEvent) {
if (!this.enabled) {
if (!this.enabled || isPromptBoxFocused.value) {
return;
}
@@ -219,8 +220,12 @@ class FreeCam extends EventDispatcher {
this.updateMovementVector();
}
isStationary() {
return this.moveVector.distanceTo(new Vector3(0, 0, 0)) === 0;
}
update(delta: number) {
if (!this.enabled) {
if (!this.enabled || this.isStationary()) {
return;
}
@@ -315,7 +315,6 @@ export class MouseControls {
return;
} else if (event.key === "b") {
// Open asset modal
event.preventDefault();
assetModalVisible.value = true;
assetModalVisibleDuringDrag.value = true;
return;
@@ -49,6 +49,7 @@ import {
focalLengthDragging,
} from "~/pages/PageEnigma/signals/camera";
import { toast } from "@storyteller/ui-toaster";
import { isPromptBoxFocused } from "~/pages/PageEnigma/signals/promptBox";
interface ReferenceImage {
id: string;
@@ -59,6 +60,7 @@ interface ReferenceImage {
import { EngineApi } from "~/Classes/ApiManager/EngineApi";
import { UploaderStates } from "~/enums/UploaderStates";
import { CameraSettingsModal } from "../CameraSettingsModal";
import { twMerge } from "tailwind-merge";
export const PromptBox = () => {
useSignals();
@@ -550,7 +552,12 @@ export const PromptBox = () => {
})}
</div>
)}
<div className="glass w-[730px] rounded-xl p-4">
<div
className={twMerge(
"glass w-[730px] rounded-xl p-4",
isPromptBoxFocused.value ? "!border !border-primary" : "",
)}
>
<input
type="file"
ref={fileInputRef}
@@ -596,9 +603,11 @@ export const PromptBox = () => {
onKeyDown={handleKeyDown}
onFocus={() => {
disableHotkeyInput(DomLevels.INPUT);
isPromptBoxFocused.value = true;
}}
onBlur={() => {
enableHotkeyInput(DomLevels.INPUT);
isPromptBoxFocused.value = false;
}}
/>
</div>
@@ -0,0 +1,3 @@
import { signal } from "@preact/signals-react";
export const isPromptBoxFocused = signal(false);
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+36
View File
@@ -0,0 +1,36 @@
{
"extends": [
"plugin:@nx/react",
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
# @frontend/tauri-utils
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @frontend/tauri-utils` to execute the unit tests via [Vitest](https://vitest.dev/).
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@storyteller/tauri-utils",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './lib/tauri-utils';
@@ -0,0 +1,13 @@
export function IsDesktopApp(): boolean {
if (typeof window !== 'undefined') {
if ('__TAURI_INTERNALS__' in window) {
return true;
} else if ('__TAURI__' in window) {
return true;
}
}
return false;
}
(window as any).IsDesktopApp = IsDesktopApp;
@@ -0,0 +1 @@
export { IsDesktopApp } from './IsDesktopApp';
+1
View File
@@ -0,0 +1 @@
// This file should be used to export ONLY server-code from the library.
@@ -0,0 +1,3 @@
import { installGlobals } from '@remix-run/node';
import "@testing-library/jest-dom/matchers";
installGlobals();
+13
View File
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
@@ -0,0 +1,49 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"lib": ["dom", "esnext"],
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"eslint.config.js",
"eslint.config.cjs",
"eslint.config.mjs"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+49
View File
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/tauri-utils',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@frontend/tauri-utils',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {setupFiles: ['./src/test-setup.ts'],
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+8
View File
@@ -2041,6 +2041,10 @@
"name": "@frontend/login",
"version": "0.0.1"
},
"libs/tauri-utils": {
"name": "@storyteller/tauri-utils",
"version": "0.0.1"
},
"node_modules/@adobe/css-tools": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz",
@@ -10047,6 +10051,10 @@
"resolved": "libs/api",
"link": true
},
"node_modules/@storyteller/tauri-utils": {
"resolved": "libs/tauri-utils",
"link": true
},
"node_modules/@storyteller/ui-button": {
"resolved": "libs/components/button",
"link": true
+9 -3
View File
@@ -5,7 +5,9 @@
"emitDeclarationOnly": true,
"importHelpers": true,
"isolatedModules": true,
"lib": ["es2022"],
"lib": [
"es2022"
],
"module": "esnext",
"moduleResolution": "bundler",
"noEmitOnError": true,
@@ -16,10 +18,14 @@
"skipLibCheck": true,
"strict": true,
"target": "es2022",
"customConditions": ["development"],
"customConditions": [
"development"
],
"baseUrl": ".",
"paths": {
"@frontend/login": ["libs/login/src/index.ts"]
"@frontend/login": [
"libs/login/src/index.ts"
]
}
}
}
+3
View File
@@ -41,6 +41,9 @@
},
{
"path": "./libs/components/tooltip"
},
{
"path": "./libs/tauri-utils"
}
]
}