Compare commits

...

10 Commits

126 changed files with 19146 additions and 432 deletions

View File

@@ -0,0 +1,30 @@
# 📱 How to Build the Offline APK
I have converted the application to **Offline Mode**. It now stores all data locally on the device, so you **DO NOT** need to deploy a backend server.
## 🚀 Steps to Build APK
1. **Copy the Project:**
Download the entire `MindShift-Windows` folder to your local machine.
2. **Install Dependencies:**
Open a terminal in the `MindShift-Windows` folder and run:
```bash
npm install
```
3. **Open Android Studio:**
Run the following command to open the project in Android Studio (you must have Android Studio installed):
```bash
npx cap open android
```
4. **Build & Run:**
- In Android Studio, wait for Gradle to sync.
- Click the **Run** (Play) button to test on an emulator/device.
- To build the APK: Go to `Build > Build Bundle(s) / APK(s) > Build APK(s)`.
## Notes
- The app now uses `localStorage` to save your Moods, Thoughts, and Gratitude entries.
- If you uninstall the app, your data will be cleared (standard behavior for local-only apps).
- No internet connection is required for the app to function.

View File

@@ -0,0 +1,55 @@
@echo off
echo ===================================================
echo MindShift APK Builder (One-Click)
echo ===================================================
echo.
:: Check for Java
where java >nul 2>nul
if %errorlevel% neq 0 (
echo [ERROR] Java is not installed or not in PATH.
echo Please install Java (JDK 17 recommended) and try again.
pause
exit /b
)
:: Check for Android Home
if "%ANDROID_HOME%"=="" (
echo [ERROR] ANDROID_HOME environment variable is not set.
echo Please install Android Studio and set ANDROID_HOME to your SDK location.
pause
exit /b
)
echo [1/3] Installing dependencies...
call npm install
if %errorlevel% neq 0 (
echo Failed to install dependencies.
pause
exit /b
)
echo [2/3] Syncing Capacitor...
call npx cap sync
if %errorlevel% neq 0 (
echo Failed to sync Capacitor.
pause
exit /b
)
echo [3/3] Building APK (Debug Mode)...
cd android
call gradlew.bat assembleDebug
cd ..
echo.
echo ===================================================
if exist "android\app\build\outputs\apk\debug\app-debug.apk" (
echo [SUCCESS] APK created successfully!
echo Location: android\app\build\outputs\apk\debug\app-debug.apk
explorer "android\app\build\outputs\apk\debug\"
) else (
echo [ERROR] Build failed. Please check the logs above.
)
echo ===================================================
pause

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

101
MindShift-Windows/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

View File

@@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace "com.mindshift.app"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.mindshift.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-haptics')
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,5 @@
package com.mindshift.app;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">MindShift</string>
<string name="title_activity_main">MindShift</string>
<string name="package_name">com.mindshift.app</string>
<string name="custom_url_scheme">com.mindshift.app</string>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
classpath 'com.google.gms:google-services:4.4.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,6 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-haptics'
project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')

View File

@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
MindShift-Windows/android/gradlew vendored Normal file
View File

@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
MindShift-Windows/android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.12.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
}

View File

@@ -0,0 +1,62 @@
#!/bin/bash
set -e
echo "🚀 Starting Automated APK Build in WSL..."
# --- SETUP JAVA 21 (Portable) ---
JAVA_DIR="$HOME/java-21"
if [ ! -d "$JAVA_DIR" ]; then
echo "⬇️ Downloading OpenJDK 21 (Temurin)..."
mkdir -p "$JAVA_DIR"
cd "$JAVA_DIR"
# Adoptium Temurin JDK 21
wget -q -L https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jdk_x64_linux_hotspot_21.0.2_13.tar.gz -O jdk21.tar.gz
tar -xzf jdk21.tar.gz --strip-components=1
rm jdk21.tar.gz
echo "✅ Java 21 Installed."
else
echo "✅ Java 21 already present."
fi
export JAVA_HOME="$JAVA_DIR"
export PATH="$JAVA_HOME/bin:$PATH"
# --- SETUP ANDROID SDK ---
SDK_DIR="$HOME/android-sdk"
mkdir -p "$SDK_DIR/cmdline-tools"
if [ ! -d "$SDK_DIR/cmdline-tools/latest" ]; then
echo "⬇️ Downloading Android Command Line Tools..."
cd "$SDK_DIR/cmdline-tools"
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O tools.zip
unzip -q tools.zip
mv cmdline-tools latest
rm tools.zip
echo "✅ Tools downloaded."
else
echo "✅ Android Tools already present."
fi
export ANDROID_HOME="$SDK_DIR"
export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools"
# --- INSTALL SDK PACKAGES ---
echo "📦 Checking SDK Packages..."
yes | sdkmanager --licenses > /dev/null
# Only install if missing to save time
if [ ! -d "$SDK_DIR/platforms/android-35" ]; then
echo "⬇️ Installing Platform 35..."
sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0" > /dev/null
fi
# --- BUILD APK ---
PROJECT_DIR="/mnt/e/TRAE Playground/MindShift-Windows/android"
cd "$PROJECT_DIR"
echo "🔨 Building APK with Java 21..."
chmod +x gradlew
./gradlew assembleDebug
# --- SUCCESS ---
echo "🎉 Build Complete!"
echo "APK Location: $PROJECT_DIR/app/build/outputs/apk/debug/app-debug.apk"

View File

@@ -0,0 +1,9 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.mindshift.app',
appName: 'MindShift',
webDir: 'src'
};
export default config;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mindshift-cbt-therapy",
"version": "1.0.0",
"version": "1.0.7",
"description": "MindShift - Your personal CBT therapy companion for Windows 11",
"main": "src/main.js",
"homepage": "./",
@@ -61,6 +61,10 @@
}
},
"dependencies": {
"@capacitor/android": "^7.4.4",
"@capacitor/cli": "^7.4.4",
"@capacitor/core": "^7.4.4",
"@capacitor/haptics": "^7.0.2",
"electron-updater": "^6.1.7"
},
"devDependencies": {

View File

@@ -135,11 +135,21 @@ export const gratitudeAPI = {
// Progress API
export const progressAPI = {
async getProgressStats() {
return await apiCall('/progress/stats');
return await apiCall('/dashboard/stats');
},
async getProgressHistory() {
return await apiCall('/progress/history');
async getProgressHistory(days = 30) {
return await apiCall(`/progress?days=${days}`);
}
};
// Exercise API
export const exerciseAPI = {
async logSession(type, duration) {
return await apiCall('/exercises', {
method: 'POST',
body: JSON.stringify({ exerciseType: type, duration })
});
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
/* Guided Relaxation - Mindfulness Session */
.guided-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at center, #2E7D32, #004D40);
z-index: 6000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
animation: fadeIn 0.5s ease;
padding: 20px;
text-align: center;
transition: background 1s ease;
}
/* Mode specific backgrounds */
.guided-overlay.mode-body_scan {
background: radial-gradient(circle at center, #4A148C, #311B92);
}
.guided-overlay.mode-visualization {
background: radial-gradient(circle at center, #00695C, #004D40);
}
.guided-title-large {
font-size: 32px;
font-weight: 300;
margin-bottom: 40px;
letter-spacing: 1px;
animation: slideInDown 0.5s ease;
}
.guided-step-icon {
font-size: 100px;
margin-bottom: 40px;
animation: floatIcon 4s ease-in-out infinite;
filter: drop-shadow(0 0 30px rgba(255,255,255,0.4));
transition: all 0.5s ease;
}
@keyframes floatIcon {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-20px) scale(1.05); }
}
.guided-instruction {
font-size: 28px;
font-weight: 400;
margin-bottom: 20px;
line-height: 1.4;
max-width: 800px;
animation: fadeIn 0.5s ease;
}
.guided-sub {
font-size: 20px;
opacity: 0.8;
margin-bottom: 50px;
max-width: 600px;
font-weight: 300;
animation: fadeIn 0.5s ease 0.2s both;
}
/* Progress Bar for auto-playing sessions */
.guided-progress-bar-container {
width: 80%;
max-width: 300px;
height: 6px;
background: rgba(255,255,255,0.2);
border-radius: 3px;
margin-bottom: 40px;
overflow: hidden;
}
.guided-progress-bar {
height: 100%;
background: white;
width: 0%;
transition: width linear;
}
/* Mode Selection Menu */
.guided-mode-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 20px;
width: 100%;
max-width: 800px;
}
.guided-mode-card {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.3);
border-radius: 20px;
padding: 24px;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.guided-mode-card:hover {
background: rgba(255,255,255,0.2);
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.guided-mode-icon {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
.guided-mode-title {
font-size: 18px;
font-weight: 500;
}
.guided-controls {
position: absolute;
top: 20px;
right: 20px;
display: flex;
gap: 16px;
z-index: 10;
}
.guided-bottom-controls {
display: flex;
gap: 20px;
align-items: center;
}
.play-pause-btn {
width: 64px;
height: 64px;
border-radius: 50%;
background: white;
color: var(--primary-dark);
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
cursor: pointer;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
transition: transform 0.2s;
}
.play-pause-btn:hover {
transform: scale(1.1);
}
.play-pause-btn:active {
transform: scale(0.95);
}
/* Pulse Animation for Body Scan */
.body-scan-pulse {
position: absolute;
width: 300px;
height: 300px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.3);
animation: scanPulse 4s infinite;
pointer-events: none;
}
@keyframes scanPulse {
0% { transform: scale(0.8); opacity: 0; }
50% { opacity: 1; }
100% { transform: scale(1.5); opacity: 0; }
}
/* RTL Fixes for Guided */
html[dir="rtl"] .guided-controls {
right: auto;
left: 20px;
}
/* Stress Slider */
.stress-slider-container {
width: 100%;
max-width: 400px;
margin: 40px 0;
}
.stress-slider {
width: 100%;
height: 10px;
background: rgba(255,255,255,0.3);
border-radius: 5px;
outline: none;
-webkit-appearance: none;
}
.stress-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 30px;
height: 30px;
background: white;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.stress-value {
font-size: 48px;
font-weight: bold;
margin-top: 20px;
}
/* Settings Modal */
.guided-settings-panel {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 20px;
display: none;
animation: slideInUp 0.3s ease;
}
.guided-settings-panel.active {
display: block;
}
.setting-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
color: white;
}
/* Summary Card */
.summary-card {
background: white;
color: var(--on-surface);
padding: 40px;
border-radius: 24px;
text-align: center;
animation: zoomIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.summary-stat {
font-size: 24px;
color: var(--primary);
margin: 20px 0;
font-weight: bold;
}

View File

@@ -2,11 +2,11 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>MindShift - CBT Therapy App</title>
<!-- PWA Meta Tags -->
<meta name="theme-color" content="#FF6B6B">
<meta name="theme-color" content="#8ECAE6">
<meta name="description" content="Your personal CBT therapy companion for mood management and mental wellness">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
@@ -24,6 +24,7 @@
<!-- Styles -->
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="guided-styles.css">
</head>
<body>
<!-- Animated Background Orbs -->
@@ -54,6 +55,9 @@
MindShift
</div>
<div class="header-actions">
<button class="nav-item" onclick="showLanguageModal()" style="color: var(--primary);">
<span class="material-icons">language</span>
</button>
<div style="position: relative;">
<button class="nav-item" onclick="toggleNotifications()" style="color: white;">
<span class="material-icons">notifications</span>
@@ -72,7 +76,11 @@
<!-- Main Content -->
<main id="main-content" class="main-content">
<!-- Content will be dynamically loaded here -->
<!-- Loading State -->
<div id="initial-loader" style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 80vh; color: #666;">
<div class="spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #FF6B6B; border-radius: 50%; animation: spin 1s linear infinite;"></div>
<p style="margin-top: 20px; font-family: sans-serif;">Initializing MindShift...</p>
</div>
</main>
<!-- Bottom Navigation -->
@@ -104,8 +112,19 @@
<span class="material-icons">add</span>
</button>
<style>
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
<!-- Scripts -->
<script type="module" src="api.js"></script>
<script type="module" src="app.js"></script>
<script>
window.addEventListener('error', function(e) {
// Display error on screen if app fails to load
var loader = document.getElementById('initial-loader');
if (loader) {
loader.innerHTML = '<div style="color: red; padding: 20px; text-align: center;"><h3>Error Loading App</h3><p>' + e.message + '</p></div>';
}
});
</script>
<script type="module" src="./app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,228 @@
// Offline API Implementation using LocalStorage
// This replaces the server-based API for the offline APK build
// --- Local Database Helper ---
const db = {
get(key) {
const data = localStorage.getItem(`mindshift_${key}`);
return data ? JSON.parse(data) : [];
},
set(key, data) {
localStorage.setItem(`mindshift_${key}`, JSON.stringify(data));
},
add(key, item) {
const data = this.get(key);
item.id = Date.now().toString();
item.created_at = new Date().toISOString();
// Add user_id if logged in
const user = getCurrentUser();
if (user) item.user_id = user.id;
data.push(item);
this.set(key, data);
return item;
},
update(key, id, updates) {
const data = this.get(key);
const index = data.findIndex(item => item.id === id);
if (index !== -1) {
data[index] = { ...data[index], ...updates };
this.set(key, data);
return data[index];
}
return null;
},
remove(key, id) {
const data = this.get(key);
const newData = data.filter(item => item.id !== id);
this.set(key, newData);
}
};
function getCurrentUser() {
const userStr = localStorage.getItem('mindshift_currentUser');
return userStr ? JSON.parse(userStr) : null;
}
// --- Authentication API ---
export const authAPI = {
async register(name, email, password) {
// Simulate network delay
await new Promise(r => setTimeout(r, 500));
const users = db.get('users');
if (users.find(u => u.email === email)) {
throw new Error('Email already exists');
}
const newUser = { id: Date.now().toString(), name, email, password }; // In real app, hash password!
users.push(newUser);
db.set('users', users);
localStorage.setItem('mindshift_currentUser', JSON.stringify(newUser));
return { token: 'offline-token', user: newUser };
},
async login(email, password) {
await new Promise(r => setTimeout(r, 500));
const users = db.get('users');
const user = users.find(u => u.email === email && u.password === password);
if (!user) {
throw new Error('Invalid credentials');
}
localStorage.setItem('mindshift_currentUser', JSON.stringify(user));
return { token: 'offline-token', user };
},
async logout() {
localStorage.removeItem('mindshift_currentUser');
},
async getProfile() {
return getCurrentUser();
}
};
// --- Mood API ---
export const moodAPI = {
async trackMood(moodType, intensity, notes) {
return db.add('moods', { mood_type: moodType, intensity, notes });
},
async getMoodHistory() {
const user = getCurrentUser();
if (!user) return [];
const moods = db.get('moods').filter(m => m.user_id === user.id);
return moods.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
}
};
// --- Thought Record API ---
export const thoughtAPI = {
async saveThoughtRecord(thoughtData) {
// Map frontend keys to DB keys if needed, but for offline we can store as is
return db.add('thoughts', thoughtData);
},
async getThoughtRecords() {
const user = getCurrentUser();
if (!user) return [];
const thoughts = db.get('thoughts').filter(t => t.user_id === user.id);
return thoughts.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
},
async updateThoughtRecord(id, data) {
return db.update('thoughts', id, data);
},
async deleteThoughtRecord(id) {
return db.remove('thoughts', id);
}
};
// --- Gratitude API ---
export const gratitudeAPI = {
async saveGratitudeEntry(entry) {
// Entry object comes as { entries: [], date: ... }
// We'll store individual entries or the whole block.
// Let's store the block to match frontend expectation
return db.add('gratitude', entry);
},
async getGratitudeEntries() {
const user = getCurrentUser();
if (!user) return [];
const entries = db.get('gratitude').filter(g => g.user_id === user.id);
return entries.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
}
};
// --- Progress API ---
export const progressAPI = {
async getProgressStats() {
const user = getCurrentUser();
if (!user) return { today: {}, week: {}, totals: {} };
const moods = db.get('moods').filter(m => m.user_id === user.id);
const gratitude = db.get('gratitude').filter(g => g.user_id === user.id);
const exercises = db.get('exercises').filter(e => e.user_id === user.id);
const today = new Date().toISOString().split('T')[0];
const todayMoods = moods.filter(m => m.created_at.startsWith(today));
// Calculate stats
const todayAvg = todayMoods.length > 0
? todayMoods.reduce((sum, m) => sum + parseInt(m.intensity), 0) / todayMoods.length
: 0;
return {
today: { mood_score: todayAvg.toFixed(1) },
totals: {
totalSessions: exercises.length,
totalGratitude: gratitude.length
},
week: { avgMood: 0 } // simplified for offline
};
},
async getProgressHistory(days = 7) {
const user = getCurrentUser();
if (!user) return [];
const moods = db.get('moods').filter(m => m.user_id === user.id);
// Group by date and avg score
const history = [];
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const dayMoods = moods.filter(m => m.created_at.startsWith(dateStr));
const score = dayMoods.length > 0
? dayMoods.reduce((sum, m) => sum + parseInt(m.intensity), 0) / dayMoods.length
: 0;
history.push({ date: dateStr, mood_score: score });
}
return history;
}
};
// --- Exercise API ---
export const exerciseAPI = {
async logSession(type, duration) {
return db.add('exercises', { exercise_type: type, duration });
}
};
// --- Notification API ---
export const notificationAPI = {
async getNotifications() {
const user = getCurrentUser();
if (!user) return [];
return db.get('notifications').filter(n => n.user_id === user.id);
},
async markAsRead(id) {
return db.update('notifications', id, { read: true });
},
async deleteNotification(id) {
return db.remove('notifications', id);
},
async addNotification(notification) {
return db.add('notifications', notification);
}
};
export function isAuthenticated() {
return !!getCurrentUser();
}
export function initializeAPI() {
return true;
}

View File

@@ -1,29 +1,36 @@
/* Base Styles & Variables */
:root {
--primary: #FF6B6B;
--primary-light: #FF8E8E;
--primary-dark: #E55555;
--primary-container: #FFE5E5;
/* Relaxing Palette */
--primary: #6B9080; /* Soft Sage Green */
--primary-light: #A4C3B2;
--primary-dark: #3A5A4A;
--primary-container: #EAF4F0;
--on-primary: #FFFFFF;
--on-primary-container: #410002;
--secondary: #FFB74D;
--secondary-container: #FFF3E0;
--on-primary-container: #1C3329;
--secondary: #8ECAE6; /* Soft Sky Blue */
--secondary-container: #E1F5FE;
--on-secondary: #FFFFFF;
--on-secondary-container: #4E2B00;
--tertiary: #4FC3F7;
--tertiary-container: #E1F5FE;
--surface: rgba(255, 255, 255, 0.9);
--surface-variant: #F5F5F5;
--on-surface: #212121;
--on-surface-variant: #757575;
--outline: #BDBDBD;
--shadow: rgba(0,0,0,0.1);
--error: #FF5252;
--success: #66BB6A;
--warning: #FFA726;
--joy: #AB47BC;
--peace: #26A69A;
--energy: #FFEE58;
--on-secondary-container: #004D61;
--tertiary: #B8B8FF; /* Gentle Lavender */
--tertiary-container: #EFEEFF;
--surface: rgba(255, 255, 255, 0.92);
--surface-variant: #F4F7F6; /* Very soft cool grey */
--on-surface: #2C3E50;
--on-surface-variant: #607D8B;
--outline: #CFD8DC;
--shadow: rgba(44, 62, 80, 0.1);
--error: #E57373;
--success: #81C784;
--warning: #FFB74D;
--joy: #9575CD;
--peace: #4DB6AC;
--energy: #FFF176;
}
* {
@@ -34,9 +41,9 @@
body {
font-family: 'Roboto', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #FF6B6B 100%);
background: linear-gradient(135deg, #FDFBF7 0%, #E6F3F0 100%); /* Relaxing Cream to Sage */
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
animation: gradientBG 20s ease infinite;
color: var(--on-surface);
line-height: 1.5;
min-height: 100vh;
@@ -113,7 +120,7 @@ body {
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #FF6B6B 0%, #4ECDC4 100%);
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
display: flex;
flex-direction: column;
justify-content: center;
@@ -159,10 +166,10 @@ body {
/* Header */
.app-header {
background-color: rgba(255, 107, 107, 0.95);
color: var(--on-primary);
background-color: rgba(255, 255, 255, 0.95);
color: var(--primary);
padding: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
position: sticky;
top: 0;
z-index: 100;
@@ -171,8 +178,8 @@ body {
}
.app-header:hover {
background-color: rgba(255, 107, 107, 1);
box-shadow: 0 6px 24px rgba(0,0,0,0.15);
background-color: rgba(255, 255, 255, 1);
box-shadow: 0 6px 24px rgba(0,0,0,0.08);
}
.header-content {
@@ -189,12 +196,16 @@ body {
position: relative;
}
.header-actions button {
color: var(--primary) !important; /* Override inline style */
}
/* Notification Badge Pulse */
.notification-badge {
position: absolute;
top: 8px;
right: 8px;
background: var(--secondary);
background: var(--error);
color: white;
font-size: 10px;
font-weight: bold;
@@ -238,15 +249,119 @@ body {
bottom: 0;
left: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
box-shadow: 0 -4px 20px rgba(0,0,0,0.1);
background-color: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
box-shadow: 0 -4px 30px rgba(0,0,0,0.05);
display: flex;
justify-content: space-around;
padding: 12px 0;
/* Android Safe Area Fix - Significantly Increased Padding */
padding-bottom: calc(40px + env(safe-area-inset-bottom));
z-index: 100;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
border-top-left-radius: 24px;
border-top-right-radius: 24px;
}
/* Guided Relaxation Styles */
.guided-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at center, #2E7D32, #004D40);
z-index: 6000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
animation: fadeIn 0.5s ease;
padding: 20px;
text-align: center;
}
.guided-step-icon {
font-size: 80px;
margin-bottom: 30px;
animation: floatIcon 3s ease-in-out infinite;
filter: drop-shadow(0 0 20px rgba(255,255,255,0.3));
}
@keyframes floatIcon {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-15px); }
}
.guided-instruction {
font-size: 28px;
font-weight: 300;
margin-bottom: 16px;
line-height: 1.4;
}
.guided-sub {
font-size: 18px;
opacity: 0.8;
margin-bottom: 40px;
max-width: 80%;
}
.guided-progress-dots {
display: flex;
gap: 8px;
margin-bottom: 40px;
}
.progress-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transition: all 0.3s;
}
.progress-dot.active {
background: white;
transform: scale(1.2);
}
.guided-action-btn {
background: white;
color: #004D40;
border: none;
padding: 16px 40px;
border-radius: 50px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
transition: transform 0.2s;
}
.guided-action-btn:active {
transform: scale(0.95);
}
.guided-controls {
position: absolute;
top: 20px;
right: 20px;
display: flex;
gap: 16px;
}
.icon-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.nav-item {
@@ -299,7 +414,8 @@ body {
max-width: 1200px;
margin: 0 auto;
padding: 16px;
padding-bottom: 100px;
/* Increased bottom padding to prevent content overlap with taller nav */
padding-bottom: calc(120px + env(safe-area-inset-bottom));
min-height: calc(100vh - 120px);
position: relative;
z-index: 1;
@@ -381,7 +497,7 @@ body {
border-color: var(--primary);
background: linear-gradient(135deg, var(--primary-container), white);
transform: scale(1.05);
box-shadow: 0 0 0 4px rgba(255, 107, 107, 0.2);
box-shadow: 0 0 0 4px rgba(107, 144, 128, 0.2);
}
.mood-emoji {
@@ -401,6 +517,97 @@ body {
100% { transform: scale(1.2); }
}
/* Progress Section */
.progress-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin: 16px 0;
}
.progress-card {
background-color: var(--surface-variant);
border-radius: 12px;
padding: 16px;
text-align: center;
transition: all 0.3s ease;
}
.progress-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
background-color: white;
}
.progress-value {
font-size: 24px;
font-weight: 500;
color: var(--primary);
margin-bottom: 4px;
}
.progress-label {
font-size: 14px;
color: var(--on-surface-variant);
}
/* Chart Container */
.chart-container {
position: relative;
height: 250px;
background: rgba(255, 255, 255, 0.5);
border-radius: 16px;
padding: 16px;
}
/* Analytics Cards */
.analytics-card {
background: rgba(255, 255, 255, 0.6);
border-radius: 16px;
padding: 16px;
border: 1px solid rgba(255,255,255,0.5);
}
.analytics-card h4 {
margin: 0 0 12px 0;
color: var(--primary);
font-size: 16px;
}
.emotion-bar {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.emotion-label {
flex: 1;
font-size: 14px;
}
.emotion-value {
width: 40px;
text-align: right;
font-size: 14px;
font-weight: 500;
}
.emotion-progress {
flex: 2;
height: 6px;
background: var(--surface-variant);
border-radius: 3px;
margin: 0 8px;
overflow: hidden;
}
.emotion-progress-fill {
height: 100%;
background: var(--primary);
border-radius: 3px;
transition: width 0.3s ease;
}
/* Buttons - Alive */
.btn {
padding: 16px 32px;
@@ -440,19 +647,19 @@ body {
.btn-primary {
background: linear-gradient(45deg, var(--primary), var(--secondary));
color: white;
box-shadow: 0 8px 20px rgba(255, 107, 107, 0.4);
box-shadow: 0 8px 20px rgba(107, 144, 128, 0.4);
animation: breathBtn 3s infinite ease-in-out;
}
@keyframes breathBtn {
0% { transform: scale(1); box-shadow: 0 8px 20px rgba(255, 107, 107, 0.4); }
50% { transform: scale(1.02); box-shadow: 0 12px 24px rgba(255, 107, 107, 0.6); }
100% { transform: scale(1); box-shadow: 0 8px 20px rgba(255, 107, 107, 0.4); }
0% { transform: scale(1); box-shadow: 0 8px 20px rgba(107, 144, 128, 0.4); }
50% { transform: scale(1.02); box-shadow: 0 12px 24px rgba(107, 144, 128, 0.6); }
100% { transform: scale(1); box-shadow: 0 8px 20px rgba(107, 144, 128, 0.4); }
}
.btn-primary:hover {
transform: translateY(-4px) scale(1.05);
box-shadow: 0 15px 30px rgba(255, 107, 107, 0.5);
box-shadow: 0 15px 30px rgba(107, 144, 128, 0.5);
animation: none; /* Stop breathing on hover to focus */
}
@@ -490,20 +697,20 @@ body {
border-radius: 50%;
background: var(--primary);
cursor: pointer;
box-shadow: 0 4px 10px rgba(255, 107, 107, 0.4);
box-shadow: 0 4px 10px rgba(107, 144, 128, 0.4);
transition: all 0.3s ease;
}
.slider:hover::-webkit-slider-thumb {
transform: scale(1.2);
box-shadow: 0 6px 15px rgba(255, 107, 107, 0.6);
box-shadow: 0 6px 15px rgba(107, 144, 128, 0.6);
}
textarea, input[type="text"] {
width: 100%;
padding: 16px;
border-radius: 16px;
border: 2px solid rgba(0,0,0,0.1);
border: 2px solid rgba(0,0,0,0.05);
background: rgba(255, 255, 255, 0.9);
font-size: 16px;
font-family: 'Roboto', sans-serif;
@@ -513,7 +720,7 @@ textarea, input[type="text"] {
textarea:focus, input[type="text"]:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(255, 107, 107, 0.1);
box-shadow: 0 0 0 4px rgba(107, 144, 128, 0.1);
transform: scale(1.01);
}
@@ -607,7 +814,7 @@ textarea:focus, input[type="text"]:focus {
/* FAB - Alive */
.fab {
position: fixed;
bottom: 90px;
bottom: 110px; /* Increased from 90px */
right: 24px;
width: 64px;
height: 64px;
@@ -615,7 +822,7 @@ textarea:focus, input[type="text"]:focus {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border: none;
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.5);
box-shadow: 0 8px 25px rgba(107, 144, 128, 0.5);
cursor: pointer;
display: flex;
align-items: center;
@@ -626,7 +833,7 @@ textarea:focus, input[type="text"]:focus {
.fab:hover {
transform: scale(1.15) rotate(90deg);
box-shadow: 0 12px 35px rgba(255, 107, 107, 0.6);
box-shadow: 0 12px 35px rgba(107, 144, 128, 0.6);
}
.fab .material-icons {
@@ -667,8 +874,490 @@ textarea:focus, input[type="text"]:focus {
.nav-label { font-size: 10px; }
}
/* Exercise Modals */
.exercise-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
z-index: 2000;
animation: zoomIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.exercise-modal .card {
margin: 0;
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
border: 2px solid rgba(255, 255, 255, 0.8);
}
@keyframes zoomIn {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
.exercise-modal h3 {
color: var(--primary);
margin-bottom: 24px;
font-size: 24px;
}
.exercise-actions {
display: flex;
gap: 12px;
margin-top: 24px;
justify-content: flex-end;
}
/* Desktop Sidebar */
@media (min-width: 1024px) {
.bottom-nav { display: none; }
/* ... (keep existing desktop styles if needed, but adapt to new look) ... */
.bottom-nav {
display: flex;
flex-direction: column;
top: 80px;
left: 20px;
right: auto;
bottom: 20px;
width: 100px;
height: auto;
border-radius: 20px;
justify-content: flex-start;
padding-top: 40px;
gap: 20px;
background: rgba(255, 255, 255, 0.85);
}
.main-content {
margin-left: 120px;
max-width: calc(100% - 140px);
}
.nav-item.active::after {
left: 0;
top: 50%;
transform: translateY(-50%);
bottom: auto;
width: 4px;
height: 20px;
border-radius: 4px;
}
}
/* History Lists */
.history-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.history-item {
background: rgba(255, 255, 255, 0.5);
border-radius: 12px;
padding: 16px;
transition: all 0.3s ease;
}
.history-item:hover {
background: rgba(255, 255, 255, 0.8);
transform: translateX(5px);
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.history-type {
font-weight: bold;
font-size: 16px;
}
.history-date {
font-size: 12px;
color: var(--on-surface-variant);
}
.history-details {
font-size: 14px;
color: var(--on-surface);
}
.history-notes {
font-style: italic;
color: var(--on-surface-variant);
margin-top: 4px;
}
.empty-state {
text-align: center;
padding: 40px;
color: var(--on-surface-variant);
font-size: 18px;
}
.error-state {
text-align: center;
padding: 20px;
color: var(--error);
background: rgba(255, 82, 82, 0.1);
border-radius: 12px;
}
.btn-sm {
padding: 8px 16px;
font-size: 14px;
}
/* Loading Spinner */
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0,0,0,0.1);
border-left-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Auth Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
display: flex;
justify-content: center;
align-items: center;
z-index: 3000;
animation: fadeIn 0.3s ease;
}
.modal-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
padding: 40px;
border-radius: 30px;
width: 90%;
max-width: 400px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.8);
text-align: center;
animation: slideInUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
/* Animated gradient border top */
.modal-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(90deg, var(--primary), var(--secondary), var(--tertiary));
animation: gradientFlow 3s linear infinite;
background-size: 200% 100%;
}
@keyframes gradientFlow {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
.auth-title {
font-size: 32px;
font-weight: 800;
margin-bottom: 30px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: pulseText 3s infinite ease-in-out;
}
@keyframes pulseText {
0%, 100% { transform: scale(1); filter: brightness(100%); }
50% { transform: scale(1.05); filter: brightness(110%); }
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--on-surface-variant);
font-weight: 500;
font-size: 14px;
margin-left: 4px;
}
.form-input {
width: 100%;
padding: 16px;
border-radius: 16px;
border: 2px solid rgba(0,0,0,0.05);
background: rgba(255, 255, 255, 0.5);
font-size: 16px;
transition: all 0.3s ease;
}
.form-input:focus {
background: white;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(107, 144, 128, 0.1);
transform: translateY(-2px);
}
.switch-form {
margin-top: 24px;
font-size: 14px;
color: var(--on-surface-variant);
}
.switch-form a {
color: var(--primary);
text-decoration: none;
font-weight: 700;
margin-left: 4px;
position: relative;
}
.switch-form a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: var(--primary);
transform: scaleX(0);
transition: transform 0.3s ease;
transform-origin: right;
}
.switch-form a:hover::after {
transform: scaleX(1);
transform-origin: left;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Smart Breathing Enhanced Styles */
.breathing-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at center, #1a2a6c, #b21f1f, #fdbb2d); /* Deep calming gradient */
background: radial-gradient(circle at center, #2b5876, #4e4376); /* Deep calming blue/purple */
z-index: 5000;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
animation: fadeIn 0.5s ease;
}
.breathing-visual-container {
position: relative;
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 40px;
}
.breath-circle-main {
width: 200px;
height: 200px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 0 40px rgba(255, 255, 255, 0.2);
position: absolute;
transition: transform 0.1s linear, background-color 0.5s ease;
}
.breath-circle-inner {
width: 150px;
height: 150px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.8);
position: absolute;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
transition: transform 0.1s linear;
}
.breath-particles {
position: absolute;
width: 100%;
height: 100%;
animation: rotateParticles 20s linear infinite;
}
.breath-instruction-text {
font-size: 36px;
font-weight: 300;
letter-spacing: 2px;
text-align: center;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
margin-bottom: 10px;
min-height: 50px;
}
.breath-sub-text {
font-size: 18px;
opacity: 0.8;
margin-bottom: 40px;
}
.technique-selector {
display: flex;
gap: 12px;
margin-bottom: 30px;
overflow-x: auto;
padding: 10px;
max-width: 100%;
}
.technique-btn {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 12px 20px;
border-radius: 30px;
color: white;
cursor: pointer;
transition: all 0.3s;
white-space: nowrap;
}
.technique-btn.active {
background: white;
color: #4e4376;
font-weight: bold;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.breath-controls {
display: flex;
gap: 20px;
}
.control-btn-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
}
.control-btn-icon:hover {
background: rgba(255, 255, 255, 0.4);
transform: scale(1.1);
}
@keyframes rotateParticles {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Specific States */
.inhale .breath-circle-main { background: rgba(100, 255, 218, 0.3); }
.hold .breath-circle-main { background: rgba(255, 235, 59, 0.3); }
.exhale .breath-circle-main { background: rgba(255, 107, 107, 0.3); }
/* RTL Support Overrides */
html[dir="rtl"] {
text-align: right;
--direction: rtl;
}
html[dir="rtl"] .fab {
right: auto;
left: 24px;
}
html[dir="rtl"] .notification-badge {
right: auto;
left: 8px;
}
html[dir="rtl"] .notification-dropdown {
right: auto;
left: 0;
}
html[dir="rtl"] .card-title::after {
transform-origin: right;
left: auto;
right: 0;
}
html[dir="rtl"] .switch-form a {
margin-left: 0;
margin-right: 4px;
}
html[dir="rtl"] .form-group label {
margin-left: 0;
margin-right: 4px;
}
html[dir="rtl"] .guided-controls {
right: auto;
left: 20px;
}
/* Adjust history item hover transform for RTL */
html[dir="rtl"] .history-item:hover {
transform: translateX(-5px);
}
/* Adjust emotion value alignment */
html[dir="rtl"] .emotion-value {
text-align: left;
}
/* Adjust desktop sidebar for RTL */
@media (min-width: 1024px) {
html[dir="rtl"] .bottom-nav {
left: auto;
right: 20px;
}
html[dir="rtl"] .main-content {
margin-left: 0;
margin-right: 120px;
}
html[dir="rtl"] .nav-item.active::after {
left: auto;
right: 0;
}
}

View File

@@ -0,0 +1,504 @@
export const translations = {
en: {
// Meta
"app_title": "MindShift",
"app_subtitle": "Your personal CBT companion",
"init_loading": "Initializing MindShift...",
"init_error": "Error Loading App",
"proactive_badge": "✨ Time for a vibe check?",
// Navigation
"nav_home": "Home",
"nav_mood": "Mood",
"nav_thoughts": "Thoughts",
"nav_gratitude": "Gratitude",
"nav_progress": "Progress",
// Auth
"auth_welcome": "Welcome to MindShift",
"auth_login": "Login",
"auth_register": "Register",
"auth_name": "Name",
"auth_email": "Email",
"auth_password": "Password",
"auth_no_account": "Don't have an account?",
"auth_has_account": "Already have an account?",
"auth_login_failed": "Login failed",
"auth_reg_failed": "Registration failed",
// Home
"home_welcome": "Welcome Back! 🌟",
"home_subtitle": "Ready to shift your mind?",
"home_log_mood": "Log Mood",
"home_record_thought": "Record Thought",
"home_gratitude": "Gratitude",
"home_daily_vibe": "Daily Vibe Check 📊",
"home_quick_relief": "Quick Relief 🌿",
"home_breathe": "Breathe",
"home_relax": "Relax",
"home_stats_mood": "Today's Mood",
"home_stats_sessions": "Sessions",
"home_stats_avg": "Weekly Avg",
"home_stats_gratitude": "Gratitude",
"home_retry": "Tap to Retry",
// Mood
"mood_title": "How are you feeling?",
"mood_joy": "Joy",
"mood_peace": "Peace",
"mood_energy": "Energy",
"mood_anxiety": "Anxiety",
"mood_sadness": "Sadness",
"mood_anger": "Anger",
"mood_intensity": "Intensity",
"mood_notes_placeholder": "Any thoughts?",
"mood_save": "Save Mood",
"mood_saved_success": "Mood tracked successfully!",
"mood_select_warning": "Please select a mood",
// Thoughts
"thought_title": "Thought Record 🧠",
"thought_situation": "Situation (Who, what, where, when?)",
"thought_automatic": "Automatic Thoughts (What went through your mind?)",
"thought_emotions": "Emotions",
"thought_add_emotion": "+ Add Emotion",
"thought_evidence": "Evidence For/Against",
"thought_alternative": "Alternative Thought",
"thought_save": "Save Record",
"thought_saved_success": "Thought record saved successfully!",
"thought_fill_warning": "Please fill in at least the situation and thoughts",
// Gratitude
"gratitude_title": "Gratitude Journal 🙏",
"gratitude_intro": "List 3 things you are grateful for today:",
"gratitude_placeholder": "I am grateful for...",
"gratitude_add": "+ Add Another",
"gratitude_save": "Save Entry",
"gratitude_saved_success": "Gratitude entries saved successfully!",
"gratitude_empty_warning": "Please add at least one gratitude entry",
// Progress & History
"progress_title": "Your Progress 📈",
"progress_weekly": "Weekly Mood 📅",
"progress_history": "Recent History 📜",
"history_tab_moods": "Moods",
"history_tab_thoughts": "Thoughts",
"history_tab_gratitude": "Gratitude",
"history_empty_mood": "No mood entries yet. Start tracking! 📝",
"history_empty_thoughts": "No thought records yet. 🧠",
"history_empty_gratitude": "No gratitude entries yet. 🙏",
"history_select_prompt": "Select a category to view history",
// Quick Actions
"quick_title": "Quick Actions ⚡",
"quick_relax_now": "Relax Now",
"close": "Close",
// Guided Relaxation
"guided_title": "Mindfulness Session",
"guided_select_mode": "Select a Session",
"mode_grounding": "Grounding (5-4-3-2-1)",
"mode_body_scan": "Body Scan",
"mode_visualization": "Visualization",
"guided_pre_stress": "How stressed are you now?",
"guided_post_stress": "How do you feel now?",
"guided_start": "Start Session",
"guided_settings": "Settings",
"guided_ambience": "Background Sounds",
"guided_voice": "Voice Guide",
"guided_summary_title": "Session Complete",
"guided_summary_reduced": "Stress Reduced by",
"guided_summary_maintained": "You maintained your calm.",
// Grounding
"guided_sight_title": "Sight",
"guided_sight_instruction": "Look around you.",
"guided_sight_sub": "Find 5 things you can see.",
"guided_touch_title": "Touch",
"guided_touch_instruction": "Feel the textures.",
"guided_touch_sub": "Find 4 things you can touch.",
"guided_sound_title": "Sound",
"guided_sound_instruction": "Listen carefully.",
"guided_sound_sub": "Identify 3 sounds you hear.",
"guided_smell_title": "Smell",
"guided_smell_instruction": "Breathe in deep.",
"guided_smell_sub": "Notice 2 things you can smell.",
"guided_taste_title": "Taste",
"guided_taste_instruction": "Focus on your mouth.",
"guided_taste_sub": "Find 1 thing you can taste.",
"guided_found_btn": "I found one",
"guided_complete_title": "You did great!",
"guided_complete_sub": "Feeling more grounded?",
"guided_complete_btn": "Complete",
// Body Scan Script
"scan_intro": "Find a comfortable position and close your eyes. Let's begin by taking a few deep breaths.",
"scan_feet": "Bring your awareness to your feet. Notice any sensations of warmth, coolness, or pressure. Let them soften.",
"scan_legs": "Move your attention up to your calves and thighs. If you notice any tension, imagine it melting away with your exhale.",
"scan_stomach": "Focus on your belly. Feel it rise gently as you inhale, and fall as you exhale. Soften your stomach muscles.",
"scan_chest": "Bring your attention to your chest and heart center. Notice the rhythm of your breath. Let your shoulders drop down away from your ears.",
"scan_shoulders": "Notice your neck and throat. Let go of any tightness here. Allow your jaw to unhinge slightly.",
"scan_face": "Soften the muscles around your eyes and forehead. Let your entire face be smooth and relaxed.",
"scan_outro": "Take a moment to feel your whole body, resting in this state of relaxation. When you are ready, gently wiggle your fingers and toes, and open your eyes.",
// Visualization Script
"vis_intro": "Close your eyes and take a deep breath. We are going to take a journey to a peaceful place.",
"vis_step1": "Imagine you are standing at the edge of a lush, ancient forest. The trees are tall and protective. You feel safe here.",
"vis_step2": "As you walk deeper into the woods, the air becomes cool and fresh. You can smell the scent of pine and damp earth.",
"vis_step3": "Sunlight filters through the canopy above, creating dappled patterns of light on the soft mossy path beneath your feet.",
"vis_step4": "In the distance, you hear the gentle sound of a stream flowing over smooth stones. The sound is rhythmic and calming.",
"vis_outro": "Take a moment to absorb the peace of this place. Know that you can return here anytime. Slowly bring your awareness back to the room and open your eyes.",
// Smart Breathing
"breath_balance": "Balance",
"breath_relax": "Relax",
"breath_focus": "Focus",
"breath_in": "Breathe In",
"breath_out": "Breathe Out",
"breath_hold": "Hold",
"breath_ready": "Get Ready...",
"breath_sit": "Sit comfortably",
"breath_complete": "Breathing session complete! 🌬️",
// Notifications
"notifications_empty": "No notifications",
"just_now": "Just now",
"ago_m": "m ago",
"ago_h": "h ago"
},
ru: {
// Meta
"app_title": "MindShift",
"app_subtitle": "Ваш личный помощник КПТ",
"init_loading": "Загрузка MindShift...",
"init_error": "Ошибка загрузки",
"proactive_badge": "✨ Время проверить настроение?",
// Navigation
"nav_home": "Главная",
"nav_mood": "Настроение",
"nav_thoughts": "Мысли",
"nav_gratitude": "Благодарность",
"nav_progress": "Прогресс",
// Auth
"auth_welcome": "Добро пожаловать в MindShift",
"auth_login": "Войти",
"auth_register": "Регистрация",
"auth_name": "Имя",
"auth_email": "Email",
"auth_password": "Пароль",
"auth_no_account": "Нет аккаунта?",
"auth_has_account": "Уже есть аккаунт?",
"auth_login_failed": "Ошибка входа",
"auth_reg_failed": "Ошибка регистрации",
// Home
"home_welcome": "С возвращением! 🌟",
"home_subtitle": "Готовы изменить мышление?",
"home_log_mood": "Настроение",
"home_record_thought": "Запись мыслей",
"home_gratitude": "Благодарность",
"home_daily_vibe": "Статистика дня 📊",
"home_quick_relief": "Быстрая помощь 🌿",
"home_breathe": "Дыхание",
"home_relax": "Релакс",
"home_stats_mood": "Настроение",
"home_stats_sessions": "Сессии",
"home_stats_avg": "Ср. за неделю",
"home_stats_gratitude": "Благодарности",
"home_retry": "Повторить",
// Mood
"mood_title": "Как вы себя чувствуете?",
"mood_joy": "Радость",
"mood_peace": "Покой",
"mood_energy": "Энергия",
"mood_anxiety": "Тревога",
"mood_sadness": "Грусть",
"mood_anger": "Гнев",
"mood_intensity": "Интенсивность",
"mood_notes_placeholder": "О чем думаете?",
"mood_save": "Сохранить",
"mood_saved_success": "Настроение сохранено!",
"mood_select_warning": "Выберите настроение",
// Thoughts
"thought_title": "Дневник мыслей 🧠",
"thought_situation": "Ситуация (Кто, что, где, когда?)",
"thought_automatic": "Автоматические мысли (Что пришло в голову?)",
"thought_emotions": "Эмоции",
"thought_add_emotion": "+ Добавить эмоцию",
"thought_evidence": "За и Против",
"thought_alternative": "Альтернативная мысль",
"thought_save": "Сохранить запись",
"thought_saved_success": "Запись сохранена!",
"thought_fill_warning": "Заполните ситуацию и мысли",
// Gratitude
"gratitude_title": "Дневник благодарности 🙏",
"gratitude_intro": "3 вещи, за которые вы благодарны:",
"gratitude_placeholder": "Я благодарен за...",
"gratitude_add": "+ Добавить еще",
"gratitude_save": "Сохранить",
"gratitude_saved_success": "Записи сохранены!",
"gratitude_empty_warning": "Добавьте хотя бы одну запись",
// Progress & History
"progress_title": "Ваш прогресс 📈",
"progress_weekly": "Настроение за неделю 📅",
"progress_history": "История 📜",
"history_tab_moods": "Настроение",
"history_tab_thoughts": "Мысли",
"history_tab_gratitude": "Благодарность",
"history_empty_mood": "Нет записей. Начните отслеживать! 📝",
"history_empty_thoughts": "Нет записей мыслей. 🧠",
"history_empty_gratitude": "Нет записей благодарности. 🙏",
"history_select_prompt": "Выберите категорию для просмотра",
// Quick Actions
"quick_title": "Быстрые действия ⚡",
"quick_relax_now": "Релакс сейчас",
"close": "Закрыть",
// Guided Relaxation
"guided_title": "Сессия осознанности",
"guided_select_mode": "Выберите сессию",
"mode_grounding": "Заземление (5-4-3-2-1)",
"mode_body_scan": "Сканирование тела",
"mode_visualization": "Визуализация",
"guided_pre_stress": "Ваш уровень стресса?",
"guided_post_stress": "Как вы себя чувствуете?",
"guided_start": "Начать сессию",
"guided_settings": "Настройки",
"guided_ambience": "Фоновые звуки",
"guided_voice": "Голос гида",
"guided_summary_title": "Сессия завершена",
"guided_summary_reduced": "Стресс снижен на",
"guided_summary_maintained": "Вы сохранили спокойствие.",
"guided_sight_title": "Зрение",
"guided_sight_instruction": "Оглянитесь вокруг.",
"guided_sight_sub": "Найдите 5 вещей, которые вы видите.",
"guided_touch_title": "Осязание",
"guided_touch_instruction": "Почувствуйте текстуры.",
"guided_touch_sub": "Найдите 4 вещи, которые можно потрогать.",
"guided_sound_title": "Слух",
"guided_sound_instruction": "Прислушайтесь.",
"guided_sound_sub": "Найдите 3 звука, которые вы слышите.",
"guided_smell_title": "Обоняние",
"guided_smell_instruction": "Сделайте глубокий вдох.",
"guided_smell_sub": "Найдите 2 запаха.",
"guided_taste_title": "Вкус",
"guided_taste_instruction": "Сосредоточьтесь на вкусе.",
"guided_taste_sub": "Найдите 1 вещь, которую можно попробовать.",
"guided_found_btn": "Найдено",
"guided_complete_title": "Отлично!",
"guided_complete_sub": "Чувствуете себя спокойнее?",
"guided_complete_btn": "Завершить",
// Body Scan Script
"scan_intro": "Примите удобное положение и закройте глаза. Давайте начнем с нескольких глубоких вдохов.",
"scan_feet": "Сосредоточьтесь на ногах. Почувствуйте их вес, тепло или холод. Позвольте им расслабиться.",
"scan_legs": "Переведите внимание на голени и бедра. Если есть напряжение, представьте, как оно уходит с выдохом.",
"scan_stomach": "Заметьте свой живот. Почувствуйте, как он мягко поднимается при вдохе и опускается при выдохе.",
"scan_chest": "Перенесите внимание на грудь и сердце. Заметьте ритм дыхания. Опустите плечи.",
"scan_shoulders": "Заметьте шею и горло. Отпустите любое напряжение. Слегка разомкните челюсти.",
"scan_face": "Расслабьте мышцы вокруг глаз и лба. Пусть все лицо станет гладким и спокойным.",
"scan_outro": "Почувствуйте все свое тело в состоянии покоя. Когда будете готовы, пошевелите пальцами и откройте глаза.",
// Visualization Script
"vis_intro": "Закройте глаза и сделайте глубокий вдох. Мы отправимся в путешествие в спокойное место.",
"vis_step1": "Представьте, что вы стоите на краю тихого древнего леса. Деревья высокие и защищают вас. Здесь безопасно.",
"vis_step2": "Вы идете вглубь леса, воздух становится прохладным и свежим. Вы чувствуете запах хвои и влажной земли.",
"vis_step3": "Солнечный свет пробивается сквозь листву, создавая узоры света на мягкой моховой тропинке под ногами.",
"vis_step4": "Вдали вы слышите тихое журчание ручья, текущего по гладким камням. Этот звук ритмичный и успокаивающий.",
"vis_outro": "Впитайте покой этого места. Знайте, что можете вернуться сюда в любой момент. Медленно откройте глаза.",
// Smart Breathing
"breath_balance": "Баланс",
"breath_relax": "Релакс",
"breath_focus": "Фокус",
"breath_in": "Вдох",
"breath_out": "Выдох",
"breath_hold": "Пауза",
"breath_ready": "Приготовьтесь...",
"breath_sit": "Сядьте удобно",
"breath_complete": "Сессия завершена! 🌬️",
// Notifications
"notifications_empty": "Нет уведомлений",
"just_now": "Только что",
"ago_m": "м назад",
"ago_h": "ч назад"
},
he: {
// Meta
"app_title": "MindShift",
"app_subtitle": "המאמן האישי שלך ל-CBT",
"init_loading": "טוען MindShift...",
"init_error": "שגיאה בטעינת האפליקציה",
"proactive_badge": "✨ זמן לבדיקת מצב רוח?",
// Navigation
"nav_home": "בית",
"nav_mood": "מצב רוח",
"nav_thoughts": "מחשבות",
"nav_gratitude": "הוקרת תודה",
"nav_progress": "התקדמות",
// Auth
"auth_welcome": "ברוכים הבאים ל-MindShift",
"auth_login": "התחברות",
"auth_register": "הרשמה",
"auth_name": "שם",
"auth_email": "אימייל",
"auth_password": "סיסמה",
"auth_no_account": "אין לך חשבון?",
"auth_has_account": "יש לך כבר חשבון?",
"auth_login_failed": "התחברות נכשלה",
"auth_reg_failed": "הרשמה נכשלה",
// Home
"home_welcome": "ברוכים השבים! 🌟",
"home_subtitle": "מוכנים לשינוי מחשבתי?",
"home_log_mood": "יומן מצב רוח",
"home_record_thought": "יומן מחשבות",
"home_gratitude": "הוקרת תודה",
"home_daily_vibe": "בדיקה יומית 📊",
"home_quick_relief": "הקלה מהירה 🌿",
"home_breathe": "נשימה",
"home_relax": "הרפיה",
"home_stats_mood": "מצב רוח",
"home_stats_sessions": "אימונים",
"home_stats_avg": "ממוצע שבועי",
"home_stats_gratitude": "תודות",
"home_retry": "לחץ לנסות שוב",
// Mood
"mood_title": "איך אתם מרגישים?",
"mood_joy": "שמחה",
"mood_peace": "שלווה",
"mood_energy": "אנרגיה",
"mood_anxiety": "חרדה",
"mood_sadness": "עצב",
"mood_anger": "כעס",
"mood_intensity": "עוצמה",
"mood_notes_placeholder": "מחשבות נוספות?",
"mood_save": "שמור מצב רוח",
"mood_saved_success": "מצב רוח נשמר בהצלחה!",
"mood_select_warning": "אנא בחר מצב רוח",
// Thoughts
"thought_title": "יומן מחשבות 🧠",
"thought_situation": "סיטואציה (מי, מה, איפה, מתי?)",
"thought_automatic": "מחשבות אוטומטיות (מה עבר בראש?)",
"thought_emotions": "רגשות",
"thought_add_emotion": "+ הוסף רגש",
"thought_evidence": "בעד ונגד",
"thought_alternative": "מחשבה אלטרנטיבית",
"thought_save": "שמור רשומה",
"thought_saved_success": "הרשומה נשמרה בהצלחה!",
"thought_fill_warning": "אנא מלא לפחות את הסיטואציה והמחשבות",
// Gratitude
"gratitude_title": "יומן הוקרת תודה 🙏",
"gratitude_intro": "3 דברים שאתם מוקירים עליהם תודה היום:",
"gratitude_placeholder": "אני מוקיר תודה על...",
"gratitude_add": "+ הוסף עוד",
"gratitude_save": "שמור",
"gratitude_saved_success": "נשמר בהצלחה!",
"gratitude_empty_warning": "אנא הוסף לפחות פריט אחד",
// Progress & History
"progress_title": "ההתקדמות שלך 📈",
"progress_weekly": "מצב רוח שבועי 📅",
"progress_history": "היסטוריה 📜",
"history_tab_moods": "מצבי רוח",
"history_tab_thoughts": "מחשבות",
"history_tab_gratitude": "תודות",
"history_empty_mood": "אין עדיין רשומות. התחילו לתעד! 📝",
"history_empty_thoughts": "אין עדיין רשומות מחשבה. 🧠",
"history_empty_gratitude": "אין עדיין רשומות תודה. 🙏",
"history_select_prompt": "בחר קטגוריה לצפייה בהיסטוריה",
// Quick Actions
"quick_title": "פעולות מהירות ⚡",
"quick_relax_now": "הרפיה עכשיו",
"close": "סגור",
// Guided Relaxation
"guided_title": "אימון קשיבות",
"guided_select_mode": "בחר אימון",
"mode_grounding": "קרקוע (5-4-3-2-1)",
"mode_body_scan": "סריקת גוף",
"mode_visualization": "דמיון מודרך",
"guided_pre_stress": "כמה אתם לחוצים?",
"guided_post_stress": "איך אתם מרגישים כעת?",
"guided_start": "התחל אימון",
"guided_settings": "הגדרות",
"guided_ambience": "צלילי רקע",
"guided_voice": "קול מנחה",
"guided_summary_title": "האימון הושלם",
"guided_summary_reduced": "הלחץ ירד ב-",
"guided_summary_maintained": "שמרתם על רוגע.",
"guided_sight_title": "ראייה",
"guided_sight_instruction": "הביטו סביבכם.",
"guided_sight_sub": "מצאו 5 דברים שאתם רואים.",
"guided_touch_title": "מגע",
"guided_touch_instruction": "הרגישו מרקמים.",
"guided_touch_sub": "מצאו 4 דברים שאפשר לגעת בהם.",
"guided_sound_title": "שמיעה",
"guided_sound_instruction": "הקשיבו בתשומת לב.",
"guided_sound_sub": "זהו 3 צלילים שאתם שומעים.",
"guided_smell_title": "ריח",
"guided_smell_instruction": "קחו נשימה עמוקה.",
"guided_smell_sub": "שימו לב ל-2 ריחות.",
"guided_taste_title": "טעם",
"guided_taste_instruction": "התמקדו בפה.",
"guided_taste_sub": "מצאו דבר 1 שניתן לטעום.",
"guided_found_btn": "מצאתי",
"guided_complete_title": "כל הכבוד!",
"guided_complete_sub": "מרגישים מקורקעים יותר?",
"guided_complete_btn": "סיים",
// Body Scan Script
"scan_intro": "מצאו תנוחה נוחה ועצמו עיניים. נתחיל בכמה נשימות עמוקות.",
"scan_feet": "התמקדו בכפות הרגליים. הרגישו את המשקל שלהן, חום או קור. תנו להן להתרכך.",
"scan_legs": "העבירו את תשומת הלב לשוקיים ולירכיים. אם יש מתח, דמיינו אותו נמס עם הנשיפה.",
"scan_stomach": "שימו לב לבטן. הרגישו אותה עולה בעדינות בשאיפה ויורדת בנשיפה. הרפו את שרירי הבטן.",
"scan_chest": "העבירו את תשומת הלב לחזה וללב. שימו לב לקצב הנשימה. שחררו את הכתפיים מטה.",
"scan_shoulders": "שימו לב לצוואר ולגרון. שחררו כל מתח. אפשרו ללסת להשתחרר מעט.",
"scan_face": "הרפו את השרירים סביב העיניים והמצח. תנו לכל הפנים להיות חלקים ורגועים.",
"scan_outro": "קחו רגע להרגיש את כל הגוף במצב של רוגע. כשאתם מוכנים, הניעו בעדינות את האצבעות ופקחו עיניים.",
// Visualization Script
"vis_intro": "עצמו עיניים וקחו נשימה עמוקה. אנו יוצאים למסע למקום שליו.",
"vis_step1": "דמיינו שאתם עומדים בקצה של יער עתיק ושקט. העצים גבוהים ומגנים. אתם מרגישים בטוחים כאן.",
"vis_step2": "כשאתם הולכים עמוק יותר לתוך היער, האוויר נעשה קריר ורענן. אתם יכולים להריח ריח של אורנים ואדמה לחה.",
"vis_step3": "אור השמש מסתנן מבעד לחופה מעל, יוצר תבניות של אור על שביל הטחב הרך שמתחת לרגליכם.",
"vis_step4": "במרחק, אתם שומעים צליל עדין של פלג מים הזורם על אבנים חלקות. הצליל קצבי ומרגיע.",
"vis_outro": "ספגו את השלווה של המקום הזה. דעו שאתם יכולים לחזור לכאן בכל עת. החזירו את המודעות לחדר ופקחו עיניים.",
// Smart Breathing
"breath_balance": "איזון",
"breath_relax": "רוגע",
"breath_focus": "מיקוד",
"breath_in": "שאיפה",
"breath_out": "נשיפה",
"breath_hold": "החזקה",
"breath_ready": "היכונו...",
"breath_sit": "שבו בנוחות",
"breath_complete": "אימון נשימה הסתיים! 🌬️",
// Notifications
"notifications_empty": "אין התראות",
"just_now": "כרגע",
"ago_m": "לפני דק'",
"ago_h": "לפני שע'"
}
};

View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,318 @@
# 🚀 NanoJason - Universal AI Prompt Language Translator & Optimizer
<div align="center">
![NanoJason](https://img.shields.io/badge/NanoJason-AI%20Prompt%20Optimizer-blue?style=for-the-badge&logo=sparkles)
![Version](https://img.shields.io/badge/Version-3.0-green?style=for-the-badge)
![License](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)
![Offline](https://img.shields.io/badge/Status-Offline%20Ready-purple?style=for-the-badge)
</div>
<p align="center">
<strong>NanoJason</strong> is a revolutionary, offline-only prompt engineering tool that transforms natural language descriptions into optimized Jason format for maximum AI accuracy across all image generation services.
</p>
<p align="center">
<a href="#features">🌟 Features</a> •
<a href="#getting-started">🚀 Getting Started</a> •
<a href="#usage">💡 Usage</a> •
<a href="#contributing">🤝 Contributing</a> •
<a href="#license">📜 License</a>
</p>
---
## 🌟 **Amazing Features**
### 🎯 **Three Powerful Modes**
#### 1. **Jason Translator**
- Converts natural language to universal Jason format
- Style, mood, character, and item detection
- Extracts color palette, composition, and lighting
- Perfect for AI image generation compatibility
#### 2. **NanoPrompt Optimizer**
- Deep reverse engineering for maximum AI accuracy
- Pattern mapping and keyword enhancement
- Quality descriptors and composition optimization
- Lighting and color enhancement
- Error prevention and artifact reduction
- **Accuracy Score:** Up to 95%
#### 3. **NanoCoder Technical Optimizer**
- Specialized for coding and engineering prompts
- Technical domain detection (programming, web, mobile, AI, etc.)
- Framework and language pattern recognition
- Architecture and security optimization
- Code context extraction
- **Technical Accuracy:** Up to 98%
### 🔥 **Key Benefits**
- **🚀 Offline-Only** - No internet connection required
- **⚡ Instant Results** - Real-time translation and optimization
- **🎨 Universal Compatibility** - Works with all AI image generators
- **🔒 Privacy Focused** - No data leaves your device
- **📱 Responsive Design** - Perfect for desktop and mobile
- **🎯 High Accuracy** - Advanced pattern recognition and optimization
- **🔄 Export Options** - Copy, download, and share optimized prompts
---
## 🚀 **Getting Started**
### Prerequisites
- Node.js 18+
- npm or pnpm
### Installation
1. **Clone the repository**
```bash
git clone https://github.com/roman-ryzenadvanced/NanoJason.git
cd NanoJason
```
2. **Install dependencies**
```bash
npm install
# or
pnpm install
```
3. **Run the application**
```bash
npm run dev
# or
pnpm dev
```
4. **Open your browser**
```
http://localhost:3000
```
---
## 💡 **Usage Examples**
### **Jason Translator Mode**
```text
Input: "A magical forest with glowing mushrooms and fairies"
Output:
{
"prompt": "A magical forest with glowing mushrooms and fairies",
"style": "fantasy",
"mood": "peaceful",
"characters": ["fairies"],
"items": ["mushrooms", "forest"],
"setting": "forest",
"action": undefined,
"color_palette": ["glowing", "magical"],
"composition": undefined,
"lighting": "glowing"
}
```
### **NanoPrompt Optimizer Mode**
```text
Input: "A beautiful landscape"
Output:
- **Original:** "A beautiful landscape"
- **Optimized:** "A beautiful landscape, breathtakingly beautiful, ultra-detailed, photorealistic, perfect composition, dramatic lighting, high resolution, no artifacts"
- **Accuracy Score:** 85%
```
### **NanoCoder Technical Mode**
```text
Input: "React web application with TypeScript"
Output:
- **Original:** "React web application with TypeScript"
- **Technical:** "React web application with TypeScript, well-structured and maintainable, clean code, DRY principle, SOLID principles, responsive and accessible web application, technical excellence"
- **Technical Accuracy:** 92%
```
---
## 🏗️ **Architecture**
### **Core Components**
#### **JasonTranslator** (`src/services/jason-translator.ts`)
- Natural language to Jason format conversion
- Pattern recognition for style, mood, characters, items
- Setting, action, color palette extraction
- Composition and lighting detection
#### **NanoPrompt** (`src/services/nano-prompt.ts`)
- Deep reverse engineering optimization
- Pattern mapping and keyword enhancement
- Quality descriptors and technical specifications
- Performance optimization and error prevention
#### **NanoCoder** (`src/services/nano-coder.ts`)
- Technical prompt optimization
- Programming language and framework detection
- Architecture and security enhancement
- Code context extraction
---
## 🎨 **UI Components**
### **Tabbed Interface**
- **Jason Translator** - Blue theme for basic translation
- **NanoPrompt** - Purple theme for AI optimization
- **NanoCoder** - Green theme for technical optimization
### **Interactive Features**
- Quick templates for each mode
- Real-time processing with loading indicators
- Copy/download functionality for results
- Accuracy score display
- Performance tips and optimization details
---
## 🛠️ **Development**
### **Project Structure**
```
src/
├── app/
│ ├── page.tsx # Main application page
│ ├── layout.tsx # Root layout
│ └── globals.css # Global styles
├── components/
│ ├── layout/
│ │ ├── header.tsx # Navigation header
│ │ └── footer.tsx # Footer section
│ └── ui/
│ ├── button.tsx # Button component
│ ├── card.tsx # Card component
│ └── input.tsx # Input component
└── services/
├── jason-translator.ts # Jason translation service
├── nano-prompt.ts # NanoPrompt optimization
├── nano-coder.ts # NanoCoder technical optimization
└── api-utils.ts # API utilities
```
### **Technologies Used**
- **Next.js** - React framework with App Router
- **TypeScript** - Type-safe development
- **Tailwind CSS** - Modern utility-first styling
- **Lucide React** - Beautiful icon library
### **Build & Development**
```bash
# Development server
npm run dev
# Build for production
npm run build
# Start production server
npm start
# Linting
npm run lint
```
---
## 🤝 **Contributing**
We welcome contributions! Here's how you can help:
1. **Fork the repository**
2. **Create a feature branch** (`git checkout -b feature/amazing-feature`)
3. **Make your changes**
4. **Commit your changes** (`git commit -m 'Add some amazing feature'`)
5. **Push to the branch** (`git push origin feature/amazing-feature`)
6. **Open a Pull Request**
### **Development Guidelines**
- Follow TypeScript best practices
- Use Tailwind CSS for styling
- Write clean, readable code
- Add comments for complex logic
- Test your changes thoroughly
---
## 📜 **License**
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
---
## 👨‍💻 **About the Developer**
**NanoJason** is developed by **Roman | RyzenAdvanced**
- **GitHub:** [roman-ryzenadvanced](https://github.com/roman-ryzenadvanced)
- **Project Hub:** [Custom Engineered Agents and Tools for Vibe Coders](https://github.com/roman-ryzenadvanced/Custom-Engineered-Agents-and-Tools-for-Vibe-Coders)
- **Tools:** [GLM 4.6 Coding Model](https://z.ai/subscribe?ic=R0K78RJKNW)
- **Built with:** [TRAE IDE](https://www.trae.ai/s/WJtxyE)
### **Special Thanks**
- **GLM 4.6** - Advanced coding model for AI assistance
- **TRAE IDE** - Amazing development environment
- **Open Source Community** - For inspiration and collaboration
---
## 🚀 **Deployment**
### **Vercel (Recommended)**
```bash
# Deploy to Vercel
npm i -g vercel
vercel
```
### **GitHub Pages**
```bash
# Build and deploy
npm run build
npm run export
```
### **Other Platforms**
- Netlify
- Railway
- DigitalOcean App Platform
- AWS Amplify
---
## 🌟 **Show Your Support**
If you find NanoJason useful, please consider:
-**Star the repository** on GitHub
- 🐛 **Report bugs** or suggest features
- 📢 **Share** with fellow developers
- 💡 **Contribute** to make it even better
---
## 📞 **Contact**
- **Issues:** [GitHub Issues](https://github.com/roman-ryzenadvanced/NanoJason/issues)
- **Discussions:** [GitHub Discussions](https://github.com/roman-ryzenadvanced/NanoJason/discussions)
- **Email:** [Create an issue](https://github.com/roman-ryzenadvanced/NanoJason/issues) for contact
---
<div align="center">
<strong>Made with ❤️ by Roman | RyzenAdvanced</strong>
[🚀 Deploy](https://vercel.com/new/clone?repository-url=https://github.com/roman-ryzenadvanced/NanoJason) •
[💡 Features](#-amazing-features) •
[📖 Documentation](#-getting-started)
</div>

View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

View File

@@ -0,0 +1,19 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Vercel configuration
output: 'export',
trailingSlash: true,
distDir: 'out',
images: {
unoptimized: true
},
// Enable React compiler for better performance
reactCompiler: true,
// Additional optimizations
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
};
export default nextConfig;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
{
"name": "nanojason",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"next": "16.1.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"eslint": "^9",
"eslint-config-next": "16.1.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,66 @@
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
try {
const { code, state } = await request.json()
if (!code) {
return NextResponse.json(
{ error: 'Authorization code is required' },
{ status: 400 }
)
}
// Validate state for CSRF protection
const storedState = request.cookies.get('qwen_auth_state')?.value
if (state && storedState !== state) {
return NextResponse.json(
{ error: 'Invalid state parameter' },
{ status: 400 }
)
}
// In production, exchange code for actual Qwen token
// For demo, create a mock token
const mockAccessToken = `qwen_token_${code}_${Date.now()}`
const mockRefreshToken = `qwen_refresh_${code}_${Date.now()}`
const mockExpiresIn = 3600 // 1 hour
// Store in cookie for server-side access
const response = NextResponse.json({
access_token: mockAccessToken,
refresh_token: mockRefreshToken,
expires_in: mockExpiresIn,
token_type: 'Bearer',
success: true,
redirect_to: '/',
})
// Set secure HTTP-only cookie
response.cookies.set('qwen_access_token', mockAccessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: mockExpiresIn,
path: '/',
})
// Clear state cookie
response.cookies.set('qwen_auth_state', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0,
path: '/',
})
return response
} catch (error) {
console.error('Qwen auth callback error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from 'next/server'
import { sanitizeApiKey } from '@/utils/api-utils'
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ error: 'Authorization header is required' },
{ status: 401 }
)
}
const token = authHeader.substring(7)
// In a real implementation, you would verify the token with Qwen's API
// For now, we'll just check if it exists and is not expired
if (!token || token.startsWith('mock_')) {
// For demo purposes, accept mock tokens
// In production, you would validate this with Qwen's token endpoint
return NextResponse.json({
valid: true,
message: 'Token is valid (demo mode)'
})
}
// In production, you would call Qwen's token validation endpoint:
/*
const validationResponse = await fetch('https://open.bigmodel.cn/api/paas/v4/oauth2/validate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sanitizeApiKey(token) || token}`,
'Content-Type': 'application/json',
},
})
if (!validationResponse.ok) {
return NextResponse.json(
{ error: 'Token is invalid or expired' },
{ status: 401 }
)
}
*/
return NextResponse.json({
valid: true,
message: 'Token is valid'
})
} catch (error) {
console.error('Token verification error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,248 @@
import { NextRequest, NextResponse } from 'next/server'
export interface NarrativeRequest {
basePrompt: string
title: string
description: string
imageCount: number
style: string
characters?: string
items?: string
}
export interface NarrativeResponse {
prompts: string[]
}
export async function POST(request: NextRequest) {
try {
const body: NarrativeRequest = await request.json()
const {
basePrompt,
title,
description,
imageCount,
style,
characters = '',
items = '',
} = body
if (!basePrompt || !title || !description || imageCount <= 0) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
)
}
// Generate narrative progression
const prompts = generateNarrativeProgression({
basePrompt,
title,
description,
imageCount,
style,
characters,
items,
})
return NextResponse.json({
prompts,
success: true,
})
} catch (error) {
console.error('Narrative generation error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
function generateNarrativeProgression(config: NarrativeRequest): string[] {
const {
basePrompt,
title,
description,
imageCount,
style,
characters,
items,
} = config
const prompts: string[] = []
// Define narrative structure based on image count
const narrativeStructure = getNarrativeStructure(imageCount)
// Generate prompts for each narrative stage
for (let i = 0; i < imageCount; i++) {
const stage = narrativeStructure[i] || narrativeStructure[narrativeStructure.length - 1]
const prompt = generateStagePrompt({
stage,
basePrompt,
title,
description,
style,
characters,
items,
index: i,
total: imageCount,
})
prompts.push(prompt)
}
return prompts
}
function getNarrativeStructure(imageCount: number): string[] {
if (imageCount <= 3) {
return ['introduction', 'development', 'climax']
} else if (imageCount <= 5) {
return [
'introduction',
'rising_action',
'midpoint',
'climax',
'resolution'
]
} else if (imageCount <= 8) {
return [
'introduction',
'setup',
'rising_action_1',
'rising_action_2',
'midpoint',
'climax',
'falling_action',
'resolution'
]
} else {
// For longer series, use a more detailed structure
const structure = [
'introduction',
'world_building',
'character_introduction',
'initial_challenge',
'rising_action_1',
'rising_action_2',
'midpoint_twist',
'climax_buildup',
'climax',
'resolution',
'epilogue'
]
// Repeat middle elements for longer series
while (structure.length < imageCount) {
structure.splice(-2, 0, `development_${structure.length}`)
}
return structure.slice(0, imageCount)
}
}
function generateStagePrompt(config: {
stage: string
basePrompt: string
title: string
description: string
style: string
characters: string
items: string
index: number
total: number
}): string {
const {
stage,
basePrompt,
title,
description,
style,
characters,
items,
index,
total,
} = config
// Stage-specific templates
const stageTemplates: Record<string, string[]> = {
introduction: [
`Wide establishing shot of ${title} with ${description}. ${characters ? `Featuring ${characters}.` : ''} ${items ? `With ${items} visible.` : ''} ${style} style, atmospheric lighting.`,
`Close-up introduction to the main elements of ${title}, showcasing ${description}. ${characters ? `Focus on ${characters}.` : ''} ${style} aesthetic.`,
`Panoramic view of the ${title} scene, ${description}. Epic scale with ${characters} ${items ? `and ${items}` : ''} in ${style} style.`
],
rising_action: [
`${title}: Building tension as ${description}. ${characters} ${items ? `interacting with ${items}` : ''}. Dramatic composition in ${style} style.`,
`Progressive development of ${title}, ${description}. ${characters} showing character development. ${items} becoming more prominent. ${style} visual style.`,
`Action sequence in ${title}, ${description}. ${characters} in motion with ${items}. Dynamic angles and ${style} rendering.`
],
climax: [
`Dramatic climax of ${title}: ${description}. ${characters} at peak action with ${items}. High contrast lighting, ${style} style.`,
`Intense climax scene showing ${title}: ${description}. ${characters} in decisive moment with ${items}. Cinematic ${style} composition.`,
`Peak action in ${title}: ${description}. ${characters} and ${items} in dramatic confrontation. Epic scale with ${style} aesthetic.`
],
resolution: [
`Resolution of ${title}: ${description}. ${characters} ${items ? `with ${items}` : ''} in peaceful setting. Warm lighting, ${style} style.`,
`Conclusion of ${title}: ${description}. ${characters} ${items ? `and ${items}` : ''} in final arrangement. Serene composition, ${style} aesthetic.`,
`Final scene of ${title}: ${description}. ${characters} ${items ? `and ${items}` : ''} in satisfying conclusion. ${style} style with atmospheric mood.`
],
midpoint: [
`Midpoint twist in ${title}: ${description}. ${characters} facing unexpected challenge with ${items}. ${style} style with dramatic lighting.`,
`Turning point in ${title}: ${description}. ${characters} experiencing major development with ${items}. ${style} visual approach.`
],
development: [
`Development scene in ${title}: ${description}. ${characters} evolving story with ${items}. ${style} style with detailed composition.`,
`Progressive scene of ${title}: ${description}. ${characters} advancing narrative with ${items}. ${style} aesthetic with atmospheric elements.`
],
world_building: [
`World-building scene for ${title}: ${description}. Detailed environment showing ${characters} ${items ? `with ${items}` : ''} in ${style} style.`,
`Environmental storytelling in ${title}: ${description}. Rich world details featuring ${characters} ${items ? `and ${items}` : ''}. ${style} aesthetic.`
],
character_introduction: [
`Character introduction in ${title}: ${description}. Focus on ${characters} ${items ? `with ${items}` : ''} in ${style} style.`,
`Detailed character presentation in ${title}: ${description}. ${characters} ${items ? `interacting with ${items}` : ''}. ${style} visual treatment.`
],
initial_challenge: [
`Initial challenge in ${title}: ${description}. ${characters} facing ${items} in ${style} style.`,
`First obstacle scene: ${title} with ${description}. ${characters} encountering ${items}. ${style} composition.`
],
falling_action: [
`Falling action in ${title}: ${description}. ${characters} ${items ? `with ${items}` : ''} in aftermath. ${style} style with reflective mood.`,
`Resolution aftermath: ${title} - ${description}. ${characters} ${items ? `and ${items}` : ''} in peaceful setting. ${style} aesthetic.`
],
epilogue: [
`Epilogue scene for ${title}: ${description}. ${characters} ${items ? `with ${items}` : ''} in final moments. ${style} style with nostalgic atmosphere.`,
`Closing scene of ${title}: ${description}. ${characters} ${items ? `and ${items}` : ''} in satisfying conclusion. ${style} visual style.`
]
}
// Default template for unknown stages
const defaultTemplates = [
`${title}: ${description}. ${characters} ${items ? `with ${items}` : ''} in ${style} style.`,
`Scene from ${title}: ${description}. Featuring ${characters} ${items ? `and ${items}` : ''}. ${style} aesthetic.`,
`${title} scene: ${description}. ${characters} ${items ? `interacting with ${items}` : ''}. ${style} visual approach.`
]
// Get templates for the current stage
const templates = stageTemplates[stage] || defaultTemplates
// Select template based on index for variety
const templateIndex = index % templates.length
let prompt = templates[templateIndex]
// Add progress-specific details
const progress = (index + 1) / total
if (progress < 0.3) {
prompt += ' Beginning of the journey, hopeful atmosphere.'
} else if (progress < 0.7) {
prompt += ' Mid-journey, building intensity and drama.'
} else {
prompt += ' Conclusion, satisfying resolution and closure.'
}
return prompt
}

View File

@@ -0,0 +1,168 @@
import { NextRequest, NextResponse } from 'next/server'
import { seriesGenerator } from '@/services/series-generator'
export interface GenerateSeriesRequest {
basePrompt: string
config: {
title: string
description: string
imageCount: number
style: string
}
characters?: Array<{
name: string
description: string
features: any
}>
items?: Array<{
name: string
description: string
features: any
}>
}
export interface GenerateSeriesResponse {
seriesId: string
config: any
estimatedTime: number
status: 'pending' | 'generating' | 'completed' | 'error'
}
export async function POST(request: NextRequest) {
try {
const body: GenerateSeriesRequest = await request.json()
const { basePrompt, config, characters = [], items = [] } = body
if (!basePrompt || !config || !config.title || !config.description) {
return NextResponse.json(
{ error: 'Missing required fields: basePrompt, config.title, config.description' },
{ status: 400 }
)
}
if (config.imageCount < 1 || config.imageCount > 30) {
return NextResponse.json(
{ error: 'Image count must be between 1 and 30' },
{ status: 400 }
)
}
// Create the series
const series = await seriesGenerator.createSeries(
basePrompt,
config,
characters,
items
)
// Simulate image generation (in production, this would trigger actual generation)
const estimatedTime = Math.max(30, config.imageCount * 10) // 10-30 seconds per image
return NextResponse.json({
seriesId: series.id,
config: series.config,
estimatedTime,
status: 'pending',
})
} catch (error) {
console.error('Series generation error:', error)
return NextResponse.json(
{ error: 'Failed to generate series' },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const seriesId = searchParams.get('seriesId')
if (!seriesId) {
return NextResponse.json(
{ error: 'seriesId parameter is required' },
{ status: 400 }
)
}
const series = seriesGenerator.getSeries(seriesId)
if (!series) {
return NextResponse.json(
{ error: 'Series not found' },
{ status: 404 }
)
}
// Calculate generation status
const imageCount = series.images.length
const totalImages = series.config.imageCount
const progressPercentage = Math.round((imageCount / totalImages) * 100)
let status: 'pending' | 'generating' | 'completed' | 'error' = 'pending'
if (imageCount === 0) {
status = 'pending'
} else if (imageCount < totalImages) {
status = 'generating'
} else if (imageCount === totalImages) {
status = 'completed'
}
return NextResponse.json({
seriesId: series.id,
config: series.config,
images: series.images,
progress: {
current: imageCount,
total: totalImages,
percentage: progressPercentage,
},
status,
createdAt: series.createdAt,
})
} catch (error) {
console.error('Get series error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const seriesId = searchParams.get('seriesId')
if (!seriesId) {
return NextResponse.json(
{ error: 'seriesId parameter is required' },
{ status: 400 }
)
}
const success = seriesGenerator.deleteSeries(seriesId)
if (!success) {
return NextResponse.json(
{ error: 'Series not found' },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
message: 'Series deleted successfully',
})
} catch (error) {
console.error('Delete series error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,170 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Sparkles, Loader2, ExternalLink } from "lucide-react"
export default function AuthPage() {
const [isAuthenticating, setIsAuthenticating] = useState(false)
const [error, setError] = useState<string | null>(null)
const router = useRouter()
const searchParams = useSearchParams()
useEffect(() => {
// Check if we have auth code in URL
const code = searchParams.get('code')
const error = searchParams.get('error')
const success = searchParams.get('success')
if (error) {
setError(`Authentication failed: ${error}`)
return
}
if (success === 'true') {
// Successfully authenticated, redirect to home
router.push('/')
return
}
if (code) {
handleAuthCallback(code)
}
}, [searchParams, router])
const handleAuthCallback = async (authCode: string) => {
setIsAuthenticating(true)
setError(null)
try {
// Exchange auth code for access token
const response = await fetch('/api/auth/qwen/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code: authCode }),
})
if (!response.ok) {
throw new Error('Token exchange failed')
}
const data = await response.json()
if (data.success) {
// Store the token (in production, this would be more secure)
localStorage.setItem('qwen_access_token', data.access_token)
localStorage.setItem('qwen_authenticated', 'true')
// Redirect to home
router.push(data.redirect_to || '/')
} else {
throw new Error(data.error || 'Authentication failed')
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Authentication failed')
} finally {
setIsAuthenticating(false)
}
}
const handleQwenOAuth = () => {
// Redirect to Qwen OAuth for actual authentication
const clientId = process.env.NEXT_PUBLIC_QWEN_CLIENT_ID || 'demo_client_id'
const redirectUri = encodeURIComponent(process.env.NEXT_PUBLIC_QWEN_REDIRECT_URI || 'http://localhost:3000/auth')
const scope = 'api_access'
const state = Math.random().toString(36).substring(7)
// Store state for CSRF protection
localStorage.setItem('qwen_auth_state', state)
// Qwen OAuth URL (following Qwen Code implementation)
const authUrl = `https://qwen.ai/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`
window.location.href = authUrl
}
const handleDirectLogin = () => {
// Direct login for demo - simulates successful OAuth
const mockToken = `demo_token_${Date.now()}`
localStorage.setItem('qwen_access_token', mockToken)
localStorage.setItem('qwen_authenticated', 'true')
router.push('/')
}
if (isAuthenticating) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="w-96">
<CardContent className="pt-6">
<div className="text-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin mx-auto text-blue-600" />
<p className="text-gray-600">Authenticating with Qwen...</p>
</div>
</CardContent>
</Card>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="w-96">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<Sparkles className="h-12 w-12 bg-gradient-to-r from-blue-600 to-purple-600" />
</div>
<CardTitle className="text-2xl">Welcome to NanoJason</CardTitle>
<CardDescription>
Connect with Qwen AI to start creating stunning image series
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{error}
</div>
)}
<div className="space-y-3">
<Button
onClick={handleDirectLogin}
className="w-full bg-blue-600 text-white hover:bg-blue-700"
size="lg"
>
<Sparkles className="mr-2 h-4 w-4" />
Quick Demo Login
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="bg-white px-2 text-xs text-gray-500">OR</span>
</div>
<hr className="border-gray-300" />
</div>
<Button
onClick={handleQwenOAuth}
variant="outline"
className="w-full"
size="lg"
>
<ExternalLink className="mr-2 h-4 w-4" />
Connect with Qwen OAuth
</Button>
</div>
<div className="text-xs text-gray-600 text-center space-y-1">
<p>By connecting, you agree to Qwen's terms of service</p>
<p className="font-medium">🚀 Free tier: 2,000 requests/day</p>
<p className="text-green-600"> 60 requests/minute limit</p>
<p className="text-blue-600">🔐 OAuth authentication with automatic token refresh</p>
</div>
</CardContent>
</Card>
</div>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,151 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
--card: #ffffff;
--card-foreground: #171717;
--primary: #0ea5e9;
--primary-foreground: #ffffff;
--secondary: #f1f5f9;
--secondary-foreground: #0f172a;
--accent: #d946ef;
--accent-foreground: #ffffff;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--border: #e2e8f0;
--input: #e2e8f0;
--ring: #0ea5e9;
--radius: 0.5rem;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--card: #0a0a0a;
--card-foreground: #ededed;
--primary: #0284c7;
--primary-foreground: #ffffff;
--secondary: #1e293b;
--secondary-foreground: #f1f5f9;
--accent: #a21caf;
--accent-foreground: #ffffff;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--border: #334155;
--input: #334155;
--ring: #0ea5e9;
}
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius: var(--radius);
}
@layer base {
* {
border-color: hsl(var(--border));
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-family: "Inter", system-ui, sans-serif;
}
}
@layer components {
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.glass-effect {
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.card-hover {
transition: all 0.3s ease;
&:hover {
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1);
transform: scale(1.05);
}
}
.text-gradient {
background: linear-gradient(to right, hsl(var(--primary)), hsl(var(--accent)));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.btn-primary {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
padding: 0.5rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s ease;
&:hover {
background: hsl(var(--primary) / 0.9);
}
}
.btn-secondary {
background: hsl(var(--secondary));
color: hsl(var(--secondary-foreground));
padding: 0.5rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s ease;
&:hover {
background: hsl(var(--secondary) / 0.8);
}
}
}
@layer utilities {
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-slide-up {
animation: slideUp 0.3s ease-out;
}
.animate-fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
@keyframes slideUp {
0% { transform: translateY(10px); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
}

View File

@@ -0,0 +1,31 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Header } from "@/components/layout/header";
const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
});
export const metadata: Metadata = {
title: "NanoJason - AI-Powered Image Series Creation",
description: "Create stunning image series with consistent characters and narrative using AI. Transform your ideas into visual stories.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${inter.variable} font-sans antialiased`}>
<Header />
<main className="min-h-screen">
{children}
</main>
</body>
</html>
);
}

View File

@@ -0,0 +1,5 @@
import OllamaSettings from "@/components/layout/ollama-settings"
export default function OllamaSettingsPage() {
return <OllamaSettings />
}

View File

@@ -0,0 +1,768 @@
'use client'
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Copy, Download, Sparkles, Info, FileText } from "lucide-react"
import { jasonTranslator, JasonFormat } from "@/services/jason-translator"
import { nanoPrompt, NanoPromptResult } from "@/services/nano-prompt"
import { nanoCoder, NanoCoderResult } from "@/services/nano-coder"
export default function HomePage() {
const [inputText, setInputText] = useState('')
const [jasonFormat, setJasonFormat] = useState<JasonFormat | null>(null)
const [nanoPromptResult, setNanoPromptResult] = useState<NanoPromptResult | null>(null)
const [nanoCoderResult, setNanoCoderResult] = useState<NanoCoderResult | null>(null)
const [isTranslating, setIsTranslating] = useState(false)
const [isOptimizing, setIsOptimizing] = useState(false)
const [isCoding, setIsCoding] = useState(false)
const [copied, setCopied] = useState(false)
const [activeTab, setActiveTab] = useState<'jason' | 'nano' | 'code'>('jason')
const handleTranslate = async () => {
if (!inputText.trim()) return
setIsTranslating(true)
try {
const result = await jasonTranslator.translateToJason(inputText)
setJasonFormat(result)
setNanoPromptResult(null) // Clear nano prompt result
} catch (error) {
console.error('Translation failed:', error)
} finally {
setIsTranslating(false)
}
}
const handleNanoOptimize = async () => {
if (!inputText.trim()) return
setIsOptimizing(true)
try {
const result = await nanoPrompt.optimizePrompt(inputText)
setNanoPromptResult(result)
setJasonFormat(null) // Clear jason format result
setNanoCoderResult(null) // Clear nano coder result
} catch (error) {
console.error('Nano optimization failed:', error)
} finally {
setIsOptimizing(false)
}
}
const handleNanoCode = async () => {
if (!inputText.trim()) return
setIsCoding(true)
try {
const result = await nanoCoder.optimizePrompt(inputText)
setNanoCoderResult(result)
setJasonFormat(null) // Clear jason format result
setNanoPromptResult(null) // Clear nano prompt result
} catch (error) {
console.error('Nano coding failed:', error)
} finally {
setIsCoding(false)
}
}
const handleCopy = async () => {
if (!jasonFormat) return
try {
await navigator.clipboard.writeText(jasonFormat.jason_text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (error) {
console.error('Failed to copy:', error)
}
}
const handleDownload = () => {
if (!jasonFormat) return
const dataStr = JSON.stringify(jasonFormat, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
link.download = `jason-format-${Date.now()}.json`
link.click()
URL.revokeObjectURL(url)
}
const handleCopyNanoPrompt = async () => {
if (!nanoPromptResult) return
try {
await navigator.clipboard.writeText(nanoPromptResult.jason_format)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (error) {
console.error('Failed to copy:', error)
}
}
const handleDownloadNanoPrompt = () => {
if (!nanoPromptResult) return
const dataStr = JSON.stringify(nanoPromptResult, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
link.download = `nano-prompt-${Date.now()}.json`
link.click()
URL.revokeObjectURL(url)
}
const handleCopyNanoCoder = async () => {
if (!nanoCoderResult) return
try {
await navigator.clipboard.writeText(nanoCoderResult.jason_format)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (error) {
console.error('Failed to copy:', error)
}
}
const handleDownloadNanoCoder = () => {
if (!nanoCoderResult) return
const dataStr = JSON.stringify(nanoCoderResult, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
link.download = `nano-coder-${Date.now()}.json`
link.click()
URL.revokeObjectURL(url)
}
const quickTemplates = [
{ text: "A magical forest with glowing mushrooms and fairies", label: "Fantasy Scene" },
{ text: "Futuristic city skyline at sunset with flying cars", label: "Sci-Fi City" },
{ text: "Cozy cabin in the woods with warm lights", label: "Peaceful Nature" },
{ text: "Dragon breathing fire in a medieval castle", label: "Epic Fantasy" }
]
const nanoQuickTemplates = [
{ text: "A beautiful portrait of a woman", label: "Portrait" },
{ text: "Mountains at sunset", label: "Landscape" },
{ text: "A superhero character", label: "Character" },
{ text: "A smartphone product", label: "Product" }
]
const nanoCoderQuickTemplates = [
{ text: "React web application with TypeScript", label: "Web App" },
{ text: "React Native mobile app", label: "Mobile App" },
{ text: "Node.js microservice with REST API", label: "Microservice" },
{ text: "REST API with authentication", label: "API" },
{ text: "Data visualization dashboard", label: "Dashboard" }
]
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
<div className="container mx-auto px-4 py-8 max-w-6xl">
{/* Header */}
<div className="text-center mb-12">
<h1 className="text-4xl md:text-6xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-4">
NanoJason
</h1>
<p className="text-xl text-gray-600 mb-2">
Universal AI Prompt Language Translator & Optimizer
</p>
<p className="text-sm text-gray-500 max-w-2xl mx-auto">
Convert natural language descriptions to Jason format and optimize for maximum AI accuracy
</p>
</div>
{/* Tabs */}
<div className="flex justify-center mb-8">
<div className="bg-white rounded-full p-1 shadow-lg border border-gray-200">
<button
onClick={() => setActiveTab('jason')}
className={`px-6 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
activeTab === 'jason'
? 'bg-blue-600 text-white shadow-md'
: 'text-gray-600 hover:text-blue-600'
}`}
>
Jason Translator
</button>
<button
onClick={() => setActiveTab('nano')}
className={`px-6 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
activeTab === 'nano'
? 'bg-purple-600 text-white shadow-md'
: 'text-gray-600 hover:text-purple-600'
}`}
>
NanoPrompt
</button>
<button
onClick={() => setActiveTab('code')}
className={`px-6 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
activeTab === 'code'
? 'bg-green-600 text-white shadow-md'
: 'text-gray-600 hover:text-green-600'
}`}
>
NanoCoder
</button>
</div>
</div>
{/* Jason Translator */}
{activeTab === 'jason' && (
<Card className="mb-12 border-2 border-blue-200 shadow-xl">
<CardHeader className="bg-gradient-to-r from-blue-50 to-purple-50">
<CardTitle className="flex items-center gap-2 text-2xl">
<Sparkles className="h-6 w-6 text-blue-600" />
Jason Language Translator
</CardTitle>
<CardDescription className="text-lg">
Describe what you want to create, and I'll convert it to universal Jason format
</CardDescription>
</CardHeader>
<CardContent className="space-y-6 p-6">
{/* Quick Templates */}
<div>
<h4 className="font-semibold text-gray-700 mb-3">Quick Templates:</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{quickTemplates.map((template, index) => (
<Button
key={index}
variant="outline"
size="sm"
onClick={() => setInputText(template.text)}
className="text-xs h-auto py-2 px-3 whitespace-normal text-left"
>
{template.label}
</Button>
))}
</div>
</div>
{/* Input */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Describe your image:
</label>
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="A beautiful landscape with mountains and a lake at sunset..."
className="min-h-32 border-2 border-gray-200 focus:border-blue-400 rounded-lg p-3 w-full"
/>
</div>
{/* Translate Button */}
<Button
onClick={handleTranslate}
disabled={!inputText.trim() || isTranslating}
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold py-3 text-lg"
>
{isTranslating ? (
<span className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Translating...
</span>
) : (
<span className="flex items-center gap-2">
<Sparkles className="h-5 w-5" />
Translate to Jason Format
</span>
)}
</Button>
{/* Results */}
{jasonFormat && (
<div className="space-y-4 mt-6">
<div className="bg-gradient-to-r from-green-50 to-blue-50 border-2 border-green-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-green-800 flex items-center gap-2">
<FileText className="h-5 w-5" />
Jason Format Result
</h4>
<div className="flex gap-2">
<Button
onClick={handleCopy}
variant="outline"
size="sm"
className="text-xs"
>
{copied ? (
<span className="flex items-center gap-1 text-green-600">
<span className="w-3 h-3 bg-green-600 rounded-full"></span>
Copied!
</span>
) : (
<span className="flex items-center gap-1">
<Copy className="h-3 w-3" />
Copy
</span>
)}
</Button>
<Button
onClick={handleDownload}
variant="outline"
size="sm"
className="text-xs"
>
<Download className="h-3 w-3 mr-1" />
Download
</Button>
</div>
</div>
<div className="bg-white rounded-lg p-3 border border-gray-200">
<pre className="text-sm text-gray-800 whitespace-pre-wrap break-words">
{JSON.stringify(jasonFormat, null, 2)}
</pre>
</div>
</div>
{/* Format Info */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h5 className="font-semibold text-blue-800 mb-2 flex items-center gap-2">
<Info className="h-4 w-4" />
About Jason Format
</h5>
<p className="text-sm text-blue-700">
Jason is a universal prompt language that works with all AI image generators.
It includes detailed style, mood, character, and item information for consistent results.
</p>
</div>
{/* Style Badges */}
<div className="flex flex-wrap gap-2">
{jasonFormat.style && (
<span className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs">
Style: {jasonFormat.style}
</span>
)}
{jasonFormat.mood && (
<span className="bg-purple-100 text-purple-800 px-2 py-1 rounded text-xs">
Mood: {jasonFormat.mood}
</span>
)}
{jasonFormat.characters && jasonFormat.characters.length > 0 && (
<span className="bg-green-100 text-green-800 px-2 py-1 rounded text-xs">
Characters: {jasonFormat.characters.join(', ')}
</span>
)}
{jasonFormat.items && jasonFormat.items.length > 0 && (
<span className="bg-orange-100 text-orange-800 px-2 py-1 rounded text-xs">
Items: {jasonFormat.items.join(', ')}
</span>
)}
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* NanoPrompt Optimizer */}
{activeTab === 'nano' && (
<Card className="mb-12 border-2 border-purple-200 shadow-xl">
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50">
<CardTitle className="flex items-center gap-2 text-2xl">
<Sparkles className="h-6 w-6 text-purple-600" />
NanoPrompt Optimizer
</CardTitle>
<CardDescription className="text-lg">
Deep reverse engineering for maximum AI accuracy and performance
</CardDescription>
</CardHeader>
<CardContent className="space-y-6 p-6">
{/* Quick Templates */}
<div>
<h4 className="font-semibold text-gray-700 mb-3">Quick Templates:</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{nanoQuickTemplates.map((template, index) => (
<Button
key={index}
variant="outline"
size="sm"
onClick={() => setInputText(template.text)}
className="text-xs h-auto py-2 px-3 whitespace-normal text-left"
>
{template.label}
</Button>
))}
</div>
</div>
{/* Input */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Enter your English prompt:
</label>
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="A beautiful portrait of a woman..."
className="min-h-32 border-2 border-gray-200 focus:border-purple-400 rounded-lg p-3 w-full"
/>
</div>
{/* Optimize Button */}
<Button
onClick={handleNanoOptimize}
disabled={!inputText.trim() || isOptimizing}
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-3 text-lg"
>
{isOptimizing ? (
<span className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Optimizing...
</span>
) : (
<span className="flex items-center gap-2">
<Sparkles className="h-5 w-5" />
Optimize with NanoPrompt
</span>
)}
</Button>
{/* Results */}
{nanoPromptResult && (
<div className="space-y-4 mt-6">
<div className="bg-gradient-to-r from-purple-50 to-pink-50 border-2 border-purple-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-purple-800 flex items-center gap-2">
<FileText className="h-5 w-5" />
NanoPrompt Optimization Result
</h4>
<div className="flex gap-2">
<Button
onClick={() => handleCopyNanoPrompt()}
variant="outline"
size="sm"
className="text-xs"
>
{copied ? (
<span className="flex items-center gap-1 text-green-600">
<span className="w-3 h-3 bg-green-600 rounded-full"></span>
Copied!
</span>
) : (
<span className="flex items-center gap-1">
<Copy className="h-3 w-3" />
Copy
</span>
)}
</Button>
<Button
onClick={() => handleDownloadNanoPrompt()}
variant="outline"
size="sm"
className="text-xs"
>
<Download className="h-3 w-3 mr-1" />
Download
</Button>
</div>
</div>
{/* Score */}
<div className="bg-white rounded-lg p-3 border border-gray-200 mb-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Accuracy Score:</span>
<span className="text-lg font-bold text-green-600">{nanoPromptResult.accuracy_score}%</span>
</div>
</div>
{/* Original vs Optimized */}
<div className="grid md:grid-cols-2 gap-3 mb-3">
<div>
<div className="text-xs text-gray-500 font-medium mb-1">Original:</div>
<div className="bg-gray-50 rounded p-2 text-sm text-gray-800">
{nanoPromptResult.original_prompt}
</div>
</div>
<div>
<div className="text-xs text-gray-500 font-medium mb-1">Optimized:</div>
<div className="bg-purple-50 rounded p-2 text-sm text-purple-800">
{nanoPromptResult.nano_prompt}
</div>
</div>
</div>
{/* Optimizations Applied */}
<div className="mb-3">
<div className="text-xs text-gray-500 font-medium mb-1">Optimizations Applied:</div>
<div className="flex flex-wrap gap-1">
{nanoPromptResult.optimizations_applied.map((opt, index) => (
<span key={index} className="bg-purple-100 text-purple-700 text-xs px-2 py-1 rounded">
{opt}
</span>
))}
</div>
</div>
{/* Performance Tips */}
<div>
<div className="text-xs text-gray-500 font-medium mb-1">Performance Tips:</div>
<ul className="text-xs text-gray-700 space-y-1">
{nanoPromptResult.performance_tips.map((tip, index) => (
<li key={index} className="flex items-start gap-1">
<span className="w-1 h-1 bg-purple-600 rounded-full mt-2 flex-shrink-0"></span>
{tip}
</li>
))}
</ul>
</div>
</div>
{/* Jason Format */}
<div className="bg-gradient-to-r from-blue-50 to-purple-50 border-2 border-blue-200 rounded-lg p-4">
<h5 className="font-semibold text-blue-800 mb-2 flex items-center gap-2">
<Info className="h-4 w-4" />
Optimized Jason Format
</h5>
<div className="bg-white rounded-lg p-3 border border-gray-200">
<pre className="text-sm text-gray-800 whitespace-pre-wrap break-words">
{nanoPromptResult.jason_format}
</pre>
</div>
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* NanoCoder Optimizer */}
{activeTab === 'code' && (
<Card className="mb-12 border-2 border-green-200 shadow-xl">
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50">
<CardTitle className="flex items-center gap-2 text-2xl">
<Sparkles className="h-6 w-6 text-green-600" />
NanoCoder Technical Optimizer
</CardTitle>
<CardDescription className="text-lg">
Deep reverse engineering for maximum coding and engineering accuracy
</CardDescription>
</CardHeader>
<CardContent className="space-y-6 p-6">
{/* Quick Templates */}
<div>
<h4 className="font-semibold text-gray-700 mb-3">Technical Templates:</h4>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{nanoCoderQuickTemplates.map((template, index) => (
<Button
key={index}
variant="outline"
size="sm"
onClick={() => setInputText(template.text)}
className="text-xs h-auto py-2 px-3 whitespace-normal text-left"
>
{template.label}
</Button>
))}
</div>
</div>
{/* Input */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Enter your technical prompt:
</label>
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="React web application with TypeScript..."
className="min-h-32 border-2 border-gray-200 focus:border-green-400 rounded-lg p-3 w-full"
/>
</div>
{/* Optimize Button */}
<Button
onClick={handleNanoCode}
disabled={!inputText.trim() || isCoding}
className="w-full bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white font-semibold py-3 text-lg"
>
{isCoding ? (
<span className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Optimizing...
</span>
) : (
<span className="flex items-center gap-2">
<Sparkles className="h-5 w-5" />
Optimize with NanoCoder
</span>
)}
</Button>
{/* Results */}
{nanoCoderResult && (
<div className="space-y-4 mt-6">
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-green-800 flex items-center gap-2">
<FileText className="h-5 w-5" />
NanoCoder Technical Result
</h4>
<div className="flex gap-2">
<Button
onClick={() => handleCopyNanoCoder()}
variant="outline"
size="sm"
className="text-xs"
>
{copied ? (
<span className="flex items-center gap-1 text-green-600">
<span className="w-3 h-3 bg-green-600 rounded-full"></span>
Copied!
</span>
) : (
<span className="flex items-center gap-1">
<Copy className="h-3 w-3" />
Copy
</span>
)}
</Button>
<Button
onClick={() => handleDownloadNanoCoder()}
variant="outline"
size="sm"
className="text-xs"
>
<Download className="h-3 w-3 mr-1" />
Download
</Button>
</div>
</div>
{/* Score */}
<div className="bg-white rounded-lg p-3 border border-gray-200 mb-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Technical Accuracy:</span>
<span className="text-lg font-bold text-green-600">{nanoCoderResult.accuracy_score}%</span>
</div>
</div>
{/* Original vs Optimized */}
<div className="grid md:grid-cols-2 gap-3 mb-3">
<div>
<div className="text-xs text-gray-500 font-medium mb-1">Original:</div>
<div className="bg-gray-50 rounded p-2 text-sm text-gray-800">
{nanoCoderResult.original_prompt}
</div>
</div>
<div>
<div className="text-xs text-gray-500 font-medium mb-1">Technical:</div>
<div className="bg-green-50 rounded p-2 text-sm text-green-800">
{nanoCoderResult.nano_coder_prompt}
</div>
</div>
</div>
{/* Technical Specifications */}
<div className="mb-3">
<div className="text-xs text-gray-500 font-medium mb-1">Technical Specifications:</div>
<div className="flex flex-wrap gap-1">
{nanoCoderResult.technical_specifications.map((spec, index) => (
<span key={index} className="bg-green-100 text-green-700 text-xs px-2 py-1 rounded">
{spec}
</span>
))}
</div>
</div>
{/* Code Context */}
{nanoCoderResult.code_context.length > 0 && (
<div className="mb-3">
<div className="text-xs text-gray-500 font-medium mb-1">Code Context:</div>
<div className="bg-blue-50 rounded p-2 text-xs text-blue-800">
{nanoCoderResult.code_context.join(', ')}
</div>
</div>
)}
{/* Performance Tips */}
<div>
<div className="text-xs text-gray-500 font-medium mb-1">Technical Tips:</div>
<ul className="text-xs text-gray-700 space-y-1">
{nanoCoderResult.performance_tips.map((tip, index) => (
<li key={index} className="flex items-start gap-1">
<span className="w-1 h-1 bg-green-600 rounded-full mt-2 flex-shrink-0"></span>
{tip}
</li>
))}
</ul>
</div>
</div>
{/* Technical Jason Format */}
<div className="bg-gradient-to-r from-blue-50 to-green-50 border-2 border-blue-200 rounded-lg p-4">
<h5 className="font-semibold text-blue-800 mb-2 flex items-center gap-2">
<Info className="h-4 w-4" />
Technical Jason Format
</h5>
<div className="bg-white rounded-lg p-3 border border-gray-200">
<pre className="text-sm text-gray-800 whitespace-pre-wrap break-words">
{nanoCoderResult.jason_format}
</pre>
</div>
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Developer Section */}
<div className="mt-16 text-center py-8 border-t border-gray-200">
<div className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-6 max-w-4xl mx-auto">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Developed by Roman | RyzenAdvanced</h3>
<div className="flex flex-col md:flex-row justify-center items-center gap-6 text-sm text-gray-600">
<div className="flex items-center gap-2">
<span className="font-medium">GitHub:</span>
<a
href="https://github.com/roman-ryzenadvanced/Custom-Engineered-Agents-and-Tools-for-Vibe-Coders"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline"
>
Custom Engineered Agents and Tools for Vibe Coders
</a>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">Tools:</span>
<a
href="https://z.ai/subscribe?ic=R0K78RJKNW"
target="_blank"
rel="noopener noreferrer"
className="text-green-600 hover:text-green-800 underline"
>
GLM 4.6 Coding Model
</a>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">Built with:</span>
<a
href="https://www.trae.ai/s/WJtxyE"
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 hover:text-purple-800 underline"
>
TRAE IDE
</a>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,90 @@
'use client'
import { useState, useEffect } from 'react'
import { ollamaClient } from '@/services/ollama-client'
export default function TestErrorPage() {
const [testResult, setTestResult] = useState<string>('Click test button to start')
const [error, setError] = useState<string>('')
const testOllamaConnection = async () => {
try {
setTestResult('Testing API key...')
setError('')
// Test with your actual API key
const testKey = '81be8de35b254163a87ba41a5e4e565d.x-TOGbA79gS7AAHTwziOUAIb'
console.log('🔍 Testing API key:', testKey)
console.log('🔍 API key length:', testKey.length)
console.log('🔍 API key chars:', testKey.split('').map(c => c.charCodeAt(0)))
// Set the API key
ollamaClient.setApiKey(testKey, false)
// Test if available
const isAvailable = ollamaClient.isAvailable()
console.log('🔍 Client available:', isAvailable)
setTestResult(`API Key Set: ${isAvailable ? '✅' : '❌'}`)
if (isAvailable) {
setTestResult('Testing API call...')
// Try to list models
const models = await ollamaClient.listModels()
console.log('✅ Models retrieved:', models)
setTestResult(`✅ Success! Found ${models.length} models`)
}
} catch (err) {
console.error('❌ Test failed:', err)
setError(`Error: ${err.message}`)
setTestResult('❌ Test Failed')
}
}
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-2xl mx-auto">
<h1 className="text-3xl font-bold mb-6">API Error Debug Test</h1>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">Test Ollama Connection</h2>
<button
onClick={testOllamaConnection}
className="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600"
>
Test API Connection
</button>
<div className="mt-4">
<div className="font-semibold">Test Result:</div>
<div className="mt-2 p-3 bg-gray-100 rounded">
{testResult}
</div>
</div>
{error && (
<div className="mt-4">
<div className="font-semibold text-red-600">Error:</div>
<div className="mt-2 p-3 bg-red-100 rounded text-red-700">
{error}
</div>
</div>
)}
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Browser Console</h2>
<p className="text-sm text-gray-600">
Check the browser console (F12) for detailed debugging information.
</p>
<p className="text-sm text-gray-600 mt-2">
Look for 🔍 emoji markers in console logs.
</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,556 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { jasonProcessor, CharacterConsistency, ItemConsistency, SeriesConsistency } from "@/services/jason-processor"
import { seriesGenerator } from "@/services/series-generator"
import { Users, Package, Palette, Settings, Eye, Edit, Trash2, Plus, CheckCircle, AlertCircle } from "lucide-react"
interface ConsistencyTrackerProps {
seriesId?: string
onConsistencyUpdate?: (consistency: SeriesConsistency) => void
}
export function ConsistencyTracker({ seriesId, onConsistencyUpdate }: ConsistencyTrackerProps) {
const [consistencyProfile, setConsistencyProfile] = useState<SeriesConsistency | null>(null)
const [characters, setCharacters] = useState<CharacterConsistency[]>([])
const [items, setItems] = useState<ItemConsistency[]>([])
const [activeTab, setActiveTab] = useState<'characters' | 'items' | 'colors'>('characters')
const [isEditing, setIsEditing] = useState(false)
const [editForm, setEditForm] = useState<{
type: 'character' | 'item'
data: Partial<CharacterConsistency> | Partial<ItemConsistency>
index: number
} | null>(null)
useEffect(() => {
if (seriesId) {
loadConsistencyProfile(seriesId)
}
}, [seriesId])
const loadConsistencyProfile = (seriesId: string) => {
const series = seriesGenerator.getSeries(seriesId)
if (series && series.images.length > 0) {
const jasonPrompts = series.images.map(image =>
jasonProcessor.parseJasonPrompt(image.jasonPrompt || "")
).filter(Boolean) as any[]
if (jasonPrompts.length > 0) {
const profile = jasonProcessor.extractConsistencyProfiles(jasonPrompts)
setConsistencyProfile(profile)
setCharacters(profile.characters)
setItems(profile.items)
}
}
}
const addCharacter = () => {
const newCharacter: CharacterConsistency = {
name: "New Character",
description: "Character description",
features: {
hair: "unknown",
eyes: "unknown",
clothing: "unknown",
accessories: [],
expression: "neutral",
},
consistency: {
mustMaintain: ["name", "style"],
canVary: ["pose", "expression", "position"],
priority: "high",
},
}
setCharacters(prev => [...prev, newCharacter])
setEditForm({ type: 'character', data: newCharacter, index: characters.length })
setIsEditing(true)
}
const addItem = () => {
const newItem: ItemConsistency = {
name: "New Item",
description: "Item description",
features: {
color: "unknown",
material: "unknown",
size: "unknown",
},
consistency: {
mustMaintain: ["name", "style"],
canVary: ["position", "lighting", "size"],
priority: "medium",
},
}
setItems(prev => [...prev, newItem])
setEditForm({ type: 'item', data: newItem, index: items.length })
setIsEditing(true)
}
const updateCharacter = (index: number, updatedCharacter: CharacterConsistency) => {
const newCharacters = [...characters]
newCharacters[index] = updatedCharacter
setCharacters(newCharacters)
}
const updateItem = (index: number, updatedItem: ItemConsistency) => {
const newItems = [...items]
newItems[index] = updatedItem
setItems(newItems)
}
const removeCharacter = (index: number) => {
setCharacters(prev => prev.filter((_, i) => i !== index))
}
const removeItem = (index: number) => {
setItems(prev => prev.filter((_, i) => i !== index))
}
const updateConsistencyProfile = () => {
if (characters.length > 0 || items.length > 0) {
const profile: SeriesConsistency = {
characters,
items,
colorPalette: consistencyProfile?.colorPalette || [],
style: consistencyProfile?.style || "realistic",
mood: consistencyProfile?.mood || "neutral",
narrativeProgression: consistencyProfile?.narrativeProgression || {
start: "Beginning",
middle: "Middle",
end: "End",
},
}
setConsistencyProfile(profile)
onConsistencyUpdate?.(profile)
}
}
const saveEdit = () => {
if (!editForm) return
if (editForm.type === 'character') {
updateCharacter(editForm.index, editForm.data as CharacterConsistency)
} else {
updateItem(editForm.index, editForm.data as ItemConsistency)
}
setIsEditing(false)
setEditForm(null)
updateConsistencyProfile()
}
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'text-red-600 bg-red-50'
case 'medium': return 'text-yellow-600 bg-yellow-50'
case 'low': return 'text-green-600 bg-green-50'
default: return 'text-gray-600 bg-gray-50'
}
}
const getPriorityIcon = (priority: string) => {
switch (priority) {
case 'high': return <AlertCircle className="h-3 w-3" />
case 'medium': return <AlertCircle className="h-3 w-3" />
case 'low': return <CheckCircle className="h-3 w-3" />
default: return <CheckCircle className="h-3 w-3" />
}
}
return (
<div className="space-y-6">
{/* Overview */}
{consistencyProfile && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Consistency Overview
</CardTitle>
<CardDescription>
Track character and item consistency across your image series
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-muted rounded-lg">
<Users className="h-8 w-8 mx-auto mb-2 text-primary" />
<div className="text-2xl font-bold">{characters.length}</div>
<div className="text-sm text-muted-foreground">Characters</div>
</div>
<div className="text-center p-4 bg-muted rounded-lg">
<Package className="h-8 w-8 mx-auto mb-2 text-accent" />
<div className="text-2xl font-bold">{items.length}</div>
<div className="text-sm text-muted-foreground">Items</div>
</div>
<div className="text-center p-4 bg-muted rounded-lg">
<Palette className="h-8 w-8 mx-auto mb-2 text-green-600" />
<div className="text-2xl font-bold">{consistencyProfile.colorPalette.length}</div>
<div className="text-sm text-muted-foreground">Colors</div>
</div>
<div className="text-center p-4 bg-muted rounded-lg">
<Eye className="h-8 w-8 mx-auto mb-2 text-blue-600" />
<div className="text-2xl font-bold">
{Math.round((characters.length + items.length) * 0.8)}%
</div>
<div className="text-sm text-muted-foreground">Consistency</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* Tab Navigation */}
<Card>
<CardHeader>
<CardTitle>Consistency Management</CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-2 mb-6">
<Button
variant={activeTab === 'characters' ? 'default' : 'outline'}
onClick={() => setActiveTab('characters')}
>
<Users className="mr-2 h-4 w-4" />
Characters
</Button>
<Button
variant={activeTab === 'items' ? 'default' : 'outline'}
onClick={() => setActiveTab('items')}
>
<Package className="mr-2 h-4 w-4" />
Items
</Button>
<Button
variant={activeTab === 'colors' ? 'default' : 'outline'}
onClick={() => setActiveTab('colors')}
>
<Palette className="mr-2 h-4 w-4" />
Colors
</Button>
</div>
{/* Characters Tab */}
{activeTab === 'characters' && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-medium">Character Profiles</h3>
<Button onClick={addCharacter} size="sm">
<Plus className="mr-2 h-4 w-4" />
Add Character
</Button>
</div>
{characters.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Users className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No characters defined</p>
<p className="text-sm">Add characters to maintain consistency</p>
</div>
) : (
<div className="space-y-3">
{characters.map((character, index) => (
<Card key={index} className="p-4">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h4 className="font-medium">{character.name}</h4>
<span className={`px-2 py-1 rounded-full text-xs ${getPriorityColor(character.consistency.priority)}`}>
{getPriorityIcon(character.consistency.priority)}
{character.consistency.priority} priority
</span>
</div>
<p className="text-sm text-muted-foreground mb-3">
{character.description}
</p>
<div className="grid md:grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Appearance:</span>
<div className="mt-1">
<div>Hair: {character.features.hair}</div>
<div>Eyes: {character.features.eyes}</div>
<div>Clothing: {character.features.clothing}</div>
</div>
</div>
<div>
<span className="text-muted-foreground">Consistency:</span>
<div className="mt-1">
<div>Must maintain: {character.consistency.mustMaintain.join(', ')}</div>
<div>Can vary: {character.consistency.canVary.join(', ')}</div>
</div>
</div>
</div>
</div>
<div className="flex gap-1 ml-4">
<Button
variant="ghost"
size="sm"
onClick={() => setEditForm({ type: 'character', data: character, index })}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => removeCharacter(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</Card>
))}
</div>
)}
</div>
)}
{/* Items Tab */}
{activeTab === 'items' && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-medium">Item Profiles</h3>
<Button onClick={addItem} size="sm">
<Plus className="mr-2 h-4 w-4" />
Add Item
</Button>
</div>
{items.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Package className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No items defined</p>
<p className="text-sm">Add items to maintain consistency</p>
</div>
) : (
<div className="space-y-3">
{items.map((item, index) => (
<Card key={index} className="p-4">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h4 className="font-medium">{item.name}</h4>
<span className={`px-2 py-1 rounded-full text-xs ${getPriorityColor(item.consistency.priority)}`}>
{getPriorityIcon(item.consistency.priority)}
{item.consistency.priority} priority
</span>
</div>
<p className="text-sm text-muted-foreground mb-3">
{item.description}
</p>
<div className="grid md:grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Appearance:</span>
<div className="mt-1">
<div>Color: {item.features.color}</div>
<div>Material: {item.features.material}</div>
<div>Size: {item.features.size}</div>
</div>
</div>
<div>
<span className="text-muted-foreground">Consistency:</span>
<div className="mt-1">
<div>Must maintain: {item.consistency.mustMaintain.join(', ')}</div>
<div>Can vary: {item.consistency.canVary.join(', ')}</div>
</div>
</div>
</div>
</div>
<div className="flex gap-1 ml-4">
<Button
variant="ghost"
size="sm"
onClick={() => setEditForm({ type: 'item', data: item, index })}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => removeItem(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</Card>
))}
</div>
)}
</div>
)}
{/* Colors Tab */}
{activeTab === 'colors' && (
<div className="space-y-4">
<h3 className="font-medium">Color Palette</h3>
{consistencyProfile?.colorPalette ? (
<div className="flex flex-wrap gap-2">
{consistencyProfile.colorPalette.map((color, index) => (
<div
key={index}
className="w-12 h-12 rounded-lg border-2 border-gray-200 flex items-center justify-center text-xs font-medium text-white"
style={{ backgroundColor: color }}
>
{color}
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
<Palette className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No color palette defined</p>
<p className="text-sm">Colors will be extracted from your prompts</p>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Edit Modal */}
{isEditing && editForm && (
<Card>
<CardHeader>
<CardTitle>
{editForm.type === 'character' ? 'Edit Character' : 'Edit Item'}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium mb-2 block">Name</label>
<Input
value={(editForm.data as any).name || ''}
onChange={(e) => setEditForm({
...editForm,
data: { ...editForm.data, name: e.target.value }
})}
/>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Description</label>
<Input
value={(editForm.data as any).description || ''}
onChange={(e) => setEditForm({
...editForm,
data: { ...editForm.data, description: e.target.value }
})}
/>
</div>
{editForm.type === 'character' && (
<div className="grid md:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium mb-2 block">Hair</label>
<Input
value={(editForm.data as CharacterConsistency).features.hair || ''}
onChange={(e) => setEditForm({
...editForm,
data: {
...editForm.data,
features: {
...editForm.data.features,
hair: e.target.value
}
}
})}
/>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Eyes</label>
<Input
value={(editForm.data as CharacterConsistency).features.eyes || ''}
onChange={(e) => setEditForm({
...editForm,
data: {
...editForm.data,
features: {
...editForm.data.features,
eyes: e.target.value
}
}
})}
/>
</div>
</div>
)}
{editForm.type === 'item' && (
<div className="grid md:grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium mb-2 block">Color</label>
<Input
value={(editForm.data as ItemConsistency).features.color || ''}
onChange={(e) => setEditForm({
...editForm,
data: {
...editForm.data,
features: {
...editForm.data.features,
color: e.target.value
}
}
})}
/>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Material</label>
<Input
value={(editForm.data as ItemConsistency).features.material || ''}
onChange={(e) => setEditForm({
...editForm,
data: {
...editForm.data,
features: {
...editForm.data.features,
material: e.target.value
}
}
})}
/>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Size</label>
<Input
value={(editForm.data as ItemConsistency).features.size || ''}
onChange={(e) => setEditForm({
...editForm,
data: {
...editForm.data,
features: {
...editForm.data.features,
size: e.target.value
}
}
})}
/>
</div>
</div>
)}
<div className="flex gap-2">
<Button onClick={saveEdit}>Save</Button>
<Button variant="outline" onClick={() => setIsEditing(false)}>Cancel</Button>
</div>
</CardContent>
</Card>
)}
{/* Actions */}
<div className="flex justify-end">
<Button onClick={updateConsistencyProfile} className="btn-primary">
Update Consistency Profile
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,203 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Plus, Trash2, Sparkles, Wand2, Layers } from "lucide-react"
interface SeriesConfig {
id: string
title: string
description: string
imageCount: number
style: string
prompts: string[]
}
export function Creator() {
const [prompt, setPrompt] = useState("")
const [series, setSeries] = useState<SeriesConfig[]>([{
id: "1",
title: "My First Series",
description: "An epic adventure story",
imageCount: 5,
style: "anime",
prompts: []
}])
const addSeries = () => {
const newSeries: SeriesConfig = {
id: Date.now().toString(),
title: `Series ${series.length + 1}`,
description: "Enter series description",
imageCount: 5,
style: "realistic",
prompts: []
}
setSeries([...series, newSeries])
}
const removeSeries = (id: string) => {
setSeries(series.filter(s => s.id !== id))
}
return (
<section className="container mx-auto px-4 py-16">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold mb-4">Create Your Image Series</h2>
<p className="text-gray-600">
Describe your vision and configure your story series
</p>
</div>
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5" />
Image Prompt
</CardTitle>
<CardDescription>
Describe the image you want to create. Be detailed and specific
for best results.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<Input
placeholder="A beautiful magical forest with glowing trees and floating islands..."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="min-h-[100px] resize-none"
/>
<div className="flex justify-end">
<Button className="bg-blue-600 text-white hover:bg-blue-700">
<Wand2 className="mr-2 h-4 w-4" />
Generate Ideas
</Button>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Layers className="h-5 w-5" />
Series Configuration
</div>
<Button variant="outline" size="sm" onClick={addSeries}>
<Plus className="h-4 w-4 mr-2" />
Add Series
</Button>
</CardTitle>
<CardDescription>
Configure up to 30 images per series with consistent characters
and narrative flow.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{series.map((item) => (
<div key={item.id} className="border border-gray-200 rounded-lg p-4 space-y-4">
<div className="flex justify-between items-start">
<div className="flex-1 space-y-3">
<Input
placeholder="Series title"
value={item.title}
onChange={(e) => {
const updated = series.map(s =>
s.id === item.id ? {...s, title: e.target.value} : s
)
setSeries(updated)
}}
/>
<Input
placeholder="Series description"
value={item.description}
onChange={(e) => {
const updated = series.map(s =>
s.id === item.id ? {...s, description: e.target.value} : s
)
setSeries(updated)
}}
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => removeSeries(item.id)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium mb-2 block">
Number of Images
</label>
<Input
type="number"
min="1"
max="30"
value={item.imageCount}
onChange={(e) => {
const updated = series.map(s =>
s.id === item.id ? {...s, imageCount: parseInt(e.target.value)} : s
)
setSeries(updated)
}}
/>
</div>
<div>
<label className="text-sm font-medium mb-2 block">
Art Style
</label>
<select
className="flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm"
value={item.style}
onChange={(e) => {
const updated = series.map(s =>
s.id === item.id ? {...s, style: e.target.value} : s
)
setSeries(updated)
}}
>
<option value="realistic">Realistic</option>
<option value="anime">Anime</option>
<option value="cartoon">Cartoon</option>
<option value="fantasy">Fantasy</option>
<option value="cyberpunk">Cyberpunk</option>
</select>
</div>
</div>
</div>
))}
</div>
<div className="mt-6 pt-6 border-t border-gray-200">
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
Total images: {series.reduce((sum, item) => sum + item.imageCount, 0)}
</div>
<div className="space-x-2">
<Button variant="outline">
Save Draft
</Button>
<Button className="bg-blue-600 text-white hover:bg-blue-700">
<Sparkles className="mr-2 h-4 w-4" />
Generate Series
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</section>
)
}

View File

@@ -0,0 +1,76 @@
import { Sparkles, Github, Twitter, Mail } from "lucide-react"
export function Footer() {
return (
<footer className="border-t bg-white">
<div className="container mx-auto px-4 py-12">
<div className="grid md:grid-cols-4 gap-8">
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Sparkles className="h-6 w-6 bg-gradient-to-r from-blue-600 to-purple-600" />
<span className="font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">NanoJason</span>
</div>
<p className="text-sm text-gray-600">
Transform your ideas into stunning visual stories with AI-powered image generation.
</p>
<div className="flex space-x-4">
<a href="#" className="text-gray-600 hover:text-gray-900">
<Github className="h-4 w-4" />
<span className="sr-only">GitHub</span>
</a>
<a href="#" className="text-gray-600 hover:text-gray-900">
<Twitter className="h-4 w-4" />
<span className="sr-only">Twitter</span>
</a>
<a href="#" className="text-gray-600 hover:text-gray-900">
<Mail className="h-4 w-4" />
<span className="sr-only">Email</span>
</a>
</div>
</div>
<div className="space-y-4">
<h4 className="text-sm font-semibold">Product</h4>
<ul className="space-y-2 text-sm text-gray-600">
<li><a href="#" className="hover:text-gray-900">Features</a></li>
<li><a href="#" className="hover:text-gray-900">Gallery</a></li>
<li><a href="#" className="hover:text-gray-900">API</a></li>
<li><a href="#" className="hover:text-gray-900">Pricing</a></li>
</ul>
</div>
<div className="space-y-4">
<h4 className="text-sm font-semibold">Resources</h4>
<ul className="space-y-2 text-sm text-gray-600">
<li><a href="#" className="hover:text-gray-900">Documentation</a></li>
<li><a href="#" className="hover:text-gray-900">Tutorials</a></li>
<li><a href="#" className="hover:text-gray-900">Blog</a></li>
<li><a href="#" className="hover:text-gray-900">Community</a></li>
</ul>
</div>
<div className="space-y-4">
<h4 className="text-sm font-semibold">Company</h4>
<ul className="space-y-2 text-sm text-gray-600">
<li><a href="#" className="hover:text-gray-900">About</a></li>
<li><a href="#" className="hover:text-gray-900">Contact</a></li>
<li><a href="#" className="hover:text-gray-900">Privacy</a></li>
<li><a href="#" className="hover:text-gray-900">Terms</a></li>
</ul>
</div>
</div>
<div className="mt-8 pt-8 border-t border-gray-200">
<div className="flex flex-col md:flex-row justify-between items-center">
<p className="text-sm text-gray-600">
© 2024 NanoJason. All rights reserved.
</p>
<p className="text-sm text-gray-600 mt-2 md:mt-0">
Powered by Qwen AI and Next.js
</p>
</div>
</div>
</div>
</footer>
)
}

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