Initial commit from ux_aura_central
This commit is contained in:
128
.gitea/workflows/feature_branch_autopr.yml
Normal file
128
.gitea/workflows/feature_branch_autopr.yml
Normal file
@@ -0,0 +1,128 @@
|
||||
name: Auto-PR feature -> playtest and merge
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "feature-**"
|
||||
jobs:
|
||||
pr_merge:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:20-bullseye
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
# Change if your instance URL differs
|
||||
GITEA_BASE_URL: https://git.code.svchub.com
|
||||
PLAYTEST_BRANCH: playtest
|
||||
|
||||
# Provide this as a Gitea Actions secret in the repo (Settings -> Actions -> Secrets)
|
||||
# Must have rights to create PRs, merge PRs, and delete branches.
|
||||
GITEA_TOKEN: ${{ vars.GIT_TOKEN }}
|
||||
|
||||
# Repo + branch from the event context (GitHub-compatible in Gitea Actions)
|
||||
REPO: ${{ github.repository }} # "owner/name"
|
||||
BRANCH: ${{ github.ref_name }} # e.g. "feature/issue-123"
|
||||
steps:
|
||||
- name: Install prerequisites (rsync, jq, git)
|
||||
run: |
|
||||
set -eu
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -y
|
||||
apt-get install -y rsync jq git ca-certificates curl
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache rsync jq git ca-certificates curl
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y rsync jq git ca-certificates curl
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y rsync jq git ca-certificates curl
|
||||
else
|
||||
echo "Unsupported base image"; exit 1
|
||||
fi
|
||||
|
||||
- name: Create PR (if needed), merge with merge-commit, delete branch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
: "${GITEA_TOKEN:?Missing secrets.GITEA_TOKEN}"
|
||||
: "${REPO:?Missing repo context}"
|
||||
: "${BRANCH:?Missing branch context}"
|
||||
|
||||
OWNER="${REPO%%/*}"
|
||||
NAME="${REPO##*/}"
|
||||
|
||||
API="${GITEA_BASE_URL%/}/api/v1"
|
||||
AUTH="Authorization: token ${GITEA_TOKEN}"
|
||||
|
||||
FEATURE_BRANCH="${BRANCH}"
|
||||
BASE_BRANCH="${PLAYTEST_BRANCH}"
|
||||
|
||||
echo "[INFO] Repo: ${OWNER}/${NAME}"
|
||||
echo "[INFO] Feature branch: ${FEATURE_BRANCH}"
|
||||
echo "[INFO] Base branch: ${BASE_BRANCH}"
|
||||
|
||||
# 1) Create PR from feature branch to playtest
|
||||
echo "[INFO] Creating PR from ${FEATURE_BRANCH} → ${BASE_BRANCH}"
|
||||
|
||||
PR_PAYLOAD="$(jq -n \
|
||||
--arg title "Auto-merge ${FEATURE_BRANCH} -> ${BASE_BRANCH}" \
|
||||
--arg head "${FEATURE_BRANCH}" \
|
||||
--arg base "${BASE_BRANCH}" \
|
||||
--arg body "Automated PR created on push to ${FEATURE_BRANCH}" \
|
||||
'{title:$title, head:$head, base:$base, body:$body}')"
|
||||
|
||||
CREATE_URL="${API}/repos/${OWNER}/${NAME}/pulls"
|
||||
CREATE_HTTP="$(curl -sS -o /tmp/pr_create.json -w "%{http_code}" \
|
||||
-H "Content-Type: application/json" -H "$AUTH" \
|
||||
-X POST -d "$PR_PAYLOAD" \
|
||||
"$CREATE_URL" || true)"
|
||||
|
||||
if [ "$CREATE_HTTP" != "201" ]; then
|
||||
echo "[ERROR] PR create failed (HTTP $CREATE_HTTP):"
|
||||
cat /tmp/pr_create.json || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER="$(jq -r '.number' /tmp/pr_create.json)"
|
||||
echo "[INFO] Created PR #${PR_NUMBER}"
|
||||
|
||||
# 2) Merge PR using a merge commit ("Do": "merge")
|
||||
# Branch deletion is handled separately in step 4 with safety checks
|
||||
MERGE_PAYLOAD="$(jq -n \
|
||||
--arg do "merge" \
|
||||
--arg title "Merge ${FEATURE_BRANCH} into ${BASE_BRANCH}" \
|
||||
--arg message "Merged automatically from ${FEATURE_BRANCH}" \
|
||||
'{Do:$do, MergeTitleField:$title, MergeMessageField:$message}')"
|
||||
|
||||
MERGE_URL="${API}/repos/${OWNER}/${NAME}/pulls/${PR_NUMBER}/merge"
|
||||
MERGE_HTTP="$(curl -sS -o /tmp/pr_merge.json -w "%{http_code}" \
|
||||
-H "Content-Type: application/json" -H "$AUTH" \
|
||||
-X POST -d "$MERGE_PAYLOAD" \
|
||||
"$MERGE_URL" || true)"
|
||||
|
||||
if [ "$MERGE_HTTP" != "200" ] && [ "$MERGE_HTTP" != "201" ] && [ "$MERGE_HTTP" != "204" ]; then
|
||||
echo "[ERROR] PR merge failed (HTTP $MERGE_HTTP):"
|
||||
cat /tmp/pr_merge.json || true
|
||||
exit 1
|
||||
fi
|
||||
echo "[INFO] PR #${PR_NUMBER} merged."
|
||||
|
||||
# 4) Ensure branch is deleted (in case delete-after-merge is disabled)
|
||||
# Safety check: only delete if branch name starts with "feature-" or "bugfix-"
|
||||
if [[ "${FEATURE_BRANCH}" == feature-* ]] || [[ "${FEATURE_BRANCH}" == bugfix-* ]]; then
|
||||
echo "[INFO] Deleting temporary branch: ${FEATURE_BRANCH}"
|
||||
DEL_URL="${API}/repos/${OWNER}/${NAME}/branches/${FEATURE_BRANCH}"
|
||||
DEL_HTTP="$(curl -sS -o /tmp/branch_del.json -w "%{http_code}" \
|
||||
-H "$AUTH" -X DELETE \
|
||||
"$DEL_URL" || true)"
|
||||
|
||||
if [ "$DEL_HTTP" = "204" ] || [ "$DEL_HTTP" = "200" ]; then
|
||||
echo "[INFO] Branch ${FEATURE_BRANCH} deleted."
|
||||
else
|
||||
echo "[WARN] Branch delete returned HTTP $DEL_HTTP (may already be deleted or not permitted)."
|
||||
cat /tmp/branch_del.json || true
|
||||
fi
|
||||
else
|
||||
echo "[WARN] Branch ${FEATURE_BRANCH} does not match feature-* pattern. Skipping deletion for safety."
|
||||
fi
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
12
App.tsx
Normal file
12
App.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import WorkspaceContainer from './features/workspace/WorkspaceContainer';
|
||||
|
||||
/**
|
||||
* App is the root-level entry point.
|
||||
* It delegates to specific feature containers to maintain a clean top-level structure.
|
||||
*/
|
||||
const App: React.FC = () => {
|
||||
return <WorkspaceContainer />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Use Node.js LTS version
|
||||
FROM node:20-bookworm-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Expose the port Vite runs on
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the development server
|
||||
CMD ["npm", "run", "dev"]
|
||||
330
FIXED_cloudflare_build.yml
Normal file
330
FIXED_cloudflare_build.yml
Normal file
@@ -0,0 +1,330 @@
|
||||
name: Cloudflare Worker AI Studio Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch name'
|
||||
default: 'main'
|
||||
clone_url:
|
||||
description: 'Clone URL of repo'
|
||||
required: true
|
||||
original_url:
|
||||
description: 'Original URL of repo'
|
||||
required: false
|
||||
use_original:
|
||||
description: 'Use original repo URL'
|
||||
default: 'false'
|
||||
concurrency:
|
||||
group: aistudio-build-group
|
||||
cancel: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
container:
|
||||
image: node:20-bullseye
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.event.inputs.branch || 'main' }}
|
||||
CLONE_URL: ${{ github.event.inputs.clone_url }}
|
||||
ORIGINAL_URL: ${{ github.event.inputs.original_url }}
|
||||
USE_ORIGINAL: ${{ github.event.inputs.use_original || 'false' }}
|
||||
GIT_USERNAME: ${{ vars.GIT_USERNAME }}
|
||||
GIT_TOKEN: ${{ vars.GIT_TOKEN }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ github.event.inputs.branch == 'eteam_prod' && secrets.CF_API_TOKEN_ETEAM || secrets.CF_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ github.event.inputs.branch == 'eteam_prod' && vars.CF_ACCOUNT_ID_ETEAM || vars.CF_ACCOUNT_ID }}
|
||||
|
||||
steps:
|
||||
- name: Log input values
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "===== INPUT DETAILS ====="
|
||||
echo "Branch: $BRANCH_NAME"
|
||||
echo "Clone URL: $CLONE_URL"
|
||||
echo "Original URL: ${ORIGINAL_URL:-}"
|
||||
echo "Use Original: $USE_ORIGINAL"
|
||||
echo "========================="
|
||||
|
||||
- name: Install prerequisites (rsync, jq, git)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "[ERROR] git not found in image; cannot proceed without apt. Use an image that includes git."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "[INFO] Installing jq (static binary)..."
|
||||
curl -fsSL -o /usr/local/bin/jq \
|
||||
https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64
|
||||
chmod +x /usr/local/bin/jq
|
||||
fi
|
||||
|
||||
if ! command -v rsync >/dev/null 2>&1; then
|
||||
echo "[WARN] rsync not found; continuing (workflow does not require rsync for core steps)."
|
||||
fi
|
||||
|
||||
jq --version
|
||||
git --version
|
||||
|
||||
- name: Clone repository
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARGET_URL="$CLONE_URL"
|
||||
if [ "$USE_ORIGINAL" = "true" ] && [ -n "${ORIGINAL_URL:-}" ]; then
|
||||
TARGET_URL="$ORIGINAL_URL"
|
||||
fi
|
||||
AUTH_URL="$(echo "$TARGET_URL" | sed "s#https://#https://$GIT_USERNAME:$GIT_TOKEN@#")"
|
||||
git clone --branch "$BRANCH_NAME" "$AUTH_URL" repo
|
||||
cd repo
|
||||
echo "[INFO] Repository cloned successfully."
|
||||
git remote -v
|
||||
|
||||
- name: Derive canonical repo name from origin and export
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ORIGIN_URL="$(git -C repo remote get-url origin)"
|
||||
DERIVED_REPO_NAME="$(basename "${ORIGIN_URL%.git}")"
|
||||
echo "[DEBUG] Derived repo name from origin: '${DERIVED_REPO_NAME}'"
|
||||
echo "REPO_NAME=${DERIVED_REPO_NAME}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Fetch metadata (slug + unique name) from AI Studio Manager API
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
ORIGIN_URL="$(git -C repo remote get-url origin)"
|
||||
OWNER_REPO="$(echo "$ORIGIN_URL" | awk -F[/:] '{print $(NF-1)"/"$NF}' | sed 's/\.git$//')"
|
||||
ENCODED_REPO_NAME="$(printf '%s' "$OWNER_REPO" | jq -s -R -r @uri)"
|
||||
|
||||
if [[ "$BRANCH_NAME" == "main" ]]; then
|
||||
MANAGER_API_URL="https://www.humanizeiq.ai"
|
||||
elif [[ "$BRANCH_NAME" == "eteam_prod" ]]; then
|
||||
MANAGER_API_URL="https://www.humanizeiq.ai"
|
||||
elif [[ "$BRANCH_NAME" == "dev" ]]; then
|
||||
MANAGER_API_URL="https://www.dev.humanizeiq.ai"
|
||||
else
|
||||
MANAGER_API_URL="https://www.playtest.humanizeiq.ai"
|
||||
fi
|
||||
|
||||
echo "[INFO] Using Manager API URL: $MANAGER_API_URL"
|
||||
echo "[INFO] Querying AI Studio Manager API for repoName=$OWNER_REPO"
|
||||
|
||||
RESPONSE="$(curl -sS -X GET \
|
||||
"${MANAGER_API_URL}/api/ai_studio_manager_api/app-builder/components/by-repo?repoName=${ENCODED_REPO_NAME}" \
|
||||
-H 'accept: application/json' \
|
||||
-H 'X-API-Key: system-key' || true)"
|
||||
|
||||
echo "[DEBUG] API response: ${RESPONSE:-<empty>}"
|
||||
|
||||
SLUG="$(echo "${RESPONSE:-}" | jq -r '.additional_info.slug' 2>/dev/null || echo "")"
|
||||
UNIQUE_APP_CODE="$(echo "${RESPONSE:-}" | jq -r '.additional_info.unique_app_code' 2>/dev/null || echo "")"
|
||||
|
||||
if [ -z "${SLUG:-}" ] || [ "${SLUG:-}" = "null" ]; then
|
||||
echo "[WARN] slug missing; falling back to REPO_NAME#gais_."
|
||||
BASE_NAME_VALUE="${REPO_NAME#gais_}"
|
||||
else
|
||||
BASE_NAME_VALUE="$SLUG"
|
||||
fi
|
||||
|
||||
if [ -z "${UNIQUE_APP_CODE:-}" ] || [ "${UNIQUE_APP_CODE:-}" = "null" ]; then
|
||||
echo "[WARN] unique_app_code missing; falling back to REPO_NAME."
|
||||
UNIQUE_NAME_VALUE="${REPO_NAME}"
|
||||
else
|
||||
UNIQUE_NAME_VALUE="${UNIQUE_APP_CODE}"
|
||||
fi
|
||||
|
||||
echo "BASE_NAME=${BASE_NAME_VALUE}" >> "$GITHUB_ENV"
|
||||
echo "UNIQUE_NAME=${UNIQUE_NAME_VALUE}" >> "$GITHUB_ENV"
|
||||
echo "MANAGER_API_URL=${MANAGER_API_URL}" >> "$GITHUB_ENV"
|
||||
|
||||
echo "[INFO] Using BASE_NAME=${BASE_NAME_VALUE}"
|
||||
echo "[INFO] Using UNIQUE_NAME=${UNIQUE_NAME_VALUE} for Worker base"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: repo
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm ci || npm install
|
||||
|
||||
- name: Build AI Studio / Vite project
|
||||
working-directory: repo
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE_NAME="${BASE_NAME:-${REPO_NAME#gais_}}"
|
||||
BASE_PATH="/${BASE_NAME}/"
|
||||
printf '[INFO] Using base path: %s\n' "$BASE_PATH"
|
||||
npm run build --if-present -- --base "$BASE_PATH"
|
||||
|
||||
- name: Copy JSON assets into dist
|
||||
working-directory: repo
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp -v ./metadata.json dist/ || echo "No JSON files found in ./config"
|
||||
|
||||
- name: Install Wrangler CLI
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm install -g wrangler
|
||||
|
||||
- name: Deploy Cloudflare Worker
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ env.BRANCH_NAME == 'eteam_prod' && secrets.CF_API_TOKEN_ETEAM || secrets.CF_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ env.BRANCH_NAME == 'eteam_prod' && vars.CF_ACCOUNT_ID_ETEAM || vars.CF_ACCOUNT_ID }}
|
||||
working-directory: repo
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SAFE_BRANCH="${BRANCH_NAME//\//-}"
|
||||
if [ "$SAFE_BRANCH" = "playtest" ]; then
|
||||
SAFE_BRANCH="pt"
|
||||
fi
|
||||
|
||||
SLUG="${BASE_NAME:-${REPO_NAME#gais_}}"
|
||||
WORKER_NAME="gais_${SLUG}_${SAFE_BRANCH}"
|
||||
|
||||
MAX_LENGTH=54
|
||||
if [ ${#WORKER_NAME} -gt $MAX_LENGTH ]; then
|
||||
echo "[WARN] Worker name '${WORKER_NAME}' is ${#WORKER_NAME} chars (max: ${MAX_LENGTH})"
|
||||
HASH=$(echo -n "$WORKER_NAME" | md5sum | cut -c1-8)
|
||||
MAX_SLUG_LEN=$((39 - ${#SAFE_BRANCH}))
|
||||
TRUNCATED_SLUG="${SLUG:0:$MAX_SLUG_LEN}"
|
||||
WORKER_NAME="gais_${TRUNCATED_SLUG}_${HASH}_${SAFE_BRANCH}"
|
||||
echo "[INFO] Truncated to: ${WORKER_NAME} (${#WORKER_NAME} chars)"
|
||||
fi
|
||||
|
||||
echo "[INFO] Using Worker name: ${WORKER_NAME}"
|
||||
|
||||
echo "[INFO] Writing wrangler.json..."
|
||||
cat > wrangler.json <<'EOF'
|
||||
{
|
||||
"name": "PLACEHOLDER",
|
||||
"compatibility_date": "1970-01-01",
|
||||
"workers_dev": true
|
||||
}
|
||||
EOF
|
||||
|
||||
jq \
|
||||
--arg name "$WORKER_NAME" \
|
||||
--arg date "$(date +%Y-%m-%d)" \
|
||||
'.name=$name | .compatibility_date=$date' \
|
||||
wrangler.json > wrangler.tmp && mv wrangler.tmp wrangler.json
|
||||
|
||||
echo "[INFO] Verifying Wrangler auth..."
|
||||
wrangler whoami
|
||||
|
||||
echo "[INFO] Deploying..."
|
||||
echo "[INFO] CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID}"
|
||||
wrangler deploy --assets ./dist
|
||||
|
||||
- name: Update Route in Traefik Database
|
||||
if: success()
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
SAFE_BRANCH="${BRANCH_NAME//\//-}"
|
||||
if [ "$SAFE_BRANCH" = "playtest" ] || [ "$SAFE_BRANCH" = "pt" ]; then
|
||||
ENVIRONMENT="playtest"
|
||||
elif [ "$SAFE_BRANCH" = "dev" ]; then
|
||||
ENVIRONMENT="nonprod"
|
||||
elif [ "$SAFE_BRANCH" = "main" ]; then
|
||||
ENVIRONMENT="prod"
|
||||
elif [ "$SAFE_BRANCH" = "eteam_prod" ]; then
|
||||
ENVIRONMENT="prod"
|
||||
else
|
||||
echo "[WARN] Unknown branch: $SAFE_BRANCH, defaulting to playtest"
|
||||
ENVIRONMENT="playtest"
|
||||
fi
|
||||
|
||||
echo "[INFO] Updating route for environment: $ENVIRONMENT"
|
||||
|
||||
APP_NAME="${BASE_NAME}"
|
||||
|
||||
echo "[INFO] Calling route update API for app: $APP_NAME using $MANAGER_API_URL"
|
||||
|
||||
ROUTE_RESPONSE="$(curl -sS -X POST \
|
||||
"${MANAGER_API_URL}/api/ai_studio_manager_api/app-builder/create-route" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'X-API-Key: system-key' \
|
||||
-d "{\"appName\":\"${APP_NAME}\",\"environment\":\"${ENVIRONMENT}\"}" || echo '{"status":"error","message":"API call failed"}')"
|
||||
|
||||
echo "[INFO] Route update response: ${ROUTE_RESPONSE}"
|
||||
|
||||
if echo "$ROUTE_RESPONSE" | jq -e '.status == "success"' > /dev/null 2>&1; then
|
||||
ROUTE_URL="$(echo "$ROUTE_RESPONSE" | jq -r '.url')"
|
||||
echo "[INFO] ✓ Route updated successfully: $ROUTE_URL"
|
||||
else
|
||||
ERROR_MSG="$(echo "$ROUTE_RESPONSE" | jq -r '.message // "Unknown error"')"
|
||||
echo "[WARN] Route update may have failed: $ERROR_MSG"
|
||||
echo "[WARN] Continuing workflow - route can be updated manually if needed"
|
||||
fi
|
||||
|
||||
- name: Create PR after successful build (NO MERGE)
|
||||
if: success()
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd repo
|
||||
|
||||
ORIGIN_URL="$(git remote get-url origin)"
|
||||
ORIGIN_URL="${ORIGIN_URL%.git}"
|
||||
REPO_OWNER="$(echo "$ORIGIN_URL" | awk -F'/' '{print $(NF-1)}')"
|
||||
REPO_NAME_ONLY="$(echo "$ORIGIN_URL" | awk -F'/' '{print $NF}')"
|
||||
|
||||
GITEA_API="https://git.code.svchub.com/api/v1"
|
||||
AUTH_HEADER="Authorization: token ${GIT_TOKEN}"
|
||||
CURRENT_BRANCH="${BRANCH_NAME}"
|
||||
|
||||
# Determine PR direction based on current branch
|
||||
if [ "$CURRENT_BRANCH" = "playtest" ]; then
|
||||
FROM_BRANCH="playtest"
|
||||
TO_BRANCH="dev"
|
||||
elif [ "$CURRENT_BRANCH" = "dev" ]; then
|
||||
FROM_BRANCH="dev"
|
||||
TO_BRANCH="main"
|
||||
elif [ "$CURRENT_BRANCH" = "main" ]; then
|
||||
if git ls-remote --exit-code --heads origin eteam_prod >/dev/null 2>&1; then
|
||||
FROM_BRANCH="main"
|
||||
TO_BRANCH="eteam_prod"
|
||||
else
|
||||
echo "[INFO] No PR rule for branch: $CURRENT_BRANCH (eteam_prod does not exist)"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "[INFO] No PR rule for branch: $CURRENT_BRANCH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[INFO] Checking if PR already exists: ${FROM_BRANCH} → ${TO_BRANCH}"
|
||||
|
||||
# Check if PR already exists
|
||||
LIST_URL="${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME_ONLY}/pulls?state=open&base=${TO_BRANCH}&head=${REPO_OWNER}:${FROM_BRANCH}"
|
||||
EXISTING_PR="$(curl -sS -H "$AUTH_HEADER" "$LIST_URL" | jq -r '.[0].number // empty')"
|
||||
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "[INFO] PR already exists: #${EXISTING_PR} (${FROM_BRANCH} → ${TO_BRANCH})"
|
||||
echo "[INFO] Skipping PR creation to avoid duplicates"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[INFO] Creating PR from ${FROM_BRANCH} → ${TO_BRANCH}"
|
||||
PR_PAYLOAD=$(jq -n \
|
||||
--arg title "Auto PR: ${FROM_BRANCH} → ${TO_BRANCH}" \
|
||||
--arg head "$FROM_BRANCH" \
|
||||
--arg base "$TO_BRANCH" \
|
||||
--arg body "Automated PR created after successful build on ${FROM_BRANCH}" \
|
||||
'{title:$title, head:$head, base:$base, body:$body}')
|
||||
|
||||
HTTP_CODE=$(curl -s -o /tmp/resp.json -w "%{http_code}" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "$AUTH_HEADER" \
|
||||
-d "$PR_PAYLOAD" \
|
||||
"${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME_ONLY}/pulls" || true)
|
||||
|
||||
if [ "$HTTP_CODE" != "201" ]; then
|
||||
echo "[WARN] PR creation failed or PR already exists (HTTP $HTTP_CODE). Response:"
|
||||
cat /tmp/resp.json || true
|
||||
else
|
||||
PR_NUMBER="$(jq -r '.number' /tmp/resp.json)"
|
||||
echo "[INFO] ✓ PR #${PR_NUMBER} created: ${FROM_BRANCH} → ${TO_BRANCH}"
|
||||
fi
|
||||
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/9f4101c2-8730-451b-9134-3b46f26726c6
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
1
References/api_storage.json
Normal file
1
References/api_storage.json
Normal file
File diff suppressed because one or more lines are too long
1
References/manager_api.json
Normal file
1
References/manager_api.json
Normal file
File diff suppressed because one or more lines are too long
106
components/Dashboard.tsx
Normal file
106
components/Dashboard.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AppMetadata } from '../types';
|
||||
import { geminiService } from '../services/geminiService';
|
||||
|
||||
interface DashboardProps {
|
||||
metadata: AppMetadata;
|
||||
}
|
||||
|
||||
export const Dashboard: React.FC<DashboardProps> = ({ metadata }) => {
|
||||
const [greeting, setGreeting] = useState('Loading welcome message...');
|
||||
const [features, setFeatures] = useState<string[]>([]);
|
||||
const [isAiLoading, setIsAiLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const initializeApp = async () => {
|
||||
setIsAiLoading(true);
|
||||
try {
|
||||
const [aiGreeting, aiFeatures] = await Promise.all([
|
||||
geminiService.getAppContextualGreeting(metadata),
|
||||
geminiService.getSmartFeatureIdeas(metadata)
|
||||
]);
|
||||
setGreeting(aiGreeting);
|
||||
setFeatures(aiFeatures);
|
||||
} catch (error) {
|
||||
console.error("AI Initialization failed", error);
|
||||
setGreeting(`Welcome to ${metadata.name}`);
|
||||
} finally {
|
||||
setIsAiLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initializeApp();
|
||||
}, [metadata]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 p-8 overflow-hidden relative">
|
||||
<div className="absolute top-0 right-0 -mr-16 -mt-16 w-64 h-64 bg-blue-50 rounded-full opacity-50"></div>
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-gray-500 text-sm font-semibold uppercase tracking-wider mb-2">Identity</h2>
|
||||
<h3 className="text-4xl font-extrabold text-gray-900 mb-4">{metadata.name}</h3>
|
||||
<p className="text-lg text-gray-600 leading-relaxed max-w-2xl">{metadata.description}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="md:col-span-2 bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||||
<h4 className="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<span className="p-1 bg-blue-100 rounded text-blue-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
Smart Assistant Greeting
|
||||
</h4>
|
||||
<div className={`p-4 bg-gray-50 rounded-xl border border-gray-100 italic text-gray-700 transition-opacity duration-500 ${isAiLoading ? 'opacity-50' : 'opacity-100'}`}>
|
||||
"{greeting}"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||||
<h4 className="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<span className="p-1 bg-purple-100 rounded text-purple-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M11 3a1 1 0 10-2 0v1a1 1 0 102 0V3zM15.657 5.757a1 1 0 00-1.414-1.414l-.707.707a1 1 0 001.414 1.414l.707-.707zM18 10a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM5.05 6.464A1 1 0 106.464 5.05l-.707-.707a1 1 0 00-1.414 1.414l.707.707zM5 10a1 1 0 01-1 1H3a1 1 0 110-2h1a1 1 0 011 1zM8 16v-1a1 1 0 112 0v1a1 1 0 11-2 0zM13.536 14.95a1 1 0 010-1.414l.707-.707a1 1 0 011.414 1.414l-.707.707a1 1 0 01-1.414 0zM6.464 14.95a1 1 0 01-1.414 0l-.707-.707a1 1 0 011.414-1.414l.707.707a1 1 0 010 1.414z" />
|
||||
</svg>
|
||||
</span>
|
||||
Dynamic Smart Features
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{isAiLoading ? (
|
||||
[1, 2, 3].map(i => <li key={i} className="h-8 bg-gray-100 animate-pulse rounded"></li>)
|
||||
) : (
|
||||
features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-center gap-2 text-gray-700 bg-gray-50 p-2 rounded-lg border border-gray-100 hover:border-purple-200 hover:bg-purple-50 transition-colors cursor-default">
|
||||
<div className="w-2 h-2 rounded-full bg-purple-500"></div>
|
||||
{feature}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-8">
|
||||
<h4 className="text-xl font-bold text-gray-900 mb-6">Core Capabilities</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: 'Data Tracking', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
|
||||
{ label: 'Cloud Sync', icon: 'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z' },
|
||||
{ label: 'AI Optimization', icon: 'M13 10V3L4 14h7v7l9-11h-7z' },
|
||||
{ label: 'Privacy First', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' },
|
||||
].map((item, idx) => (
|
||||
<div key={idx} className="group p-6 rounded-xl bg-gray-50 border border-gray-100 hover:bg-blue-600 hover:text-white transition-all duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 mb-4 text-blue-600 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon} />
|
||||
</svg>
|
||||
<h5 className="font-bold">{item.label}</h5>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
43
components/Layout.tsx
Normal file
43
components/Layout.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const Layout: React.FC<LayoutProps> = ({ children, title }) => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-gray-50">
|
||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16 items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white font-bold">
|
||||
{title.charAt(0)}
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-gray-900 tracking-tight">
|
||||
{title === 'NOTSET' ? 'App Template' : title}
|
||||
</h1>
|
||||
</div>
|
||||
<nav className="hidden md:flex space-x-8">
|
||||
<a href="#" className="text-gray-500 hover:text-gray-900 text-sm font-medium">Dashboard</a>
|
||||
<a href="#" className="text-gray-500 hover:text-gray-900 text-sm font-medium">Insights</a>
|
||||
<a href="#" className="text-gray-500 hover:text-gray-900 text-sm font-medium">Settings</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-grow">
|
||||
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<footer className="bg-white border-t border-gray-200 py-4">
|
||||
<div className="max-w-7xl mx-auto px-4 text-center text-gray-400 text-xs">
|
||||
Powered by Gemini AI & Dynamic Metadata Template
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
36
components/SetupPrompt.tsx
Normal file
36
components/SetupPrompt.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const SetupPrompt: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 px-4 text-center">
|
||||
<div className="bg-amber-100 p-4 rounded-full mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">Configuration Required</h2>
|
||||
<p className="text-gray-600 max-w-md mb-8">
|
||||
The application metadata is currently set to <code className="bg-gray-100 px-1 rounded text-red-600">NOTSET</code>.
|
||||
Please update the <span className="font-mono font-semibold">metadata.json</span> file with your app's name and description to proceed.
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-900 text-gray-100 p-6 rounded-lg shadow-xl text-left font-mono text-sm max-w-xl w-full">
|
||||
<div className="flex items-center gap-2 mb-4 border-b border-gray-700 pb-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
||||
<span className="ml-2 text-gray-400">metadata.json</span>
|
||||
</div>
|
||||
<pre>{`{
|
||||
"name": "Your Awesome App",
|
||||
"description": "A powerful tool to help users manage..."
|
||||
}`}</pre>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-sm text-gray-500 italic">
|
||||
The AI will automatically generate features and interfaces based on your definition once you reload.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
172
components/icons.tsx
Normal file
172
components/icons.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const SunIcon: React.FC = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MoonIcon: React.FC = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PaperAirplaneIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className || "w-6 h-6"}>
|
||||
<path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PaperClipIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const XMarkIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const GithubIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className={className || "w-6 h-6"}>
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ExternalLinkIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className={className || "h-4 w-4 inline-block ml-1"} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CodeIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 12" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const EditIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-4 h-4"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SparklesIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CheckCircleIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const EyeIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const UploadIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DownloadIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M7.5 12.75l4.5 4.5m0 0l4.5-4.5m-4.5 4.5v-13.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ClipboardIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ListBulletIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DocumentTextIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PrinterIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015-1.837-2.175a48.041 48.041 0 00-1.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CommandLineIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const UserPlusIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM3 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 019.374 21c-2.331 0-4.512-.645-6.374-1.766z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FolderIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-5 h-5"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PlusIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-5 h-5"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ChatBubbleLeftIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-5 h-5"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ChevronRightIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-4 h-4"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ChevronDownIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-4 h-4"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const TrashIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-4 h-4"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const BookOpenIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Cog6ToothIcon: React.FC<{className?: string}> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.217.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
44
containers/MainContainer.tsx
Normal file
44
containers/MainContainer.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { User } from '../types';
|
||||
import MainLayout from './layout/MainLayout';
|
||||
|
||||
interface MainContainerProps {
|
||||
user: User | null;
|
||||
workspaceUrl: string;
|
||||
signOutUrl: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* MainContainer handles layout logic:
|
||||
* - Theme state access
|
||||
* - App version fetching
|
||||
*/
|
||||
const MainContainer: React.FC<MainContainerProps> = (props) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const [version, setVersion] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('./version.json')
|
||||
.then(response => {
|
||||
if (response.ok) return response.json();
|
||||
return null;
|
||||
})
|
||||
.then(data => {
|
||||
if (data && data.version) setVersion(data.version);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MainLayout
|
||||
{...props}
|
||||
theme={theme}
|
||||
toggleTheme={toggleTheme}
|
||||
version={version}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainContainer;
|
||||
49
containers/layout/MainLayout.tsx
Normal file
49
containers/layout/MainLayout.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { User } from '../../types';
|
||||
import Header from './components/Header';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
interface MainLayoutProps {
|
||||
user: User | null;
|
||||
workspaceUrl: string;
|
||||
signOutUrl: string;
|
||||
theme: 'light' | 'dark';
|
||||
toggleTheme: () => void;
|
||||
version: string | null;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
user,
|
||||
workspaceUrl,
|
||||
signOutUrl,
|
||||
theme,
|
||||
toggleTheme,
|
||||
version,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<div className="relative h-screen w-full flex flex-col bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 overflow-hidden text-sm sm:text-base">
|
||||
<Header
|
||||
user={user}
|
||||
workspaceUrl={workspaceUrl}
|
||||
signOutUrl={signOutUrl}
|
||||
theme={theme}
|
||||
toggleTheme={toggleTheme}
|
||||
/>
|
||||
|
||||
<main className="relative z-10 flex-1 flex flex-col overflow-hidden">
|
||||
{children || (
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center">
|
||||
<h2 className="text-2xl font-bold mb-4">Framework Ready</h2>
|
||||
<p className="text-slate-500 max-w-md">The skeleton is successfully initialized.</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<Footer version={version} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
15
containers/layout/components/Footer.tsx
Normal file
15
containers/layout/components/Footer.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
interface FooterProps {
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
const Footer: React.FC<FooterProps> = ({ version }) => {
|
||||
return (
|
||||
<footer className="flex-shrink-0 relative z-10 w-full p-3 text-center text-slate-600 dark:text-slate-400 text-xs border-t border-slate-200 dark:border-slate-800">
|
||||
<p>© 2025 HumanizeIQ. All Rights Reserved. {version && `v${version}`}</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
47
containers/layout/components/Header.tsx
Normal file
47
containers/layout/components/Header.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { User } from '../../../types';
|
||||
import { SunIcon, MoonIcon } from '../../../components/icons';
|
||||
|
||||
interface HeaderProps {
|
||||
user: User | null;
|
||||
workspaceUrl: string;
|
||||
signOutUrl: string;
|
||||
theme: 'light' | 'dark';
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ user, workspaceUrl, signOutUrl, theme, toggleTheme }) => {
|
||||
return (
|
||||
<header className="flex-shrink-0 relative z-20 w-full p-4 px-6 flex justify-between items-center border-b border-slate-200 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 backdrop-blur-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src="https://www.humanizeiq.ai/home/images/HumanizeIQ_Logo_updated.png"
|
||||
alt="HumanizeIQ Logo"
|
||||
className="h-8 sm:h-10 w-auto"
|
||||
/>
|
||||
<h1 className="text-lg sm:text-xl font-bold text-slate-800 dark:text-slate-200 hidden xs:block">Aura Craft Studio</h1>
|
||||
</div>
|
||||
|
||||
<nav className="flex items-center gap-3 sm:gap-6 text-xs sm:text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{user && (
|
||||
<>
|
||||
<a href={workspaceUrl} className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 hidden md:block">My Workspace</a>
|
||||
<span className="bg-slate-900/5 dark:bg-white/10 backdrop-blur-sm px-3 py-1.5 rounded-md text-slate-900 dark:text-slate-100 max-w-[120px] truncate">
|
||||
{user.name}
|
||||
</span>
|
||||
<a href={signOutUrl} className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200">Sign out</a>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-600 transition-colors"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
59
content.json
Normal file
59
content.json
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
[
|
||||
{
|
||||
"textId": "app_verify_access",
|
||||
"description": "Loading text while verifying authentication",
|
||||
"text": "Verifying access...",
|
||||
"format": "plain",
|
||||
"type": "Body"
|
||||
},
|
||||
{
|
||||
"textId": "app_redirect_login",
|
||||
"description": "Loading text while redirecting to login",
|
||||
"text": "Redirecting to login...",
|
||||
"format": "plain",
|
||||
"type": "Body"
|
||||
},
|
||||
{
|
||||
"textId": "app_unauthorized_title",
|
||||
"description": "Title for unauthorized access screen",
|
||||
"text": "Unauthorized",
|
||||
"format": "plain",
|
||||
"type": "Title"
|
||||
},
|
||||
{
|
||||
"textId": "app_unauthorized_desc",
|
||||
"description": "Description for unauthorized access screen",
|
||||
"text": "You do not have permission to access this application.",
|
||||
"format": "plain",
|
||||
"type": "Body"
|
||||
},
|
||||
{
|
||||
"textId": "header_title",
|
||||
"description": "Main application title in header",
|
||||
"text": "Aura Craft Studio Template",
|
||||
"format": "plain",
|
||||
"type": "Title"
|
||||
},
|
||||
{
|
||||
"textId": "nav_workspace",
|
||||
"description": "Navigation link to workspace",
|
||||
"text": "My Workspace",
|
||||
"format": "plain",
|
||||
"type": "LinkText"
|
||||
},
|
||||
{
|
||||
"textId": "nav_signout",
|
||||
"description": "Navigation link to sign out",
|
||||
"text": "Sign out",
|
||||
"format": "plain",
|
||||
"type": "LinkText"
|
||||
},
|
||||
{
|
||||
"textId": "footer_copyright",
|
||||
"description": "Footer copyright text",
|
||||
"text": "© 2025 HumanizeIQ. All Rights Reserved.",
|
||||
"format": "plain",
|
||||
"type": "Body"
|
||||
}
|
||||
]
|
||||
51
contexts/AuthContext.tsx
Normal file
51
contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { User, Role, Permission } from '../types';
|
||||
import { useAuthSession } from '../hooks/useAuthSession';
|
||||
|
||||
interface AuthContextType {
|
||||
authState: 'checking' | 'authorized' | 'unauthorized';
|
||||
user: User | null;
|
||||
roles: Role[];
|
||||
permissions: Permission[];
|
||||
workspaceUrl: string;
|
||||
signOutUrl: string;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const { authState, user, roles, permissions } = useAuthSession();
|
||||
const [workspaceUrl, setWorkspaceUrl] = useState('/home/workspace');
|
||||
const [signOutUrl, setSignOutUrl] = useState('/home/confirm-signout');
|
||||
|
||||
useEffect(() => {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.startsWith('tools.')) {
|
||||
const newHost = hostname.replace('tools.', 'www.');
|
||||
const protocol = window.location.protocol;
|
||||
setWorkspaceUrl(`${protocol}//${newHost}/home/workspace`);
|
||||
setSignOutUrl(`${protocol}//${newHost}/home/confirm-signout`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
authState,
|
||||
user,
|
||||
roles,
|
||||
permissions,
|
||||
workspaceUrl,
|
||||
signOutUrl
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuthContext = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuthContext must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
42
contexts/ThemeContext.tsx
Normal file
42
contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'light');
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
aura_central:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
command: sh -lc "if [ ! -d node_modules ] || [ -z \"$(ls -A node_modules 2>/dev/null)\" ] || [ ! -d node_modules/@rollup/rollup-linux-arm64-musl ]; then npm ci --include=optional; fi; npm run dev"
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
stdin_open: true
|
||||
tty: true
|
||||
24
docs/blueprint.md
Normal file
24
docs/blueprint.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Aura Central App Blueprint
|
||||
|
||||
## Overview
|
||||
Aura Central is a "Central Nervous System" application designed to be a personal AI assistant and productivity dashboard. It features a modern, high-density interface with real-time updates and multi-modal interaction (Chat, Voice, Meet).
|
||||
|
||||
## Core Features
|
||||
- **Multi-step Authentication**: A secure, invite-only onboarding process with email, phone, and 2FA (TOTP) verification.
|
||||
- **Subscription Management**: A billing flow for managing AI credits and plan levels.
|
||||
- **Intelligent Dashboard**: A centralized hub for tasks, schedule, files, and recent communications.
|
||||
- **Multi-modal Assistant (Ask Aura)**:
|
||||
- **Chat**: Text-based interaction with Gemini AI.
|
||||
- **Voice**: Real-time voice interaction.
|
||||
- **Meet**: Meeting transcription and summarization.
|
||||
- **Contextual View Modes**: Toggle between "Professional" and "Personal" modes to filter relevant tasks and information.
|
||||
|
||||
## Technical Stack
|
||||
- **Frontend**: React with TypeScript.
|
||||
- **Styling**: Tailwind CSS with custom theme variables.
|
||||
- **Animations**: Framer Motion (motion/react).
|
||||
- **Backend/Database**: Firebase (Firestore & Authentication).
|
||||
- **AI Integration**: Google Gemini API via `@google/genai`.
|
||||
|
||||
## Architecture
|
||||
The app follows a Single Page Application (SPA) architecture. The main `App.tsx` handles the core layout and state management, while specialized components handle the authentication and subscription flows. Styling is modularized into several CSS files to separate concerns (theme, layout, components, etc.).
|
||||
32
docs/filespec.md
Normal file
32
docs/filespec.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Aura Central File Specification
|
||||
|
||||
## Root Directory
|
||||
- **.env.example**: Template for environment variables (e.g., Gemini API Key).
|
||||
- **.gitignore**: Specifies files and directories to be ignored by Git.
|
||||
- **firebase-applet-config.json**: Configuration for the Firebase project (API keys, project IDs).
|
||||
- **firebase-blueprint.json**: Intermediate representation of the Firestore data structure.
|
||||
- **firestore.rules**: Security rules for the Firestore database.
|
||||
- **index.html**: The main entry point for the browser.
|
||||
- **metadata.json**: Application metadata (name, description, permissions).
|
||||
- **package.json**: Manages npm dependencies and scripts.
|
||||
- **tsconfig.json**: TypeScript compiler configuration.
|
||||
- **vite.config.ts**: Configuration for the Vite build tool.
|
||||
|
||||
## /src Directory
|
||||
- **App.tsx**: The main application component. It handles the core dashboard layout, state management, and view mode toggling.
|
||||
- **firebase.ts**: Initializes the Firebase SDK and exports the database and authentication instances.
|
||||
- **index.css**: The main entry point for CSS, importing all modular style files.
|
||||
- **main.tsx**: The entry point for the React application, rendering the `App` component.
|
||||
|
||||
## /src/components Directory
|
||||
- **AuthFlow.tsx**: Implements the multi-step onboarding process (Welcome, Invite Code, Email, Phone, 2FA).
|
||||
- **SubscriptionFlow.tsx**: Handles the billing and credit top-up process for Aura Pro subscriptions.
|
||||
|
||||
## /src/styles Directory
|
||||
- **aura.css**: Styles specific to the "Ask Aura" assistant (chat bubbles, suggestion pills, voice/meet circles).
|
||||
- **auth.css**: Styles for the authentication and onboarding flow.
|
||||
- **base.css**: Global base styles, resets, and custom scrollbar styling.
|
||||
- **components.css**: Reusable UI component styles (cards, inputs, buttons, badges) defined as Tailwind utilities.
|
||||
- **layout.css**: Styles for the main dashboard grid, header, and toolbar.
|
||||
- **theme.css**: Defines the color palette, CSS variables, and dark mode configuration.
|
||||
- **utilities.css**: Additional custom utility classes for the application.
|
||||
49
features/workspace/WorkspaceContainer.tsx
Normal file
49
features/workspace/WorkspaceContainer.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { isStudioMode } from '../../services/apiUtils';
|
||||
import MainContainer from '../../containers/MainContainer';
|
||||
import WorkspaceView from './WorkspaceView';
|
||||
|
||||
/**
|
||||
* WorkspaceContainer handles the 'Smart' logic:
|
||||
* - Authentication state monitoring
|
||||
* - Redirections
|
||||
* - Data fetching/preparation for the View
|
||||
*/
|
||||
const WorkspaceContainer: React.FC = () => {
|
||||
const { authState, user, workspaceUrl, signOutUrl } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (authState === 'unauthorized' && !isStudioMode()) {
|
||||
window.location.href = '/home/login';
|
||||
}
|
||||
}, [authState]);
|
||||
|
||||
if (authState === 'checking') {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 text-slate-700 dark:text-slate-300">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-cyan-500"></div>
|
||||
<p className="mt-4 text-lg font-medium">Verifying access...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (authState === 'unauthorized' && isStudioMode()) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
|
||||
<div className="text-center bg-white dark:bg-slate-800 p-8 rounded-lg shadow-2xl border border-slate-200 dark:border-slate-700">
|
||||
<h1 className="text-4xl font-bold text-red-600 mb-4">Unauthorized</h1>
|
||||
<p className="text-slate-600 dark:text-slate-300 text-lg">You do not have permission to access this application.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MainContainer user={user} workspaceUrl={workspaceUrl} signOutUrl={signOutUrl}>
|
||||
<WorkspaceView user={user} />
|
||||
</MainContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceContainer;
|
||||
36
features/workspace/WorkspaceView.tsx
Normal file
36
features/workspace/WorkspaceView.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { User } from '../../types';
|
||||
import StatusGrid from './components/StatusGrid';
|
||||
|
||||
interface WorkspaceViewProps {
|
||||
user: User | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* WorkspaceView is a 'Dumb' component:
|
||||
* - It only receives data via props.
|
||||
* - It defines the visual layout and style.
|
||||
*/
|
||||
const WorkspaceView: React.FC<WorkspaceViewProps> = ({ user }) => {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<div className="w-20 h-20 bg-blue-100 dark:bg-blue-900/30 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl font-bold text-slate-800 dark:text-white mb-2">
|
||||
Workspace Ready
|
||||
</h2>
|
||||
|
||||
<p className="text-slate-500 dark:text-slate-400 max-w-lg mx-auto mb-8">
|
||||
The HumanizeIQ shared framework is initialized. Your authentication context and AI connection layer are fully wired up.
|
||||
</p>
|
||||
|
||||
<StatusGrid user={user} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceView;
|
||||
22
features/workspace/components/StatusCard.tsx
Normal file
22
features/workspace/components/StatusCard.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
interface StatusCardProps {
|
||||
label: string;
|
||||
value: string;
|
||||
colorClass: string;
|
||||
}
|
||||
|
||||
const StatusCard: React.FC<StatusCardProps> = ({ label, value, colorClass }) => {
|
||||
return (
|
||||
<div className="p-4 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm text-left">
|
||||
<p className={`text-xs font-bold uppercase mb-1 ${colorClass}`}>
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-sm text-slate-700 dark:text-slate-300">
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusCard;
|
||||
31
features/workspace/components/StatusGrid.tsx
Normal file
31
features/workspace/components/StatusGrid.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { User } from '../../../types';
|
||||
import StatusCard from './StatusCard';
|
||||
|
||||
interface StatusGridProps {
|
||||
user: User | null;
|
||||
}
|
||||
|
||||
const StatusGrid: React.FC<StatusGridProps> = ({ user }) => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 w-full max-w-2xl">
|
||||
<StatusCard
|
||||
label="Auth"
|
||||
value={`Verified for ${user?.name || 'Guest'}`}
|
||||
colorClass="text-blue-600"
|
||||
/>
|
||||
<StatusCard
|
||||
label="Theme"
|
||||
value="Dark mode & a11y integrated"
|
||||
colorClass="text-purple-600"
|
||||
/>
|
||||
<StatusCard
|
||||
label="AI"
|
||||
value="Gemini API ready"
|
||||
colorClass="text-green-600"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusGrid;
|
||||
10
firebase-applet-config.json
Normal file
10
firebase-applet-config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"projectId": "gen-lang-client-0408711807",
|
||||
"appId": "1:1099064516953:web:2f3d7fa475bd4194546c48",
|
||||
"apiKey": "AIzaSyB28OqUKvw94yNEbkHhSSPn43qdPce-1t4",
|
||||
"authDomain": "gen-lang-client-0408711807.firebaseapp.com",
|
||||
"firestoreDatabaseId": "ai-studio-9f4101c2-8730-451b-9134-3b46f26726c6",
|
||||
"storageBucket": "gen-lang-client-0408711807.firebasestorage.app",
|
||||
"messagingSenderId": "1099064516953",
|
||||
"measurementId": ""
|
||||
}
|
||||
73
firebase-blueprint.json
Normal file
73
firebase-blueprint.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"entities": {
|
||||
"User": {
|
||||
"title": "User",
|
||||
"description": "A user of the Aura platform.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The user's full name."
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "The user's email address."
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"description": "The user's phone number."
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"description": "The user's unique Firebase Auth ID."
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the user account was created."
|
||||
}
|
||||
},
|
||||
"required": ["name", "email", "phone", "uid", "createdAt"]
|
||||
},
|
||||
"Subscription": {
|
||||
"title": "Subscription",
|
||||
"description": "A user's subscription and credit settings.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plan": {
|
||||
"type": "string",
|
||||
"description": "The selected Aura plan."
|
||||
},
|
||||
"topUpAmount": {
|
||||
"type": "number",
|
||||
"description": "The amount to top up."
|
||||
},
|
||||
"rechargeLevel": {
|
||||
"type": "number",
|
||||
"description": "The balance level at which to auto-recharge."
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"description": "The user's unique Firebase Auth ID."
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the subscription was last updated."
|
||||
}
|
||||
},
|
||||
"required": ["plan", "topUpAmount", "rechargeLevel", "uid", "updatedAt"]
|
||||
}
|
||||
},
|
||||
"firestore": {
|
||||
"/users/{uid}": {
|
||||
"schema": "User",
|
||||
"description": "User profile data."
|
||||
},
|
||||
"/subscriptions/{uid}": {
|
||||
"schema": "Subscription",
|
||||
"description": "User subscription data."
|
||||
}
|
||||
}
|
||||
}
|
||||
89
firestore.rules
Normal file
89
firestore.rules
Normal file
@@ -0,0 +1,89 @@
|
||||
rules_version = '2';
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
// ===============================================================
|
||||
// Assumed Data Model
|
||||
// ===============================================================
|
||||
//
|
||||
// Collection: users
|
||||
// Document ID: {uid} (Firebase Auth UID)
|
||||
// Fields:
|
||||
// - name: string (required, 1-100 chars)
|
||||
// - email: string (required, valid email format)
|
||||
// - phone: string (required, 1-20 chars)
|
||||
// - uid: string (required, matches document ID)
|
||||
// - createdAt: string (required, ISO 8601 format)
|
||||
//
|
||||
// Collection: subscriptions
|
||||
// Document ID: {uid} (Firebase Auth UID)
|
||||
// Fields:
|
||||
// - plan: string (required, enum: ['Aura Pro'])
|
||||
// - topUpAmount: number (required, positive)
|
||||
// - rechargeLevel: number (required, positive)
|
||||
// - uid: string (required, matches document ID)
|
||||
// - updatedAt: string (required, ISO 8601 format)
|
||||
//
|
||||
// ===============================================================
|
||||
|
||||
// ===============================================================
|
||||
// Helper Functions
|
||||
// ===============================================================
|
||||
|
||||
function isAuthenticated() {
|
||||
return request.auth != null;
|
||||
}
|
||||
|
||||
function isOwner(userId) {
|
||||
return isAuthenticated() && request.auth.uid == userId;
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
return email is string && email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
|
||||
}
|
||||
|
||||
function isValidDateString(dateStr) {
|
||||
return dateStr is string && dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$");
|
||||
}
|
||||
|
||||
function hasOnlyAllowedFields(fields) {
|
||||
return request.resource.data.keys().hasOnly(fields);
|
||||
}
|
||||
|
||||
// Domain Validators
|
||||
|
||||
function isValidUser(data) {
|
||||
return hasOnlyAllowedFields(['name', 'email', 'phone', 'uid', 'createdAt']) &&
|
||||
data.name is string && data.name.size() > 0 && data.name.size() <= 100 &&
|
||||
isValidEmail(data.email) &&
|
||||
data.phone is string && data.phone.size() > 0 && data.phone.size() <= 20 &&
|
||||
data.uid == request.auth.uid &&
|
||||
isValidDateString(data.createdAt);
|
||||
}
|
||||
|
||||
function isValidSubscription(data) {
|
||||
return hasOnlyAllowedFields(['plan', 'topUpAmount', 'rechargeLevel', 'uid', 'updatedAt']) &&
|
||||
data.plan in ['Aura Pro'] &&
|
||||
data.topUpAmount is number && data.topUpAmount > 0 &&
|
||||
data.rechargeLevel is number && data.rechargeLevel >= 0 &&
|
||||
data.uid == request.auth.uid &&
|
||||
isValidDateString(data.updatedAt);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// Rules
|
||||
// ===============================================================
|
||||
|
||||
match /users/{uid} {
|
||||
allow read, write: if true;
|
||||
}
|
||||
|
||||
match /subscriptions/{uid} {
|
||||
allow read, write: if true;
|
||||
}
|
||||
|
||||
// Default deny
|
||||
match /{path=**} {
|
||||
allow read, write: if false;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
hooks/useAuth.ts
Normal file
7
hooks/useAuth.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
import { useAuthContext } from '../contexts/AuthContext';
|
||||
|
||||
// Export as useAuth to maintain backward compatibility
|
||||
export const useAuth = () => {
|
||||
return useAuthContext();
|
||||
};
|
||||
62
hooks/useAuthSession.ts
Normal file
62
hooks/useAuthSession.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { User, Role, Permission } from '../types';
|
||||
import * as appBuilderService from '../services/appBuilderService';
|
||||
|
||||
export const useAuthSession = () => {
|
||||
const [authState, setAuthState] = useState<'checking' | 'authorized' | 'unauthorized'>('checking');
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [permissions, setPermissions] = useState<Permission[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const isStudio = window.location.href.includes('.goog');
|
||||
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||
|
||||
// Configure Gemini API Key globally for compatibility
|
||||
(window as any).GEMINI_API_KEY = isStudio
|
||||
? ((window as any).process?.env?.API_KEY || 'NOTFOUND')
|
||||
: 'NOT_SET';
|
||||
|
||||
if (isLocal) {
|
||||
setUser({ name: 'Local Dev User', company_name: 'HumanizeIQ', uid: 'local-dev-uid' });
|
||||
setAuthState('authorized');
|
||||
setRoles(['Admin']);
|
||||
setPermissions(['all:access']);
|
||||
return;
|
||||
}
|
||||
|
||||
const authUrl = isGoogDomain ? 'https://www.playtest.humanizeiq.ai/auth/ai_studio' : '/auth/ai_studio';
|
||||
|
||||
try {
|
||||
const fetchOptions: RequestInit = { credentials: 'include' };
|
||||
|
||||
const response = await fetch(authUrl, fetchOptions);
|
||||
if (response.status === 200) {
|
||||
const result = await response.json();
|
||||
if (result.data?.firstname) {
|
||||
setUser({
|
||||
name: `${result.data.firstname} ${result.data.lastname}`,
|
||||
company_name: result.data.company_name,
|
||||
auth_cookie: result.data.auth_cookie_base64,
|
||||
uid: result.data.uid
|
||||
});
|
||||
setAuthState('authorized');
|
||||
const rbacData = await appBuilderService.getMyRbacDetails().catch(() => ({ roles: [], permissions: [] }));
|
||||
setRoles(rbacData.roles);
|
||||
setPermissions(rbacData.permissions);
|
||||
} else {
|
||||
setAuthState('unauthorized');
|
||||
}
|
||||
} else {
|
||||
setAuthState('unauthorized');
|
||||
}
|
||||
} catch (error) {
|
||||
setAuthState('unauthorized');
|
||||
}
|
||||
};
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
return { authState, user, roles, permissions };
|
||||
};
|
||||
31
hooks/useLocalStorage.ts
Normal file
31
hooks/useLocalStorage.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// FIX: Import React to make the 'React' namespace available for type annotations.
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function useLocalStorage<T,>(key: string, initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] {
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return initialValue;
|
||||
}
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const valueToStore =
|
||||
typeof storedValue === 'function'
|
||||
? storedValue(storedValue)
|
||||
: storedValue;
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [key, storedValue]);
|
||||
|
||||
return [storedValue, setStoredValue];
|
||||
}
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Aura Central</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
22
index.tsx
Normal file
22
index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
43
instructions.md
Normal file
43
instructions.md
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
# 🚀 Getting Started with Aura Craft Studio
|
||||
|
||||
This template is a pre-wired, modular framework designed for AI-accelerated development. Follow these two steps to build your application.
|
||||
|
||||
## 1️⃣ Step 1: Manual Setup
|
||||
Before using AI prompts, manually update `metadata.json` in the root directory. This tells the system who you are and what you are building.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Your App Name",
|
||||
"description": "Clear description of your app's purpose",
|
||||
"organization": "YourOrg",
|
||||
"project": "YourProject",
|
||||
"component": "YourApp"
|
||||
}
|
||||
```
|
||||
*Note: The `organization`, `project`, and `component` fields are critical for the AI to fetch your specific system instructions and prompts from the CMS.*
|
||||
|
||||
## 2️⃣ Step 2: AI-Driven Development
|
||||
From this point forward, use the **Mandatory Prompt Template** below for every change or feature request. This ensures the AI respects the modular architecture and safety rules.
|
||||
|
||||
### 📋 Mandatory Prompt Template
|
||||
Copy and paste this into your prompt when asking for updates:
|
||||
|
||||
> "I want to [describe your request].
|
||||
>
|
||||
> **MANDATORY CONSTRAINTS:**
|
||||
> 1. **STRICT ADHERENCE TO rules.md**: Read and follow all rules in `rules.md` without exception.
|
||||
> 2. **CONTAINER/VIEW PATTERN**: Separate logic into smart `*Container.tsx` files and UI into presentational `*View.tsx` files.
|
||||
> 3. **200-LINE LIMIT**: No single file may exceed 200 lines of code. Decompose into smaller modules or custom hooks if necessary.
|
||||
> 4. **WIRING PROTECTION**: Do not modify core infrastructure files (Auth, API Utils, Gemini Service).
|
||||
> 5. **AESTHETICS**: Use Tailwind CSS with full dark mode support (`dark:` variants) and responsive prefixes.
|
||||
> 6. **MODULARITY**: Group new features under `features/[feature-name]/`."
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Developer Reference (For the AI)
|
||||
- **`useAuth()`**: Access user identity and RBAC permissions.
|
||||
- **`useTheme()`**: Toggle between light and dark modes.
|
||||
- **`generateResponse()`**: Call Gemini via the HumanizeIQ proxy without managing keys.
|
||||
- **`apiService.ts`**: Standardized R2 cloud storage operations (Upload/Download/List).
|
||||
- **`rules.md`**: The source of truth for all architectural constraints.
|
||||
4
local_cookie.json
Normal file
4
local_cookie.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"cookie": "VTJGc2RHVmtYMS9sVTJ5MGhJYmR0TFFOekQzRStwbnhRWmF0cDRDYndPYlZZQ1prcmpOWWtIWlBKNUx2a0lIQjAwWGczNlhFSktLV0lERjl2U0NXZDd5MG1XL0t3bTlmZmZseTN6N29tbTdmVGR2YWlRT2o3b2ZKM0RQZjEyWTVxMjJzNFBJdjlidjBLcGlhaUVWY3IrSFhzZHAwU3A3bGh6Sjlsdnd5VzlCN3BtRnVRWUhYSDUwUmt2NloreVBaQ0pnV2E4YmovT3hjRDdXM2JMNHlkeTd4QWZBbFE4OE9DaWcyREtIUHQ3aEpDeG4reGw0UG5IT3pmOG9ndFJoekViUHN0SGlLWUNhanRXSWxha0UwRVhwNllmSHBDOVlPdWEyWkk0YWJyY084djRjMDFJUWMwZXpkWEtNY05hamhqZjlkSXV5eWFjekRVMFdYYzZQZU53PT0="
|
||||
}
|
||||
|
||||
7
metadata.json
Normal file
7
metadata.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Aura Central",
|
||||
"description": "A comprehensive dashboard for managing tasks, schedules, and AI-driven insights.",
|
||||
"requestFramePermissions": [
|
||||
"microphone"
|
||||
]
|
||||
}
|
||||
37
package.json
Normal file
37
package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "react-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.29.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"firebase": "^12.11.0",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
1
public/version.json
Normal file
1
public/version.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":"1.0.0"}
|
||||
35
rules.md
Normal file
35
rules.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 📜 HumanizeIQ AI Development Rules
|
||||
|
||||
This document outlines the strict boundaries and guidelines for AI-driven modifications to this application. These rules ensure that the **"Wiring"** (Auth, API Proxies, RBAC) and **"Branding"** (Standard Layout) remain functional and consistent across all Aura Craft Studio projects.
|
||||
|
||||
## 🚫 1. Immutable "Wiring" (DO NOT MODIFY)
|
||||
The following files and logic sections are core infrastructure. Modification may break connectivity with the HumanizeIQ backend or Google AI Studio.
|
||||
|
||||
- **`services/apiUtils.ts`**: Logic for switching between Studio and Deployed modes.
|
||||
- **`services/geminiService.ts`**: The proxy-aware AI client initialization.
|
||||
- **`services/apiService.ts`**: R2 storage integration methods.
|
||||
- **`metadata.json`**: The application's DNA.
|
||||
- **`local_cookie.json`**: Essential for authentication within Google AI Studio.
|
||||
|
||||
## 🏗️ 2. Architecture: Container/View & SRP
|
||||
To maintain scalability and the Single Responsibility Principle, follow these patterns:
|
||||
|
||||
- **Logic vs. Display Separation**: Business logic, data fetching, and state orchestration must live in **Containers** (`*Container.tsx`) or **Hooks** (`use*.ts`). UI layout and styling must live in **Views** (`*View.tsx` or `*Layout.tsx`).
|
||||
- **Max File Length**: No code file should exceed **200 lines**. If a file grows beyond this, it MUST be decomposed into smaller, more specific modules, sub-components, or custom hooks.
|
||||
- **Modular Features**: New capabilities should be grouped under `features/[feature-name]/` with their own logic and display files.
|
||||
|
||||
## ✅ 3. Permitted Modification Areas
|
||||
- **`features/`**: Create domain-specific modules.
|
||||
- **`components/`**: Create shared UI atoms or molecules.
|
||||
- **`types.ts`**: Add new interfaces specific to the application's domain.
|
||||
- **`hooks/`**: Add custom React hooks for local state or specific data fetching.
|
||||
|
||||
## 🎨 4. Aesthetic & Theme Standards
|
||||
- **Tailwind Strategy**: Always use Tailwind CSS classes. Avoid inline styles.
|
||||
- **Dark Mode**: Every component **must** include `dark:` variants.
|
||||
- **Responsiveness**: Ensure layouts are mobile-friendly using `sm:`, `md:`, and `lg:` prefixes.
|
||||
|
||||
## 🤖 5. Integration Best Practices
|
||||
- **Data Fetching**: Use `getUrlWithStudioAuth` and `getFetchOptions` from `apiUtils.ts`.
|
||||
- **AI Prompts**: Use `getSystemInstruction(key)` from `geminiService.ts`.
|
||||
- **Error Handling**: Implement loading states and graceful fallbacks.
|
||||
160
services/apiService.ts
Normal file
160
services/apiService.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
|
||||
import { getUrlWithStudioAuth, getFetchOptions } from './apiUtils';
|
||||
|
||||
const getApiBaseUrl = (): string => {
|
||||
const isStudioMode = window.location.href.includes('.goog');
|
||||
|
||||
if (isStudioMode) {
|
||||
// In Studio Mode, use the absolute URL for the dev environment.
|
||||
return 'https://www.playtest.humanizeiq.ai/api/r2-explorer';
|
||||
}
|
||||
|
||||
// In deployed environments (dev, prod, local), use a relative URL.
|
||||
return '/api/r2-explorer';
|
||||
};
|
||||
|
||||
export async function uploadFile(file: File, metadata: object): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/upload`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('metadata', JSON.stringify(metadata));
|
||||
|
||||
const options = await getFetchOptions({
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
const error: any = new Error(`File upload failed with status ${response.status}: ${errorText}`);
|
||||
try {
|
||||
error.body = JSON.parse(errorText);
|
||||
} catch (e) {
|
||||
error.body = { error: errorText };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateFile(file: File, fileIdOrPath: string, isPath: boolean = false, metadata?: object): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/update-file`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
// Construct query params carefully to preserve existing auth params
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
const paramName = isPath ? 'path' : 'fileId';
|
||||
const fetchUrl = `${url}${separator}${paramName}=${encodeURIComponent(fileIdOrPath)}`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (metadata) {
|
||||
formData.append('metadata', JSON.stringify(metadata));
|
||||
}
|
||||
|
||||
const options = await getFetchOptions({
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(fetchUrl, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
const error: any = new Error(`File update failed with status ${response.status}: ${errorText}`);
|
||||
try {
|
||||
error.body = JSON.parse(errorText);
|
||||
} catch (e) {
|
||||
error.body = { error: errorText };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error updating file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listFiles(category?: string): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/files`;
|
||||
let url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
if (category) {
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
url = `${url}${separator}category=${encodeURIComponent(category)}`;
|
||||
}
|
||||
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to list files: ${response.status} ${errorText}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function listFilesByMetadata(metadata: any): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/files-by-metadata`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
const options = await getFetchOptions({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ metadata })
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to list files by metadata: ${response.status} ${errorText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error listing files by metadata:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFile(pathOrId: string, isPath: boolean = true): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/download-file`;
|
||||
let url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
const param = isPath ? 'path' : 'fileId';
|
||||
url = `${url}${separator}${param}=${encodeURIComponent(pathOrId)}`;
|
||||
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to download file: ${response.status} ${errorText}`);
|
||||
}
|
||||
// Return JSON directly as we are storing JSON files
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function deleteFile(pathOrId: string, isPath: boolean = true): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/delete-file`;
|
||||
let url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
const param = isPath ? 'path' : 'fileId';
|
||||
url = `${url}${separator}${param}=${encodeURIComponent(pathOrId)}`;
|
||||
|
||||
const options = await getFetchOptions({ method: 'DELETE' });
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to delete file: ${response.status} ${errorText}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
87
services/apiUtils.ts
Normal file
87
services/apiUtils.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
/**
|
||||
* Shared utility functions for making authenticated API calls.
|
||||
* Handles logic for both Studio Mode and Deployed Mode.
|
||||
*/
|
||||
|
||||
export const isStudioMode = (): boolean => {
|
||||
const hostname = window.location.href;
|
||||
return hostname.includes('.goog');
|
||||
};
|
||||
|
||||
// --- Studio Mode Cookie Loading (Async) ---
|
||||
// This promise ensures the cookie is fetched only once per session.
|
||||
let studioCookiePromise: Promise<string | null> | null = null;
|
||||
|
||||
export const fetchStudioCookie = (): Promise<string | null> => {
|
||||
if (!isStudioMode()) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (studioCookiePromise) {
|
||||
return studioCookiePromise;
|
||||
}
|
||||
studioCookiePromise = (async () => {
|
||||
try {
|
||||
// Fetch from the application's relative path
|
||||
const response = await fetch('./local_cookie.json');
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to fetch local_cookie.json: ${response.statusText}`);
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data && typeof data.cookie === 'string') {
|
||||
return data.cookie;
|
||||
}
|
||||
console.error("Invalid format for local_cookie.json. Expected { \"cookie\": \"...\" }");
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching or parsing local_cookie.json:", error);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
return studioCookiePromise;
|
||||
};
|
||||
// --- End Studio Mode Cookie Loading ---
|
||||
|
||||
/**
|
||||
* Appends the Studio Mode authentication cookie as a query parameter to a URL if needed.
|
||||
* @param baseUrl The base URL for the API call.
|
||||
* @returns The URL with the auth parameter if in Studio Mode.
|
||||
*/
|
||||
export const getUrlWithStudioAuth = async (baseUrl: string): Promise<string> => {
|
||||
if (!isStudioMode()) {
|
||||
return baseUrl;
|
||||
}
|
||||
const cookie = await fetchStudioCookie();
|
||||
if (!cookie) {
|
||||
return baseUrl;
|
||||
}
|
||||
const param = `X-Studio-Cookie=${encodeURIComponent(cookie)}`;
|
||||
if (baseUrl.includes('?')) {
|
||||
return `${baseUrl}&${param}`;
|
||||
} else {
|
||||
return `${baseUrl}?${param}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the options object for a fetch call, including credentials and headers.
|
||||
* @param options Initial RequestInit options.
|
||||
* @returns A complete RequestInit object for the fetch call.
|
||||
*/
|
||||
export const getFetchOptions = async (options: RequestInit = {}): Promise<RequestInit> => {
|
||||
const headers = new Headers(options.headers);
|
||||
|
||||
// For FormData, let the browser set the Content-Type with the correct boundary.
|
||||
// For other POST/PUT/PATCH requests, default to application/json if not set.
|
||||
if (!(options.body instanceof FormData) && options.method && ['POST', 'PUT', 'PATCH'].includes(options.method.toUpperCase()) && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
// Studio auth is handled via query parameter, but 'credentials: include' is still needed for deployed mode cookies.
|
||||
return {
|
||||
...options,
|
||||
credentials: 'include',
|
||||
headers: headers,
|
||||
};
|
||||
};
|
||||
122
services/appBuilder/componentService.ts
Normal file
122
services/appBuilder/componentService.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
|
||||
import type { ProjectComponent } from '../../types';
|
||||
import { isStudioMode, getUrlWithStudioAuth, getFetchOptions } from '../apiUtils';
|
||||
import { getAppBuilderApiBaseUrl } from './config';
|
||||
|
||||
// Helper to resolve the App Builder's own component based on metadata.json
|
||||
let selfComponentPromise: Promise<ProjectComponent | null> | null = null;
|
||||
|
||||
// Re-implementing simplified getOrganizations/getProjects/getComponents locally to avoid circular dependencies or importing unused modules
|
||||
// purely for getSelfComponent resolution logic if needed, or we can assume metadata is correct.
|
||||
// However, the original implementation relied on fetching from DB to confirm.
|
||||
// We will keep minimal fetch logic for getSelfComponent resolution.
|
||||
|
||||
const fetchComponentsMinimal = async (projectId: number): Promise<ProjectComponent[]> => {
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/app-builder/components`;
|
||||
const urlWithParams = new URL(baseUrl, window.location.origin);
|
||||
urlWithParams.searchParams.append('projectId', projectId.toString());
|
||||
const finalUrl = baseUrl.startsWith('http') ? urlWithParams.href : `${urlWithParams.pathname}${urlWithParams.search}`;
|
||||
const url = await getUrlWithStudioAuth(finalUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data.map((c: any) => ({...c, projectId: c.projectId || c.project_id})) : [];
|
||||
};
|
||||
|
||||
const fetchOrganizationsMinimal = async (): Promise<any[]> => {
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/app-builder/organizations`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
return response.ok ? await response.json() : [];
|
||||
};
|
||||
|
||||
const fetchProjectsMinimal = async (orgId: number): Promise<any[]> => {
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/app-builder/projects`;
|
||||
const urlWithParams = new URL(baseUrl, window.location.origin);
|
||||
urlWithParams.searchParams.append('organizationId', orgId.toString());
|
||||
const finalUrl = baseUrl.startsWith('http') ? urlWithParams.href : `${urlWithParams.pathname}${urlWithParams.search}`;
|
||||
const url = await getUrlWithStudioAuth(finalUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
return response.ok ? await response.json() : [];
|
||||
};
|
||||
|
||||
export const getSelfComponent = async (): Promise<ProjectComponent | null> => {
|
||||
if (selfComponentPromise) return selfComponentPromise;
|
||||
|
||||
selfComponentPromise = (async () => {
|
||||
try {
|
||||
console.log("getSelfComponent: Starting lookup...");
|
||||
let url: string;
|
||||
// Use document.baseURI to correctly locate metadata.json in both Studio and Deployed modes
|
||||
if (isStudioMode()) {
|
||||
const base = document.baseURI.endsWith('/') ? document.baseURI : `${document.baseURI}/`;
|
||||
url = new URL('metadata.json', base).href;
|
||||
} else {
|
||||
// Calculate base: Host + first path segment (ignoring index.html)
|
||||
const pathParts = window.location.pathname.split('/').filter(p => p && p !== 'index.html');
|
||||
const base = pathParts.length > 0
|
||||
? `${window.location.origin}/${pathParts[0]}/`
|
||||
: `${window.location.origin}/`;
|
||||
url = new URL('metadata.json', base).href;
|
||||
}
|
||||
|
||||
const metaRes = await fetch(url);
|
||||
|
||||
if (!metaRes.ok) {
|
||||
console.warn(`getSelfComponent: Could not fetch metadata.json. Status: ${metaRes.status}`);
|
||||
return null;
|
||||
}
|
||||
const metadata = await metaRes.json();
|
||||
console.log("getSelfComponent: Metadata loaded:", metadata);
|
||||
|
||||
const { organization, project, component } = metadata;
|
||||
|
||||
if (!organization || !project || !component) {
|
||||
console.warn("getSelfComponent: metadata.json missing required fields (organization, project, component)");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. Find Org
|
||||
const orgs = await fetchOrganizationsMinimal();
|
||||
const orgObj = orgs.find(o => o.name === organization);
|
||||
if (!orgObj) {
|
||||
console.warn(`getSelfComponent: Organization '${organization}' not found in DB.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Find Project
|
||||
const projects = await fetchProjectsMinimal(orgObj.id);
|
||||
const projObj = projects.find(p => p.name === project);
|
||||
if (!projObj) {
|
||||
console.warn(`getSelfComponent: Project '${project}' not found in org '${organization}'.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. Find Component
|
||||
const components = await fetchComponentsMinimal(projObj.id);
|
||||
// Match by name or title
|
||||
const compObj = components.find(c => c.name === component || c.title === component);
|
||||
if (!compObj) {
|
||||
console.warn(`getSelfComponent: Component '${component}' not found in project '${project}'.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`getSelfComponent: Resolved Self Component: ${compObj.id} for ${organization}/${project}/${component}`);
|
||||
return compObj;
|
||||
|
||||
} catch (e) {
|
||||
console.error("getSelfComponent: Failed to resolve self component", e);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
return selfComponentPromise;
|
||||
};
|
||||
|
||||
export const getSelfComponentId = async (): Promise<number | null> => {
|
||||
const comp = await getSelfComponent();
|
||||
return comp ? comp.id : null;
|
||||
};
|
||||
21
services/appBuilder/config.ts
Normal file
21
services/appBuilder/config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
export const getAppBuilderApiBaseUrl = (): string => {
|
||||
const isStudioMode = window.location.href.includes('.goog');
|
||||
if (isStudioMode) {
|
||||
// Management APIs (RBAC, Auth) in the HumanizeIQ ecosystem usually sit under this segment
|
||||
return 'https://www.playtest.humanizeiq.ai/api/ai_studio_manager_api';
|
||||
} else {
|
||||
// In deployed mode, use a relative path.
|
||||
return '/api/ai_studio_manager_api';
|
||||
}
|
||||
};
|
||||
export const getUserManagementApiBaseUrl = (): string => {
|
||||
const isStudioMode = window.location.href.includes('.goog');
|
||||
if (isStudioMode) {
|
||||
// Management APIs (RBAC, Auth) in the HumanizeIQ ecosystem usually sit under this segment
|
||||
return 'https://www.playtest.humanizeiq.ai/api/user_management';
|
||||
} else {
|
||||
// In deployed mode, use a relative path.
|
||||
return '/api/user_management';
|
||||
}
|
||||
};
|
||||
1
services/appBuilder/contentService.ts
Normal file
1
services/appBuilder/contentService.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
84
services/appBuilder/promptService.ts
Normal file
84
services/appBuilder/promptService.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
import type { ComponentPrompt } from '../../types';
|
||||
import { getUrlWithStudioAuth, getFetchOptions } from '../apiUtils';
|
||||
import { getAppBuilderApiBaseUrl } from './config';
|
||||
import { getSelfComponentId } from './componentService';
|
||||
|
||||
/**
|
||||
* Retrieves prompts for a specific component.
|
||||
* @param componentId The ID of the component.
|
||||
*/
|
||||
export const getComponentPrompts = async (componentId: number): Promise<ComponentPrompt[]> => {
|
||||
console.log(`API: Fetching prompts for componentId ${componentId}`);
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/app-builder/component-prompts`;
|
||||
|
||||
const urlWithParams = new URL(baseUrl, window.location.origin);
|
||||
urlWithParams.searchParams.append('componentId', componentId.toString());
|
||||
const finalUrl = baseUrl.startsWith('http') ? urlWithParams.href : `${urlWithParams.pathname}${urlWithParams.search}`;
|
||||
|
||||
const url = await getUrlWithStudioAuth(finalUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
const error: any = new Error(`API call failed with status ${response.status}: ${errorText}`);
|
||||
error.status = response.status;
|
||||
try { error.body = JSON.parse(errorText); } catch (e) { error.body = errorText; }
|
||||
throw error;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
console.error(`Error fetching prompts for component ${componentId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to convert prompt array to record map
|
||||
*/
|
||||
const convertPromptsToMap = (prompts: ComponentPrompt[]): Record<string, string> => {
|
||||
const map: Record<string, string> = {};
|
||||
if (Array.isArray(prompts)) {
|
||||
prompts.forEach(p => {
|
||||
if (p && p.title) {
|
||||
map[p.title] = p.content;
|
||||
}
|
||||
});
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves prompts for a specific component as a key-value map.
|
||||
* This is useful for easy lookup by prompt title.
|
||||
*/
|
||||
export const getPrompts = async (componentId: number): Promise<Record<string, string>> => {
|
||||
try {
|
||||
const prompts = await getComponentPrompts(componentId);
|
||||
return convertPromptsToMap(prompts);
|
||||
} catch (e) {
|
||||
console.error(`Failed to get prompts for component ${componentId}`, e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves system prompts for the current application (Self Component).
|
||||
* Returns a map of prompt title to prompt content.
|
||||
*/
|
||||
export const getSystemPrompts = async (): Promise<Record<string, string>> => {
|
||||
try {
|
||||
const selfId = await getSelfComponentId();
|
||||
if (!selfId) {
|
||||
console.warn("getSystemPrompts: No self component ID found.");
|
||||
return {};
|
||||
}
|
||||
const prompts = await getComponentPrompts(selfId);
|
||||
return convertPromptsToMap(prompts);
|
||||
} catch (e) {
|
||||
console.error("Failed to get system prompts", e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
31
services/appBuilder/rbacService.ts
Normal file
31
services/appBuilder/rbacService.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
import type { Role, Permission } from '../../types';
|
||||
import { getUrlWithStudioAuth, getFetchOptions } from '../apiUtils';
|
||||
import { getAppBuilderApiBaseUrl } from './config';
|
||||
|
||||
/**
|
||||
* Retrieves the current user's roles and permissions from the RBAC endpoint.
|
||||
* @returns A promise that resolves with an object containing arrays of roles and permissions.
|
||||
*/
|
||||
export const getMyRbacDetails = async (): Promise<{ roles: Role[], permissions: Permission[] }> => {
|
||||
console.log('API: Fetching RBAC details (roles and permissions)');
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/rbac/me`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to fetch RBAC details: ${response.status} ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return {
|
||||
roles: Array.isArray(data.roles) ? data.roles : [],
|
||||
permissions: Array.isArray(data.permissions) ? data.permissions : []
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching user RBAC details:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
8
services/appBuilderService.ts
Normal file
8
services/appBuilderService.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
// This file re-exports all functionalities from the modular services.
|
||||
// This maintains backward compatibility with existing imports.
|
||||
|
||||
export * from './appBuilder/config';
|
||||
export * from './appBuilder/componentService';
|
||||
export * from './appBuilder/promptService';
|
||||
export * from './appBuilder/rbacService';
|
||||
54
services/geminiService.ts
Normal file
54
services/geminiService.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { getSystemPrompts } from './appBuilderService';
|
||||
import { fetchStudioCookie } from './apiUtils'
|
||||
|
||||
/**
|
||||
* Returns a configured GoogleGenAI instance.
|
||||
* Per @google/genai guidelines, it strictly uses process.env.API_KEY.
|
||||
*/
|
||||
export const getAi = async () => {
|
||||
// The API key must be obtained exclusively from process.env.API_KEY
|
||||
const apiKey = process.env.API_KEY || 'NOT_FOUND';
|
||||
|
||||
const href = window.location.href;
|
||||
const hostname = window.location.hostname;
|
||||
const isStudioMode = href.includes('.goog');
|
||||
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1';
|
||||
|
||||
/**
|
||||
* HumanizeIQ specific: Non-studio modes (deployed apps) route through a proxy
|
||||
* for unified access control.
|
||||
*/
|
||||
let baseUrl='https://www.playtest.humanizeiq.ai/api-proxy'
|
||||
if (!isStudioMode) {
|
||||
baseUrl = `${window.location.origin}/api-proxy`;
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
'User-Agent': 'DraftingStudio'
|
||||
};
|
||||
if (isStudioMode) {
|
||||
const cookie = await fetchStudioCookie();
|
||||
if (cookie) {
|
||||
headers['X-Studio-Cookie'] = cookie;
|
||||
}
|
||||
}
|
||||
|
||||
return new GoogleGenAI({
|
||||
apiKey: apiKey,
|
||||
httpOptions: {
|
||||
baseUrl: baseUrl,
|
||||
headers: headers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Cache for system prompts to avoid repeated fetches
|
||||
let systemPromptsCache: Record<string, string> | null = null;
|
||||
|
||||
export const getSystemInstruction = async (key: string): Promise<string> => {
|
||||
if (!systemPromptsCache) {
|
||||
systemPromptsCache = await getSystemPrompts();
|
||||
}
|
||||
return systemPromptsCache[key] || '';
|
||||
};
|
||||
89
services/llmService.ts
Normal file
89
services/llmService.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
|
||||
|
||||
import { getAi } from './geminiService';
|
||||
import { isStudioMode, fetchStudioCookie } from './apiUtils';
|
||||
import OpenAI from 'openai';
|
||||
import type { ChatMessage, ModelProvider, LLMConfig } from '../types';
|
||||
|
||||
export const MODELS: Record<ModelProvider, string[]> = {
|
||||
openai: ['chatgpt-latest'],
|
||||
// Use the latest recommended model for basic text tasks
|
||||
google: ['gemini-3-flash-preview']
|
||||
};
|
||||
|
||||
export const generateResponse = async (
|
||||
config: LLMConfig,
|
||||
messages: ChatMessage[]
|
||||
): Promise<string> => {
|
||||
// Prepare headers for authentication (needed for proxy in Studio Mode)
|
||||
const headers: Record<string, string> = {};
|
||||
if (isStudioMode()) {
|
||||
const cookie = await fetchStudioCookie();
|
||||
if (cookie) {
|
||||
headers['X-Studio-Cookie'] = cookie;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.provider === 'google') {
|
||||
const ai = await getAi();
|
||||
const history = messages.slice(0, -1).map(m => ({
|
||||
role: m.role,
|
||||
parts: [{ text: m.context || m.content }]
|
||||
}));
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const content = lastMessage.context || lastMessage.content;
|
||||
|
||||
const chatConfig: any = {
|
||||
model: config.model,
|
||||
history: history,
|
||||
};
|
||||
|
||||
if (config.systemInstruction) {
|
||||
chatConfig.config = {
|
||||
systemInstruction: config.systemInstruction
|
||||
};
|
||||
}
|
||||
|
||||
const chat = ai.chats.create(chatConfig);
|
||||
|
||||
const response = await chat.sendMessage({ message: content });
|
||||
// Correct usage of .text property
|
||||
return response.text || "No response text.";
|
||||
|
||||
} else if (config.provider === 'openai') {
|
||||
// API Key handled by proxy
|
||||
const apiKey = config.apiKey || 'managed-by-proxy';
|
||||
|
||||
const baseURL = 'https://www.playtest.humanizeiq.ai/api-proxy/openai/v1';
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: apiKey,
|
||||
baseURL: baseURL,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: headers
|
||||
});
|
||||
|
||||
let openAiMessages = messages.map(m => ({
|
||||
role: m.role === 'model' ? 'assistant' : 'user',
|
||||
content: m.context || m.content
|
||||
})) as any[];
|
||||
|
||||
if (config.systemInstruction) {
|
||||
openAiMessages = [
|
||||
{ role: 'system', content: config.systemInstruction },
|
||||
...openAiMessages
|
||||
];
|
||||
}
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
messages: openAiMessages,
|
||||
model: config.model,
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content || "No response text.";
|
||||
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported provider: ${config.provider}`);
|
||||
};
|
||||
1239
src/App.tsx
Normal file
1239
src/App.tsx
Normal file
@@ -0,0 +1,1239 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Calendar,
|
||||
Bell,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Smartphone,
|
||||
MessageSquare,
|
||||
Mic,
|
||||
Menu,
|
||||
Smile,
|
||||
Send,
|
||||
FileText,
|
||||
Image as ImageIcon,
|
||||
ChevronRight,
|
||||
Layout,
|
||||
LayoutGrid,
|
||||
Mail,
|
||||
Settings,
|
||||
Folder,
|
||||
Briefcase,
|
||||
User,
|
||||
Zap,
|
||||
Timer,
|
||||
Target,
|
||||
Moon,
|
||||
Sun,
|
||||
ShieldCheck,
|
||||
Coins,
|
||||
BarChart3,
|
||||
Lock,
|
||||
Shield,
|
||||
Eye,
|
||||
Fingerprint,
|
||||
File,
|
||||
Brain,
|
||||
LogOut,
|
||||
RefreshCw,
|
||||
ChevronDown,
|
||||
Square,
|
||||
Play,
|
||||
Pause,
|
||||
X,
|
||||
Video,
|
||||
Copy
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { AuthFlow } from './components/AuthFlow';
|
||||
import { SubscriptionFlow } from './components/SubscriptionFlow';
|
||||
import { db, doc, getDoc } from './firebase';
|
||||
|
||||
// --- Types ---
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
category: 'Urgent' | 'Today' | 'Upcoming' | 'Waiting On';
|
||||
completed: boolean;
|
||||
icon?: React.ReactNode;
|
||||
type: 'Personal' | 'Professional';
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'user' | 'model';
|
||||
text: string;
|
||||
}
|
||||
|
||||
// --- Mock Data ---
|
||||
const INITIAL_TASKS: Task[] = [
|
||||
{ id: '1', title: 'Prepare for 2 PM meeting', category: 'Urgent', completed: false, icon: <Clock className="w-4 h-4 text-red-500" />, type: 'Professional' },
|
||||
{ id: '2', title: "Reply to Sarah's email", category: 'Urgent', completed: false, icon: <Bell className="w-4 h-4 text-red-500" />, type: 'Professional' },
|
||||
{ id: '3', title: 'Finish report for project X', category: 'Today', completed: false, icon: <FileText className="w-4 h-4 text-blue-500" />, type: 'Professional' },
|
||||
{ id: '4', title: 'Call mom this evening', category: 'Today', completed: false, icon: <User className="w-4 h-4 text-emerald-500" />, type: 'Personal' },
|
||||
{ id: '5', title: 'Client presentation tomorrow', category: 'Upcoming', completed: false, icon: <Briefcase className="w-4 h-4 text-indigo-500" />, type: 'Professional' },
|
||||
{ id: '6', title: 'Book doctor appointment', category: 'Upcoming', completed: false, icon: <Calendar className="w-4 h-4 text-teal-500" />, type: 'Personal' },
|
||||
{ id: '7', title: 'Approval from John', category: 'Waiting On', completed: false, icon: <CheckCircle2 className="w-4 h-4 text-amber-500" />, type: 'Professional' },
|
||||
{ id: '8', title: 'Reply from Alex', category: 'Waiting On', completed: false, icon: <MessageSquare className="w-4 h-4 text-orange-500" />, type: 'Personal' },
|
||||
];
|
||||
|
||||
const SCHEDULE = [
|
||||
{ time: '2:00 PM', title: 'Project Meeting', icon: <FileText className="w-4 h-4 text-blue-500" />, type: 'Professional' },
|
||||
{ time: '4:30 PM', title: 'Gym Session', icon: <Target className="w-4 h-4 text-emerald-500" />, type: 'Personal' },
|
||||
{ time: '7:00 PM', title: 'Dinner with Kate', icon: <User className="w-4 h-4 text-orange-500" />, type: 'Personal' },
|
||||
];
|
||||
|
||||
const RECENT_FILES = [
|
||||
{ name: 'Project X Report', type: 'doc', icon: <FileText className="w-4 h-4 text-blue-500" />, viewType: 'Professional' },
|
||||
{ name: 'Budget Plan', type: 'sheet', icon: <Layout className="w-4 h-4 text-emerald-500" />, viewType: 'Professional' },
|
||||
{ name: 'Family Photo.jpg', type: 'image', icon: <ImageIcon className="w-4 h-4 text-purple-500" />, viewType: 'Personal' },
|
||||
];
|
||||
|
||||
const RECENT_CHATS = [
|
||||
{ name: 'Sarah', lastMsg: 'Need your feedback', icon: <MessageSquare className="w-4 h-4 text-blue-500" /> },
|
||||
{ name: 'John', lastMsg: 'Sent the document', icon: <MessageSquare className="w-4 h-4 text-blue-500" /> },
|
||||
];
|
||||
|
||||
const NOTIFICATIONS = [
|
||||
{ id: '1', title: 'New message from Sarah', time: '2m ago', icon: <MessageSquare className="w-4 h-4 text-blue-500" /> },
|
||||
{ id: '2', title: 'Meeting starts in 15m', time: '15m ago', icon: <Clock className="w-4 h-4 text-amber-500" /> },
|
||||
{ id: '3', title: 'Project X report updated', time: '1h ago', icon: <FileText className="w-4 h-4 text-emerald-500" /> },
|
||||
];
|
||||
|
||||
// --- Components ---
|
||||
|
||||
interface TaskItemProps {
|
||||
task: Task;
|
||||
onClick?: (task: Task) => void;
|
||||
}
|
||||
|
||||
const TaskItem: React.FC<TaskItemProps> = ({ task, onClick }) => (
|
||||
<div className="item-row group cursor-pointer" onClick={() => onClick?.(task)}>
|
||||
<div className="flex-shrink-0">{task.icon || <CheckCircle2 className="w-4 h-4 aura-text-muted" />}</div>
|
||||
<span className="text-sm aura-text-main flex-grow">{task.title}</span>
|
||||
<MoreHorizontal className="w-4 h-4 aura-text-muted opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
);
|
||||
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
tasks: Task[];
|
||||
colorClass: string;
|
||||
onTaskClick?: (task: Task) => void;
|
||||
}
|
||||
|
||||
const Section: React.FC<SectionProps> = ({ title, tasks, colorClass, onTaskClick }) => (
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-xs font-bold uppercase tracking-wider mb-2 ${colorClass}`}>{title}</h3>
|
||||
<div className="space-y-1">
|
||||
{tasks.map(t => <TaskItem key={t.id} task={t} onClick={onTaskClick} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ToolbarItem = ({ icon, label, active = false, hideLabel = false, onClick }: { icon: React.ReactNode, label: string, active?: boolean, hideLabel?: boolean, onClick?: () => void }) => (
|
||||
<div
|
||||
title={label}
|
||||
onClick={onClick}
|
||||
className={`aura-toolbar-item ${hideLabel ? 'p-2' : 'px-3 py-2'} ${
|
||||
active ? 'aura-toolbar-item-active' : 'aura-toolbar-item-inactive'
|
||||
}`}>
|
||||
{icon}
|
||||
{!hideLabel && <span className="text-xs font-semibold">{label}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function App() {
|
||||
const [user, setUser] = useState<{ name: string; email: string; phone: string; uid: string } | null>(null);
|
||||
const [subscription, setSubscription] = useState<{ plan: string; topUpAmount: number; rechargeLevel: number; uid: string } | null>(null);
|
||||
const [isAuthReady, setIsAuthReady] = useState(false);
|
||||
const [expandedTile, setExpandedTile] = useState<'projects' | 'schedule' | 'files' | 'people' | 'billing' | 'health' | 'notifications' | null>(null);
|
||||
const [isNotificationsExpanded, setIsNotificationsExpanded] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<'Personal' | 'Professional'>('Professional');
|
||||
const [isProfileDropdownOpen, setIsProfileDropdownOpen] = useState(false);
|
||||
const [auraMode, setAuraMode] = useState<'chat' | 'voice' | 'meet'>('chat');
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [recordingTime, setRecordingTime] = useState(0);
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return (localStorage.getItem('aura_theme') as 'light' | 'dark') || 'light';
|
||||
}
|
||||
return 'light';
|
||||
});
|
||||
const [tasks] = useState(INITIAL_TASKS);
|
||||
const [chatInput, setChatInput] = useState('');
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{ role: 'model', text: `Hi ${user?.name.split(' ')[0] || 'Alex'}, Welcome to Aura Central — AI for Understanding, Reasoning, and Action. Here's your update for today:` }
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: any;
|
||||
if (isRecording) {
|
||||
interval = setInterval(() => {
|
||||
setRecordingTime((prev) => prev + 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [isRecording]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
localStorage.setItem('aura_theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadSavedData = async () => {
|
||||
const savedUid = localStorage.getItem('aura_uid');
|
||||
if (savedUid) {
|
||||
try {
|
||||
// Fetch user data
|
||||
const userDoc = await getDoc(doc(db, 'users', savedUid));
|
||||
if (userDoc.exists()) {
|
||||
setUser(userDoc.data() as any);
|
||||
}
|
||||
// Fetch subscription data
|
||||
const subDoc = await getDoc(doc(db, 'subscriptions', savedUid));
|
||||
if (subDoc.exists()) {
|
||||
setSubscription(subDoc.data() as any);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading saved data:', error);
|
||||
}
|
||||
}
|
||||
setIsAuthReady(true);
|
||||
};
|
||||
loadSavedData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setMessages([{ role: 'model', text: `Hi ${user.name.split(' ')[0]}, Here's your update for today:` }]);
|
||||
}
|
||||
}, [user]);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isAppsMenuOpen, setIsAppsMenuOpen] = useState(false);
|
||||
const chatEndRef = useRef<HTMLDivElement>(null);
|
||||
const auraColRef = useRef<HTMLDivElement>(null);
|
||||
const actionColRef = useRef<HTMLDivElement>(null);
|
||||
const notificationsColRef = useRef<HTMLDivElement>(null);
|
||||
const infoColRef = useRef<HTMLDivElement>(null);
|
||||
const usageColRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToCol = (ref: React.RefObject<HTMLDivElement>) => {
|
||||
ref.current?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!chatInput.trim()) return;
|
||||
|
||||
const userMsg = chatInput;
|
||||
setMessages(prev => [...prev, { role: 'user', text: userMsg }]);
|
||||
setChatInput('');
|
||||
setIsTyping(true);
|
||||
|
||||
try {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("GEMINI_API_KEY is not defined");
|
||||
}
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-3-flash-preview",
|
||||
contents: userMsg,
|
||||
config: {
|
||||
systemInstruction: "You are Ask Aura, a helpful personal AI assistant in the Aura Central dashboard. Keep responses concise and professional. You are talking to Alex."
|
||||
}
|
||||
});
|
||||
|
||||
setMessages(prev => [...prev, { role: 'model', text: response.text || "I'm sorry, I couldn't process that." }]);
|
||||
} catch (error) {
|
||||
console.error("Gemini Error:", error);
|
||||
setMessages(prev => [...prev, { role: 'model', text: "Sorry, I'm having trouble connecting right now. Please check your API key." }]);
|
||||
} finally {
|
||||
setIsTyping(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActionClick = async (item: any, type: 'task' | 'event' | 'file' | 'notification' | 'project') => {
|
||||
let userPrompt = "";
|
||||
let contextData = "";
|
||||
|
||||
if (type === 'task') {
|
||||
userPrompt = `I'm looking at the task: "${item.title}". Can you help me with this?`;
|
||||
contextData = `Task Details: Title: ${item.title}, Category: ${item.category}, Status: ${item.completed ? 'Completed' : 'Pending'}, Type: ${item.type}`;
|
||||
} else if (type === 'event') {
|
||||
userPrompt = `I'm looking at my schedule: "${item.title}" at ${item.time}. What should I prepare?`;
|
||||
contextData = `Event Details: Title: ${item.title}, Time: ${item.time}, Type: ${item.type}`;
|
||||
} else if (type === 'file') {
|
||||
userPrompt = `I'm looking at the file: "${item.name}". Can you summarize it or tell me more about it?`;
|
||||
contextData = `File Details: Name: ${item.name}, Type: ${item.type}, View Context: ${item.viewType}`;
|
||||
} else if (type === 'notification') {
|
||||
userPrompt = `I just saw this notification: "${item.title}". What's the context?`;
|
||||
contextData = `Notification Details: Title: ${item.title}, Time: ${item.time}`;
|
||||
} else if (type === 'project') {
|
||||
userPrompt = `I'm looking at the project: "${item.name}". It's at ${item.progress}% progress. What are the next steps?`;
|
||||
contextData = `Project Details: Name: ${item.name}, Progress: ${item.progress}%`;
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, { role: 'user', text: userPrompt }]);
|
||||
scrollToCol(auraColRef);
|
||||
setIsTyping(true);
|
||||
|
||||
try {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) throw new Error("GEMINI_API_KEY is not defined");
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-3-flash-preview",
|
||||
contents: `Context Information: ${contextData}\n\nUser Question: ${userPrompt}`,
|
||||
config: {
|
||||
systemInstruction: `You are Ask Aura, a helpful personal AI assistant. The user just clicked on an item in their dashboard, setting it as the current context. Use the provided Context Information to give a helpful, concise, and professional response. You are talking to Alex.`
|
||||
}
|
||||
});
|
||||
|
||||
setMessages(prev => [...prev, { role: 'model', text: response.text || "I'm sorry, I couldn't process that context." }]);
|
||||
} catch (error) {
|
||||
console.error("Gemini Error:", error);
|
||||
setMessages(prev => [...prev, { role: 'model', text: "I'm having trouble analyzing that context right now." }]);
|
||||
} finally {
|
||||
setIsTyping(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAuthReady) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-aura-bg">
|
||||
<RefreshCw className="w-10 h-10 aura-text-accent-blue animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <AuthFlow onComplete={(data) => setUser(data)} />;
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
return <SubscriptionFlow uid={user.uid} onComplete={(data) => setSubscription(data)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="aura-dashboard flex flex-col h-screen overflow-hidden">
|
||||
{/* Header */}
|
||||
<header className="aura-header flex-shrink-0">
|
||||
{/* Left: Logo & Title */}
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<div className="aura-logo">
|
||||
<Zap className="text-white w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg sm:text-2xl font-bold aura-text-main tracking-tight leading-none">Aura Central</h1>
|
||||
<div className="flex items-center gap-1.5 ml-1">
|
||||
<Shield className="w-3.5 h-3.5 text-blue-500" />
|
||||
<Eye className="w-3.5 h-3.5 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="hidden sm:block text-[10px] aura-text-muted font-bold uppercase tracking-wider mt-1">
|
||||
AI for Understanding, Reasoning, and Action
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Toolbar */}
|
||||
<div className="hidden lg:flex aura-toolbar flex-grow justify-center max-w-3xl mx-auto">
|
||||
<ToolbarItem icon={<MessageSquare className="w-4 h-4" />} label="Chat" active />
|
||||
<ToolbarItem icon={<Layout className="w-4 h-4" />} label="Explorer" />
|
||||
<ToolbarItem icon={<Mail className="w-4 h-4" />} label="Mail" />
|
||||
<ToolbarItem icon={<Calendar className="w-4 h-4" />} label="Calendar" />
|
||||
<ToolbarItem icon={<Settings className="w-4 h-4" />} label="Settings" />
|
||||
<div className="w-[1px] h-4 aura-bg-inverse opacity-10 mx-1"></div>
|
||||
<ToolbarItem icon={<LayoutGrid className="w-4 h-4" />} label="All" />
|
||||
</div>
|
||||
|
||||
{/* Right: Profile Dropdown & Theme Toggle */}
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
{/* Mobile Apps Button */}
|
||||
<div className="lg:hidden relative">
|
||||
<button
|
||||
onClick={() => setIsAppsMenuOpen(!isAppsMenuOpen)}
|
||||
className="p-2 rounded-xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-colors shadow-sm"
|
||||
>
|
||||
<LayoutGrid className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isAppsMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
className="absolute right-0 top-full mt-2 w-48 bg-aura-surface border border-aura-border rounded-2xl shadow-xl z-50 p-2"
|
||||
>
|
||||
<div className="px-3 py-2 text-[10px] font-bold aura-text-muted uppercase tracking-wider">Select App</div>
|
||||
<button onClick={() => setIsAppsMenuOpen(false)} className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium">
|
||||
<MessageSquare className="w-4 h-4 text-blue-500" /> Chat
|
||||
</button>
|
||||
<button onClick={() => setIsAppsMenuOpen(false)} className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium">
|
||||
<Layout className="w-4 h-4 text-purple-500" /> Explorer
|
||||
</button>
|
||||
<button onClick={() => setIsAppsMenuOpen(false)} className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium">
|
||||
<Mail className="w-4 h-4 text-emerald-500" /> Mail
|
||||
</button>
|
||||
<button onClick={() => setIsAppsMenuOpen(false)} className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium">
|
||||
<Calendar className="w-4 h-4 text-amber-500" /> Calendar
|
||||
</button>
|
||||
<button onClick={() => setIsAppsMenuOpen(false)} className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium">
|
||||
<Settings className="w-4 h-4 text-slate-500" /> Settings
|
||||
</button>
|
||||
<div className="h-[1px] aura-bg-inverse opacity-5 my-1"></div>
|
||||
<button onClick={() => setIsAppsMenuOpen(false)} className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium">
|
||||
<LayoutGrid className="w-4 h-4 text-blue-600" /> All Apps
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Profile Dropdown */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsProfileDropdownOpen(!isProfileDropdownOpen)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all border border-aura-border shadow-sm ${
|
||||
viewMode === 'Professional' ? 'aura-text-accent-blue' : 'aura-text-accent-emerald'
|
||||
} bg-aura-surface hover:bg-aura-bg`}
|
||||
>
|
||||
{viewMode === 'Personal' ? <User className="w-3.5 h-3.5" /> : <Briefcase className="w-3.5 h-3.5" />}
|
||||
<span className="hidden sm:inline">{viewMode}</span>
|
||||
<ChevronDown className={`w-3 h-3 transition-transform ${isProfileDropdownOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isProfileDropdownOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
className="absolute right-0 mt-2 w-48 aura-card p-2 shadow-xl z-50 border border-aura-border"
|
||||
>
|
||||
<button
|
||||
onClick={() => { setViewMode('Personal'); setIsProfileDropdownOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-xs font-bold transition-colors ${
|
||||
viewMode === 'Personal' ? 'bg-aura-accent-emerald/10 text-aura-accent-emerald' : 'text-aura-text-muted hover:bg-aura-bg'
|
||||
}`}
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
Personal Mode
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setViewMode('Professional'); setIsProfileDropdownOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-xs font-bold transition-colors ${
|
||||
viewMode === 'Professional' ? 'bg-aura-accent-blue/10 text-aura-accent-blue' : 'text-aura-text-muted hover:bg-aura-bg'
|
||||
}`}
|
||||
>
|
||||
<Briefcase className="w-4 h-4" />
|
||||
Professional Mode
|
||||
</button>
|
||||
<div className="h-[1px] bg-aura-bg my-2"></div>
|
||||
<button
|
||||
onClick={() => { localStorage.removeItem('aura_uid'); setUser(null); setSubscription(null); }}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-xs font-bold aura-text-accent-red hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Log Out
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="aura-btn-secondary p-2.5 rounded-xl flex items-center justify-center"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === 'light' ? <Moon className="w-5 h-5" /> : <Sun className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Grid */}
|
||||
<div className="aura-main-grid flex-grow">
|
||||
|
||||
{/* Middle Column: Ask Aura - First in JSX for mobile swipeability */}
|
||||
<div ref={auraColRef} className="aura-col-aura p-6 xl:order-2">
|
||||
<div className="aura-gradient-top-bar"></div>
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="xl:hidden p-1.5 rounded-lg hover:bg-aura-surface transition-colors aura-text-main"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
className="absolute left-0 top-full mt-2 w-48 bg-aura-surface border border-aura-border rounded-2xl shadow-xl z-50 p-2"
|
||||
>
|
||||
<button
|
||||
onClick={() => scrollToCol(auraColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<Brain className="w-4 h-4 text-purple-500" /> Ask Aura
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(actionColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4 text-blue-500" /> Actionables
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(notificationsColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<Bell className="w-4 h-4 text-indigo-500" /> Notifications
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(infoColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<Zap className="w-4 h-4 text-amber-500" /> Information
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(usageColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<BarChart3 className="w-4 h-4 text-emerald-500" /> Usage Stats
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold aura-text-main">Ask Aura</h2>
|
||||
<div className="flex gap-1 aura-toolbar p-1">
|
||||
<button
|
||||
title="Aura Chat"
|
||||
onClick={() => setAuraMode('chat')}
|
||||
className={`p-1.5 rounded-xl transition-all ${auraMode === 'chat' ? 'bg-aura-surface shadow-sm text-aura-accent-blue' : 'text-aura-text-muted hover:text-aura-text'}`}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
title="Aura Voice"
|
||||
onClick={() => setAuraMode('voice')}
|
||||
className={`p-1.5 rounded-xl transition-all ${auraMode === 'voice' ? 'bg-aura-surface shadow-sm text-aura-accent-amber' : 'text-aura-text-muted hover:text-aura-text'}`}
|
||||
>
|
||||
<Mic className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
title="Aura Meet"
|
||||
onClick={() => setAuraMode('meet')}
|
||||
className={`p-1.5 rounded-xl transition-all ${auraMode === 'meet' ? 'bg-aura-surface shadow-sm text-aura-accent-red' : 'text-aura-text-muted hover:text-aura-text'}`}
|
||||
>
|
||||
<Video className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{auraMode === 'chat' ? (
|
||||
<motion.div
|
||||
key="chat-mode"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
className="flex flex-col h-full"
|
||||
>
|
||||
|
||||
<div className="flex gap-2 mb-6 overflow-x-auto pb-2 no-scrollbar">
|
||||
<button className="aura-suggestion-pill-blue">
|
||||
<Zap className="w-3 h-3" /> Summarize Tasks
|
||||
</button>
|
||||
<button className="aura-suggestion-pill-orange">
|
||||
<Bell className="w-3 h-3" /> Set a Reminder
|
||||
</button>
|
||||
<button className="aura-suggestion-pill-indigo">
|
||||
<Timer className="w-3 h-3" /> Start a Timer
|
||||
</button>
|
||||
<button className="aura-suggestion-pill-red" onClick={() => setAuraMode('meet')}>
|
||||
<Video className="w-3 h-3" /> Aura Meet
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-bold whitespace-nowrap transition-colors bg-aura-surface border-aura-border aura-text-main hover:bg-aura-bg"
|
||||
onClick={() => setMessages(prev => [...prev, { role: 'model', text: "I am Aura Central — AI for Understanding, Reasoning, and Action. I can help you with:\n\n• **Understanding**: Summarizing tasks, analyzing your schedule, and organizing files.\n• **Reasoning**: Prioritizing your day, identifying bottlenecks, and strategic planning.\n• **Action**: Setting reminders, starting timers, and real-time meeting assistance.\n\nHow can I assist you today?" }])}
|
||||
>
|
||||
<Brain className="w-3 h-3 text-purple-500" /> What can I do?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-y-auto mb-4 space-y-4 pr-2">
|
||||
<AnimatePresence initial={false}>
|
||||
{messages.map((msg, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className={`aura-message-container flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
|
||||
>
|
||||
<div className={msg.role === 'user' ? 'aura-chat-bubble-user' : 'aura-chat-bubble-model'}>
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown>{msg.text}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
{msg.role === 'model' && (
|
||||
<div className="aura-message-actions">
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(msg.text)}
|
||||
className="p-1 rounded-md hover:bg-aura-surface text-aura-text-muted transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 rounded-md hover:bg-aura-surface text-aura-text-muted transition-colors"
|
||||
title="Retry"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
{isTyping && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex justify-start"
|
||||
>
|
||||
<div className="aura-chat-bubble-model">
|
||||
<div className="flex gap-1">
|
||||
<div className="w-1.5 h-1.5 aura-text-muted rounded-full animate-bounce"></div>
|
||||
<div className="w-1.5 h-1.5 aura-text-muted rounded-full animate-bounce [animation-delay:0.2s]"></div>
|
||||
<div className="w-1.5 h-1.5 aura-text-muted rounded-full animate-bounce [animation-delay:0.4s]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div ref={chatEndRef} />
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Prompt..."
|
||||
value={chatInput}
|
||||
onChange={(e) => setChatInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
|
||||
className="aura-input pr-24 text-sm"
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
<Smile className="w-5 h-5 aura-text-muted cursor-pointer hover:aura-text-main" />
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
className="w-8 h-8 rounded-lg bg-blue-500 flex items-center justify-center text-white hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : auraMode === 'voice' ? (
|
||||
<motion.div
|
||||
key="voice-mode"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="flex flex-col h-full items-center justify-center text-center p-4"
|
||||
>
|
||||
<div className="mb-8 relative">
|
||||
<div className={`w-40 h-40 rounded-full flex items-center justify-center transition-all duration-500 ${isRecording ? 'bg-amber-500/10' : 'bg-aura-surface border border-aura-border'}`}>
|
||||
<div className={`w-24 h-24 rounded-full flex items-center justify-center shadow-xl transition-all duration-300 ${isRecording ? 'bg-amber-500 scale-110 shadow-amber-500/40' : 'aura-bg-accent-blue shadow-blue-500/40'}`}>
|
||||
<Mic className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
{isRecording && (
|
||||
<div className="absolute -inset-8">
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.8], opacity: [0.3, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5 }}
|
||||
className="w-full h-full rounded-full border-2 border-amber-500/30"
|
||||
/>
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.4], opacity: [0.5, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, delay: 0.5 }}
|
||||
className="w-full h-full rounded-full border-2 border-amber-500/20"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold aura-text-main mb-2">Aura Voice</h3>
|
||||
<p className="aura-text-muted text-sm mb-12 max-w-xs">
|
||||
{isRecording ? 'Aura is listening...' : 'Tap to start a voice conversation with Aura.'}
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => setIsRecording(!isRecording)}
|
||||
className={`px-10 py-4 rounded-2xl font-bold text-white shadow-xl transition-all active:scale-95 ${isRecording ? 'bg-red-500 hover:bg-red-600' : 'aura-bg-accent-blue hover:aura-bg-accent-blue/90'}`}
|
||||
>
|
||||
{isRecording ? 'Stop Listening' : 'Start Talking'}
|
||||
</button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="meet-mode"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
className="flex flex-col h-full items-center justify-center text-center"
|
||||
>
|
||||
<div className="mb-8 relative">
|
||||
<div className={`w-32 h-32 rounded-full flex items-center justify-center transition-all duration-500 ${isRecording ? 'bg-red-500/10 scale-110' : 'bg-aura-surface border border-aura-border'}`}>
|
||||
{isRecording ? (
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5 }}
|
||||
className="w-16 h-16 rounded-full bg-red-500 flex items-center justify-center shadow-lg shadow-red-500/40"
|
||||
>
|
||||
<Square className="w-6 h-6 text-white fill-white" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-full aura-bg-accent-blue flex items-center justify-center shadow-lg shadow-blue-500/40">
|
||||
<Video className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isRecording && (
|
||||
<div className="absolute -inset-4">
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.5], opacity: [0.5, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
className="w-full h-full rounded-full border-2 border-red-500/30"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold aura-text-main mb-2">
|
||||
{isRecording ? 'Recording Meeting...' : 'Aura Meet'}
|
||||
</h3>
|
||||
<p className="aura-text-muted text-sm mb-8 max-w-xs">
|
||||
{isRecording ? 'Aura is listening and transcribing your meeting in real-time.' : 'Start recording to have Aura transcribe, summarize, and extract action items from your meeting.'}
|
||||
</p>
|
||||
|
||||
<div className="text-4xl font-mono font-bold aura-text-main mb-12 tracking-wider">
|
||||
{formatTime(recordingTime)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 items-center">
|
||||
<button
|
||||
onClick={() => { setIsRecording(false); setRecordingTime(0); }}
|
||||
className="p-4 rounded-full bg-aura-surface border border-aura-border text-aura-text-muted hover:text-aura-accent-red transition-colors"
|
||||
title="Cancel"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsRecording(!isRecording)}
|
||||
className={`w-20 h-20 rounded-full flex items-center justify-center shadow-xl transition-all active:scale-95 ${isRecording ? 'bg-red-500 hover:bg-red-600' : 'aura-bg-accent-blue hover:aura-bg-accent-blue/90'}`}
|
||||
>
|
||||
{isRecording ? <Square className="w-8 h-8 text-white fill-white" /> : <Play className="w-8 h-8 text-white fill-white ml-1" />}
|
||||
</button>
|
||||
<button
|
||||
className="p-4 rounded-full bg-aura-surface border border-aura-border text-aura-text-muted hover:text-aura-accent-blue transition-colors"
|
||||
title="Video Mode"
|
||||
>
|
||||
<Video className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isRecording && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-12 w-full p-4 bg-aura-surface border border-aura-border rounded-2xl text-left"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider aura-text-muted">Live Transcript</span>
|
||||
</div>
|
||||
<p className="text-xs aura-text-main italic line-clamp-2">
|
||||
"So the main goal for Q2 is to increase user retention by 15% through the new loyalty program..."
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Left Column Group: Actionables & Projects */}
|
||||
<div className="contents xl:flex xl:flex-col xl:col-span-2 xl:order-1 gap-4 h-full overflow-hidden">
|
||||
{/* Card 1: Actionables */}
|
||||
<div ref={actionColRef} className="aura-col-action p-6 xl:h-full">
|
||||
<div className="flex items-center justify-between mb-6 flex-shrink-0">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="xl:hidden p-1.5 rounded-lg hover:bg-aura-surface transition-colors aura-text-main"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
className="absolute left-0 top-full mt-2 w-48 bg-aura-surface border border-aura-border rounded-2xl shadow-xl z-50 p-2"
|
||||
>
|
||||
<button
|
||||
onClick={() => scrollToCol(auraColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<Brain className="w-4 h-4 text-purple-500" /> Ask Aura
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(actionColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4 text-blue-500" /> Actionables
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(notificationsColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<Bell className="w-4 h-4 text-indigo-500" /> Notifications
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(infoColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<Zap className="w-4 h-4 text-amber-500" /> Information
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(usageColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<BarChart3 className="w-4 h-4 text-emerald-500" /> Usage Stats
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold aura-text-main">
|
||||
{viewMode === 'Professional' ? 'Actionables' : 'Personal Hub'}
|
||||
</h2>
|
||||
<div className={`w-8 h-[2px] rounded-full ${viewMode === 'Professional' ? 'aura-bg-accent-blue' : 'aura-bg-accent-emerald'}`}></div>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-y-auto pr-2 space-y-8 no-scrollbar">
|
||||
<div>
|
||||
<Section
|
||||
title="Urgent"
|
||||
tasks={tasks.filter(t => t.category === 'Urgent' && t.type === viewMode)}
|
||||
colorClass="aura-text-accent-red"
|
||||
onTaskClick={(t) => handleActionClick(t, 'task')}
|
||||
/>
|
||||
<Section
|
||||
title="Today"
|
||||
tasks={tasks.filter(t => t.category === 'Today' && t.type === viewMode)}
|
||||
colorClass="aura-text-accent-blue"
|
||||
onTaskClick={(t) => handleActionClick(t, 'task')}
|
||||
/>
|
||||
<Section
|
||||
title="Upcoming"
|
||||
tasks={tasks.filter(t => t.category === 'Upcoming' && t.type === viewMode)}
|
||||
colorClass="aura-text-accent-emerald"
|
||||
onTaskClick={(t) => handleActionClick(t, 'task')}
|
||||
/>
|
||||
<Section
|
||||
title="Waiting On"
|
||||
tasks={tasks.filter(t => t.category === 'Waiting On' && t.type === viewMode)}
|
||||
colorClass="aura-text-accent-amber"
|
||||
onTaskClick={(t) => handleActionClick(t, 'task')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column Group: Information & Usage Stats */}
|
||||
<div className="contents xl:flex xl:flex-col xl:col-span-3 xl:order-3 gap-4 h-full overflow-hidden">
|
||||
{/* Card 4: Information */}
|
||||
<div ref={infoColRef} className="aura-col-info p-6 xl:h-full">
|
||||
<div className="flex items-center justify-between xl:hidden mb-6 flex-shrink-0">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="xl:hidden p-1.5 rounded-lg hover:bg-aura-surface transition-colors aura-text-main"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
className="absolute left-0 top-full mt-2 w-48 bg-aura-surface border border-aura-border rounded-2xl shadow-xl z-50 p-2"
|
||||
>
|
||||
<button
|
||||
onClick={() => scrollToCol(auraColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<Brain className="w-4 h-4 text-purple-500" /> Ask Aura
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(actionColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4 text-blue-500" /> Actionables
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(notificationsColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<Bell className="w-4 h-4 text-indigo-500" /> Notifications
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToCol(infoColRef)}
|
||||
className="w-full text-left px-4 py-2.5 rounded-xl hover:bg-aura-bg transition-colors flex items-center gap-3 aura-text-main text-sm font-medium"
|
||||
>
|
||||
<Zap className="w-4 h-4 text-amber-500" /> Information
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow flex flex-col">
|
||||
<AnimatePresence mode="wait">
|
||||
{!expandedTile ? (
|
||||
<motion.div
|
||||
key="grid"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="grid grid-cols-2 gap-3 w-full"
|
||||
>
|
||||
{/* Notifications Tile - Spans 2 columns */}
|
||||
<motion.div
|
||||
ref={notificationsColRef}
|
||||
layout
|
||||
className="col-span-2 p-4 rounded-3xl bg-aura-surface border border-aura-border flex flex-col transition-all overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="w-4 h-4 text-indigo-500" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider aura-text-main">Notifications</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setExpandedTile('notifications')}
|
||||
className="text-[10px] font-bold aura-text-muted hover:aura-text-main transition-colors"
|
||||
>
|
||||
View All
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{NOTIFICATIONS.slice(0, 2).map((n) => (
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
key={n.id}
|
||||
onClick={() => handleActionClick(n, 'notification')}
|
||||
className="flex items-center gap-3 p-2 rounded-xl bg-aura-bg/50 border border-aura-border/50 cursor-pointer hover:bg-aura-bg transition-colors"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-lg bg-aura-surface flex items-center justify-center shadow-sm flex-shrink-0">
|
||||
{n.icon}
|
||||
</div>
|
||||
<div className="flex-grow min-w-0">
|
||||
<p className="text-[10px] font-bold aura-text-main truncate">{n.title}</p>
|
||||
<p className="text-[8px] aura-text-muted">{n.time}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
{/* Projects Cube */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
onClick={() => setExpandedTile('projects')}
|
||||
className="aspect-square p-4 rounded-3xl bg-aura-surface border border-aura-border flex flex-col items-center justify-center text-center group transition-all cursor-pointer"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-purple-500/10 flex items-center justify-center mb-2 group-hover:bg-purple-500/20 transition-colors">
|
||||
<Briefcase className="w-6 h-6 text-purple-500" />
|
||||
</div>
|
||||
<span className="text-xl font-black aura-text-main leading-none">03</span>
|
||||
<span className="text-[10px] aura-text-muted mt-1 font-bold uppercase tracking-wider">Projects</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Schedule Cube */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
onClick={() => setExpandedTile('schedule')}
|
||||
className="aspect-square p-4 rounded-3xl bg-aura-surface border border-aura-border flex flex-col items-center justify-center text-center group transition-all cursor-pointer"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-blue-500/10 flex items-center justify-center mb-2 group-hover:bg-blue-500/20 transition-colors">
|
||||
<Calendar className="w-6 h-6 text-blue-500" />
|
||||
</div>
|
||||
<span className="text-xl font-black aura-text-main leading-none">2 PM</span>
|
||||
<span className="text-[10px] aura-text-muted mt-1 font-bold uppercase tracking-wider">Schedule</span>
|
||||
</motion.div>
|
||||
|
||||
{/* File Cube */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
onClick={() => setExpandedTile('files')}
|
||||
className="aspect-square p-4 rounded-3xl bg-aura-surface border border-aura-border flex flex-col items-center justify-center text-center group transition-all cursor-pointer"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-emerald-500/10 flex items-center justify-center mb-2 group-hover:bg-emerald-500/20 transition-colors">
|
||||
<Folder className="w-6 h-6 text-emerald-500" />
|
||||
</div>
|
||||
<span className="text-xl font-black aura-text-main leading-none">12</span>
|
||||
<span className="text-[10px] aura-text-muted mt-1 font-bold uppercase tracking-wider">Files</span>
|
||||
</motion.div>
|
||||
|
||||
{/* People Cube */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
onClick={() => setExpandedTile('people')}
|
||||
className="aspect-square p-4 rounded-3xl bg-aura-surface border border-aura-border flex flex-col items-center justify-center text-center group transition-all cursor-pointer"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-amber-500/10 flex items-center justify-center mb-2 group-hover:bg-amber-500/20 transition-colors">
|
||||
<User className="w-6 h-6 text-amber-500" />
|
||||
</div>
|
||||
<span className="text-xl font-black aura-text-main leading-none">05</span>
|
||||
<span className="text-[10px] aura-text-muted mt-1 font-bold uppercase tracking-wider">People</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Billing Cube */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
onClick={() => setExpandedTile('billing')}
|
||||
className="aspect-square p-4 rounded-3xl bg-aura-surface border border-aura-border flex flex-col items-center justify-center text-center group transition-all cursor-pointer"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-indigo-500/10 flex items-center justify-center mb-2 group-hover:bg-indigo-500/20 transition-colors">
|
||||
<Coins className="w-6 h-6 text-indigo-500" />
|
||||
</div>
|
||||
<span className="text-xl font-black aura-text-main leading-none">$42</span>
|
||||
<span className="text-[10px] aura-text-muted mt-1 font-bold uppercase tracking-wider">Billing</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Health Cube */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
onClick={() => setExpandedTile('health')}
|
||||
className="aspect-square p-4 rounded-3xl bg-aura-surface border border-aura-border flex flex-col items-center justify-center text-center group transition-all cursor-pointer"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-rose-500/10 flex items-center justify-center mb-2 group-hover:bg-rose-500/20 transition-colors">
|
||||
<Target className="w-6 h-6 text-rose-500" />
|
||||
</div>
|
||||
<span className="text-xl font-black aura-text-main leading-none">98%</span>
|
||||
<span className="text-[10px] aura-text-muted mt-1 font-bold uppercase tracking-wider">Health</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="expanded"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
className="flex flex-col h-full"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => setExpandedTile(null)}
|
||||
className="p-1.5 rounded-lg hover:bg-aura-surface aura-text-muted hover:aura-text-main transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="text-sm font-bold aura-text-main capitalize">{expandedTile}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-y-auto no-scrollbar space-y-4">
|
||||
{expandedTile === 'notifications' && (
|
||||
<div className="space-y-3">
|
||||
{NOTIFICATIONS.map((n) => (
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
key={n.id}
|
||||
onClick={() => handleActionClick(n, 'notification')}
|
||||
className="aura-row-card p-3 group hover:bg-aura-bg transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-aura-surface flex items-center justify-center shadow-sm">
|
||||
{n.icon}
|
||||
</div>
|
||||
<div className="flex-grow min-w-0">
|
||||
<p className="text-xs font-bold aura-text-main truncate">{n.title}</p>
|
||||
<p className="text-[10px] aura-text-muted">{n.time}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expandedTile === 'projects' && (
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ name: 'Project X', progress: 65, color: 'aura-bg-accent-blue' },
|
||||
{ name: 'Client Campaign', progress: 40, color: 'aura-bg-accent-indigo' },
|
||||
{ name: 'Budget Revision', progress: 85, color: 'aura-bg-accent-emerald' }
|
||||
].map((p, i) => (
|
||||
<div key={i} className="cursor-pointer group" onClick={() => handleActionClick(p, 'project')}>
|
||||
<div className="flex justify-between text-[10px] mb-1">
|
||||
<span className="font-medium aura-text-muted group-hover:aura-text-main transition-colors">{p.name}</span>
|
||||
<span className="aura-text-muted">{p.progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-1 aura-progress-track">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${p.progress}%` }}
|
||||
className={`h-full ${p.color}`}
|
||||
></motion.div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expandedTile === 'schedule' && (
|
||||
<div className="space-y-3">
|
||||
{SCHEDULE.filter(item => item.type === viewMode).map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-4 group cursor-pointer" onClick={() => handleActionClick(item, 'event')}>
|
||||
<div className="text-xs font-bold aura-text-muted w-16">{item.time}</div>
|
||||
<div className="flex-grow aura-row-card group-hover:bg-slate-100 dark:group-hover:bg-slate-800 transition-colors">
|
||||
{item.icon}
|
||||
<span className="text-sm font-medium aura-text-main">{item.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expandedTile === 'files' && (
|
||||
<div className="space-y-1">
|
||||
{RECENT_FILES.filter(file => file.viewType === viewMode).map((file, i) => (
|
||||
<div key={i} className="item-row group py-1.5 cursor-pointer" onClick={() => handleActionClick(file, 'file')}>
|
||||
<div className="w-6 h-6 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center">
|
||||
{file.icon}
|
||||
</div>
|
||||
<span className="text-xs aura-text-main flex-grow">{file.name}</span>
|
||||
<ChevronRight className="w-3 h-3 aura-text-muted group-hover:aura-text-main" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expandedTile === 'people' && (
|
||||
<div className="space-y-2">
|
||||
{RECENT_CHATS.map((chat, i) => (
|
||||
<div key={i} className="aura-row-card py-2">
|
||||
<div className="w-8 h-8 rounded-full bg-aura-surface flex items-center justify-center border border-aura-border">
|
||||
{chat.icon}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold aura-text-main">{chat.name}</span>
|
||||
<span className="text-[10px] aura-text-muted">{chat.lastMsg}</span>
|
||||
</div>
|
||||
<div className="ml-auto w-2 h-2 rounded-full bg-emerald-500"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expandedTile === 'billing' && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 rounded-2xl bg-aura-bg border border-aura-border">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] aura-text-muted font-bold uppercase">Plan</span>
|
||||
<span className="text-xs font-bold aura-text-main">{subscription?.plan}</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 bg-aura-surface rounded-full overflow-hidden">
|
||||
<div className="h-full bg-indigo-500" style={{ width: '100%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="p-2 rounded-xl bg-aura-surface border border-aura-border">
|
||||
<span className="text-[8px] aura-text-muted uppercase font-bold block">Credits</span>
|
||||
<span className="text-xs font-bold aura-text-main">100%</span>
|
||||
</div>
|
||||
<div className="p-2 rounded-xl bg-aura-surface border border-aura-border">
|
||||
<span className="text-[8px] aura-text-muted uppercase font-bold block">Storage</span>
|
||||
<span className="text-xs font-bold aura-text-main">42%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expandedTile === 'health' && (
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 rounded-2xl bg-aura-bg border border-aura-border flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-rose-500/10 flex items-center justify-center">
|
||||
<Target className="w-4 h-4 text-rose-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold aura-text-main">System Health</p>
|
||||
<p className="text-[10px] aura-text-muted">All systems operational</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-emerald-500">98%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1129
src/components/AuthFlow.tsx
Normal file
1129
src/components/AuthFlow.tsx
Normal file
@@ -0,0 +1,1129 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
User,
|
||||
Mail,
|
||||
Phone,
|
||||
AlertCircle,
|
||||
ShieldCheck,
|
||||
ArrowRight,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CheckCircle2,
|
||||
Smartphone,
|
||||
Lock,
|
||||
Zap,
|
||||
Brain,
|
||||
Target,
|
||||
Calendar,
|
||||
MessageSquare,
|
||||
FileText,
|
||||
Shield,
|
||||
Coins,
|
||||
Mic,
|
||||
Globe,
|
||||
Bell,
|
||||
RefreshCw,
|
||||
KeyRound,
|
||||
LogIn,
|
||||
BookOpen,
|
||||
Heart,
|
||||
GraduationCap,
|
||||
Users,
|
||||
Library,
|
||||
LayoutGrid,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { auth, db, doc, setDoc, signInWithGoogle, OperationType, handleFirestoreError } from '../firebase';
|
||||
import { PrivacyPolicy } from './PrivacyPolicy';
|
||||
|
||||
const BENEFITS = [
|
||||
{
|
||||
title: "Privacy & Security",
|
||||
description: "Enterprise-grade encryption and advanced threat detection.",
|
||||
icon: <Shield className="w-4 h-4 text-blue-600 dark:text-blue-400" />,
|
||||
color: "blue"
|
||||
},
|
||||
{
|
||||
title: "Peak Productivity",
|
||||
description: "Streamline your workflow with intelligent automation.",
|
||||
icon: <Zap className="w-4 h-4 text-orange-600 dark:text-orange-400" />,
|
||||
color: "orange"
|
||||
},
|
||||
{
|
||||
title: "Autonomous Assistant",
|
||||
description: "Works across WhatsApp, Web, and Voice, always synchronized.",
|
||||
icon: <Brain className="w-4 h-4 text-blue-500" />,
|
||||
color: "blue"
|
||||
},
|
||||
{
|
||||
title: "Agent Library",
|
||||
description: "Growing collection of specialized agents for any task.",
|
||||
icon: <Library className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />,
|
||||
color: "indigo"
|
||||
},
|
||||
{
|
||||
title: "Social Impact",
|
||||
description: "Your usage funds scholarships for students in need.",
|
||||
icon: <Heart className="w-4 h-4 text-rose-600 dark:text-rose-400" />,
|
||||
color: "rose"
|
||||
}
|
||||
];
|
||||
|
||||
type AuthStep =
|
||||
| 'WELCOME'
|
||||
| 'PRIVACY_POLICY'
|
||||
| 'COLLECT_INVITE_CODE'
|
||||
| 'COLLECT_EMAIL'
|
||||
| 'COLLECT_NICKNAME'
|
||||
| 'COLLECT_FULL_NAME'
|
||||
| 'COLLECT_PHONE'
|
||||
| 'VERIFY_EMAIL'
|
||||
| 'VERIFY_PHONE'
|
||||
| 'SETUP_TOTP'
|
||||
| 'VERIFY_TOTP'
|
||||
| 'SUCCESS';
|
||||
|
||||
interface AuthFlowProps {
|
||||
onComplete: (userData: { name: string; email: string; phone: string; uid: string }) => void;
|
||||
}
|
||||
|
||||
export const AuthFlow: React.FC<AuthFlowProps> = ({ onComplete }) => {
|
||||
const [step, setStep] = useState<AuthStep>('WELCOME');
|
||||
const [showMobileBenefits, setShowMobileBenefits] = useState(false);
|
||||
const [showPrivacyPolicy, setShowPrivacyPolicy] = useState(false);
|
||||
const [mobileCarouselIndex, setMobileCarouselIndex] = useState(0);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const isAutoScrolling = useRef(false);
|
||||
const [currentBenefitIndex, setCurrentBenefitIndex] = useState(0);
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [nickname, setNickname] = useState('');
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [uid, setUid] = useState(() => {
|
||||
const saved = localStorage.getItem('aura_uid');
|
||||
if (saved) return saved;
|
||||
const newId = 'user_' + Math.random().toString(36).substr(2, 9);
|
||||
localStorage.setItem('aura_uid', newId);
|
||||
return newId;
|
||||
});
|
||||
const [emailOtp, setEmailOtp] = useState('');
|
||||
const [phoneOtp, setPhoneOtp] = useState('');
|
||||
const [totpCode, setTotpCode] = useState('');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [step]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step === 'WELCOME') {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentBenefitIndex((prev) => (prev + 1) % BENEFITS.length);
|
||||
}, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [step]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showMobileBenefits) {
|
||||
const totalItems = BENEFITS.length + 1; // +1 for Enterprise card
|
||||
const interval = setInterval(() => {
|
||||
setMobileCarouselIndex((prev) => (prev + 1) % totalItems);
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [showMobileBenefits]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
const container = scrollContainerRef.current;
|
||||
const targetScroll = mobileCarouselIndex * container.clientWidth;
|
||||
|
||||
if (Math.abs(container.scrollLeft - targetScroll) > 5) {
|
||||
isAutoScrolling.current = true;
|
||||
container.scrollTo({
|
||||
left: targetScroll,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
isAutoScrolling.current = false;
|
||||
}, 600);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
}, [mobileCarouselIndex]);
|
||||
|
||||
const saveUserToFirestore = async () => {
|
||||
const userData = {
|
||||
name: fullName || nickname,
|
||||
nickname,
|
||||
fullName,
|
||||
email,
|
||||
phone,
|
||||
uid,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
try {
|
||||
await setDoc(doc(db, 'users', uid), userData);
|
||||
return userData;
|
||||
} catch (error) {
|
||||
console.error('Firestore Error (Simulated Auth):', error);
|
||||
return userData; // Fallback for demo if rules block
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = async (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
setIsProcessing(true);
|
||||
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
setIsProcessing(false);
|
||||
|
||||
switch (step) {
|
||||
case 'WELCOME':
|
||||
setError(null);
|
||||
setStep('COLLECT_INVITE_CODE');
|
||||
break;
|
||||
case 'COLLECT_INVITE_CODE':
|
||||
if (inviteCode) {
|
||||
if (inviteCode.toUpperCase() === 'AURA-2026') {
|
||||
setStep('COLLECT_EMAIL');
|
||||
setError(null);
|
||||
} else {
|
||||
setError('Invalid invite code. Access denied.');
|
||||
setStep('WELCOME');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'COLLECT_EMAIL':
|
||||
if (email) setStep('COLLECT_NICKNAME');
|
||||
break;
|
||||
case 'COLLECT_NICKNAME': if (nickname) setStep('COLLECT_FULL_NAME'); break;
|
||||
case 'COLLECT_FULL_NAME': if (fullName) setStep('COLLECT_PHONE'); break;
|
||||
case 'COLLECT_PHONE': if (phone) setStep('VERIFY_EMAIL'); break;
|
||||
case 'VERIFY_EMAIL': if (emailOtp === '123456') setStep('VERIFY_PHONE'); else alert('Use 123456 for demo'); break;
|
||||
case 'VERIFY_PHONE': if (phoneOtp === '123456') setStep('SETUP_TOTP'); else alert('Use 123456 for demo'); break;
|
||||
case 'SETUP_TOTP': setStep('VERIFY_TOTP'); break;
|
||||
case 'VERIFY_TOTP':
|
||||
if (totpCode === '123456') {
|
||||
setIsProcessing(true);
|
||||
const userData = await saveUserToFirestore();
|
||||
setIsProcessing(false);
|
||||
setStep('SUCCESS');
|
||||
} else {
|
||||
alert('Use 123456 for demo');
|
||||
}
|
||||
break;
|
||||
case 'SUCCESS': onComplete({ name: fullName || nickname, email, phone, uid }); break;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStep = () => {
|
||||
switch (step) {
|
||||
case 'WELCOME':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="w-full min-h-screen flex justify-center bg-aura-bg relative z-10 overflow-x-hidden"
|
||||
>
|
||||
<div className="w-full max-w-[1400px] flex flex-col md:flex-row overflow-hidden h-screen border-x border-aura-border/10 shadow-2xl bg-aura-surface/50">
|
||||
{/* Left Side: Hero */}
|
||||
<div className="w-full md:w-[45%] xl:w-[35%] flex-shrink-0 flex flex-col justify-between p-6 md:p-12 xl:p-16 relative overflow-hidden border-b md:border-b-0 md:border-r border-aura-border/20 h-full">
|
||||
{/* Background Decorative Element */}
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-400/10 blur-[120px] rounded-full"></div>
|
||||
<div className="absolute bottom-[-10%] right-[10%] w-[30%] h-[30%] bg-indigo-400/10 blur-[100px] rounded-full"></div>
|
||||
|
||||
<div className="relative z-10 space-y-6 md:space-y-8">
|
||||
<div className="flex flex-row items-center gap-4 md:flex-col md:items-start md:gap-8">
|
||||
<div className="aura-logo shadow-2xl shadow-blue-200 flex-shrink-0">
|
||||
<Zap className="text-white w-8 h-8 md:w-10 md:h-10" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-4xl md:text-7xl font-bold aura-text-main tracking-tighter leading-[0.9]">
|
||||
Aura <span className="aura-text-accent-blue italic">Central</span>
|
||||
</h1>
|
||||
<span className="md:hidden block text-[10px] aura-text-muted font-medium tracking-tight mt-1">
|
||||
AI for Understanding, Reasoning, and Action.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:block space-y-4 md:space-y-8">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<p className="text-base md:text-xl aura-text-muted font-medium max-w-md leading-relaxed">
|
||||
<span className="hidden md:block mb-1 md:mb-0">AI for Understanding, Reasoning, and Action.</span>
|
||||
<span className="italic">Your Lifestyle Guide in an AI world</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-4 rounded-2xl bg-rose-500/10 border border-rose-500/20 text-rose-600 dark:text-rose-400 text-sm font-bold flex items-center gap-3"
|
||||
>
|
||||
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
||||
{error}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Benefits Preview Card - Mobile Only */}
|
||||
<button
|
||||
onClick={() => setShowMobileBenefits(true)}
|
||||
className="md:hidden w-full h-56 rounded-[2.5rem] bg-aura-surface border border-aura-border shadow-xl p-6 flex flex-col justify-between relative overflow-hidden group active:scale-[0.98] transition-all"
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-4">
|
||||
<LayoutGrid className="w-4 h-4 text-aura-accent-blue opacity-40" />
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentBenefitIndex}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-aura-bg border border-aura-border flex items-center justify-center shadow-sm mb-4">
|
||||
{React.cloneElement(BENEFITS[currentBenefitIndex].icon as React.ReactElement<{ className?: string }>, { className: "w-5 h-5" })}
|
||||
</div>
|
||||
<div className="text-left space-y-1">
|
||||
<p className="text-xl font-bold aura-text-main leading-tight tracking-tight">
|
||||
{BENEFITS[currentBenefitIndex].title}
|
||||
</p>
|
||||
<p className="text-xs aura-text-muted leading-relaxed line-clamp-2">
|
||||
{BENEFITS[currentBenefitIndex].description}
|
||||
</p>
|
||||
<div className="flex gap-1 mt-3">
|
||||
{BENEFITS.map((_, i) => (
|
||||
<div key={i} className={`h-1 rounded-full transition-all duration-500 ${i === currentBenefitIndex ? 'w-6 bg-aura-accent-blue' : 'w-1.5 bg-aura-border'}`}></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Tap to expand hint */}
|
||||
<div className="absolute bottom-3 right-6 flex items-center gap-2 text-[9px] font-bold aura-text-accent-blue uppercase tracking-widest opacity-60">
|
||||
Explore
|
||||
<ArrowRight className="w-2.5 h-2.5" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div className="pt-2 flex flex-col sm:flex-row items-center gap-4">
|
||||
<button
|
||||
onClick={() => handleNext()}
|
||||
className="w-full sm:w-auto group aura-btn-primary px-8 py-4 rounded-2xl text-lg shadow-xl hover:shadow-blue-300 dark:hover:shadow-blue-900/40 flex items-center justify-center gap-4 transition-all hover:scale-[1.02] active:scale-95"
|
||||
>
|
||||
Get Started
|
||||
<ArrowRight className="w-5 h-5 group-hover:translate-x-2 transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* HumanizeIQ Branding */}
|
||||
<div className="pt-2 flex items-center gap-4 opacity-70 hover:opacity-100 transition-opacity">
|
||||
<div className="w-12 h-12 rounded-xl bg-aura-surface border border-aura-border flex items-center justify-center text-[8px] font-black aura-text-muted text-center leading-none p-1 uppercase tracking-tighter shadow-inner">
|
||||
Humanize<br/>IQ<br/>Logo
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-[9px] font-bold aura-text-muted uppercase tracking-[0.2em]">A Product From</p>
|
||||
<p className="text-base font-bold aura-text-main leading-none">HumanizeIQ</p>
|
||||
<p className="text-[10px] aura-text-accent-blue font-bold italic">Profit for Good (PFG) Company</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Stats/Trust */}
|
||||
<div className="relative z-10 mt-auto pt-6 flex items-center justify-between border-t border-aura-border/50">
|
||||
<div>
|
||||
<p className="text-xl font-bold aura-text-main">99.9%</p>
|
||||
<p className="text-[9px] aura-text-muted uppercase tracking-widest font-bold">Uptime</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xl font-bold aura-text-main">256-bit</p>
|
||||
<p className="text-[9px] aura-text-muted uppercase tracking-widest font-bold">Security</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xl font-bold aura-text-main">Global</p>
|
||||
<p className="text-[9px] aura-text-muted uppercase tracking-widest font-bold">Access</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Benefit Card Cubes */}
|
||||
<div className="hidden md:flex w-full md:w-[55%] xl:w-[65%] flex-shrink-0 bg-aura-surface flex-col overflow-y-auto md:overflow-hidden no-scrollbar">
|
||||
<div className="flex-grow p-6 md:p-8 xl:p-10 overflow-y-auto md:overflow-hidden no-scrollbar">
|
||||
<div className="h-full flex flex-col space-y-6">
|
||||
<h3 className="text-xs font-bold aura-text-muted uppercase tracking-[0.2em]">Platform Benefits</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 flex-grow content-start">
|
||||
{/* Combined Benefit: Privacy & Security */}
|
||||
<div className="p-5 rounded-[1.5rem] bg-aura-bg border border-aura-border shadow-sm hover:shadow-md transition-all group flex flex-col justify-center">
|
||||
<div className="flex gap-2 mb-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Shield className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="w-8 h-8 rounded-lg bg-red-100 dark:bg-red-900/30 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Lock className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="text-base font-bold aura-text-main mb-1">Privacy & Security</h4>
|
||||
<p className="aura-text-muted text-[10px] leading-tight mb-2">
|
||||
Enterprise-grade encryption and advanced threat detection. Your data remains yours.
|
||||
</p>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowPrivacyPolicy(true);
|
||||
}}
|
||||
className="text-[9px] font-bold uppercase tracking-widest text-blue-600 dark:text-blue-400 hover:underline text-left"
|
||||
>
|
||||
View Privacy Policy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Benefit Cube 3 */}
|
||||
<div className="p-5 rounded-[1.5rem] bg-aura-bg border border-aura-border shadow-sm hover:shadow-md transition-all group flex flex-col justify-center">
|
||||
<div className="w-8 h-8 rounded-lg bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">
|
||||
<Zap className="w-4 h-4 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<h4 className="text-base font-bold aura-text-main mb-1">Peak Productivity</h4>
|
||||
<p className="aura-text-muted text-[10px] leading-tight">
|
||||
Streamline your workflow with intelligent automation for mundane tasks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Autonomous Assistant Cube */}
|
||||
<div className="p-5 rounded-[1.5rem] bg-aura-bg border border-aura-border shadow-sm flex flex-col justify-center">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500/10 flex items-center justify-center">
|
||||
<Brain className="w-4 h-4 text-blue-500" />
|
||||
</div>
|
||||
<h4 className="text-sm font-bold aura-text-main leading-tight">Autonomous Assistant</h4>
|
||||
</div>
|
||||
<p className="aura-text-muted text-[10px] leading-tight mb-3">
|
||||
Works across WhatsApp, Web, and Voice, always synchronized.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<MessageSquare className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<Globe className="w-3.5 h-3.5 text-blue-500" />
|
||||
<Mic className="w-3.5 h-3.5 text-orange-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Library */}
|
||||
<div className="p-5 rounded-[1.5rem] bg-aura-bg border border-aura-border shadow-sm hover:shadow-md transition-all group flex flex-col justify-center">
|
||||
<div className="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">
|
||||
<Library className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />
|
||||
</div>
|
||||
<h4 className="text-base font-bold aura-text-main mb-1">Agent Library</h4>
|
||||
<p className="aura-text-muted text-[10px] leading-tight">
|
||||
Growing collection of specialized agents: Presentation, Tutor, Research, and more.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Combined Benefit: Social Impact & Community - 2 Slots */}
|
||||
<div className="lg:col-span-2 p-6 rounded-[1.5rem] bg-aura-bg border border-aura-border shadow-sm hover:shadow-md transition-all group flex flex-col md:flex-row items-center gap-6">
|
||||
<div className="flex -space-x-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform relative z-10">
|
||||
<Heart className="w-6 h-6 text-rose-600 dark:text-rose-400" />
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform translate-y-2">
|
||||
<Users className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="inline-block px-2 py-0.5 rounded-full bg-rose-100 dark:bg-rose-900/30 text-[9px] font-bold text-rose-600 dark:text-rose-400 uppercase tracking-widest">The Aura Difference</div>
|
||||
<h4 className="text-xl font-bold aura-text-main">Why Choose Aura?</h4>
|
||||
<p className="aura-text-muted text-[11px] leading-tight">
|
||||
By using Aura, you support students receiving paid training to survive in an AI world.
|
||||
Our profits fund scholarships and stipends for those in need.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enterprise Level Plan - Aligned to Bottom */}
|
||||
<div className="p-8 pt-0">
|
||||
<div className="p-6 rounded-[2rem] bg-gradient-to-br from-blue-600 via-indigo-700 to-purple-800 text-white shadow-2xl flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div className="space-y-3 max-w-md">
|
||||
<div>
|
||||
<div className="inline-block px-2 py-0.5 rounded-full bg-white/20 text-[9px] font-bold uppercase tracking-widest mb-1">Standard for All</div>
|
||||
<h4 className="text-2xl font-bold">Enterprise Level Plan</h4>
|
||||
<p className="text-blue-100 text-[11px]">Premium features unlocked for every consumer.</p>
|
||||
</div>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
|
||||
<li className="flex items-center gap-2 text-[11px]"><CheckCircle2 className="w-4 h-4 text-blue-200" /> Bundled Usage</li>
|
||||
<li className="flex items-center gap-2 text-[11px]"><CheckCircle2 className="w-4 h-4 text-blue-200" /> Pay-as-you-go</li>
|
||||
<li className="flex items-center gap-2 text-[11px]"><CheckCircle2 className="w-4 h-4 text-blue-200" /> Never Expire</li>
|
||||
<li className="flex items-center gap-2 text-[11px]"><CheckCircle2 className="w-4 h-4 text-blue-200" /> Profit-Sharing</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center md:items-end gap-3 bg-white/10 p-6 rounded-2xl border border-white/20 backdrop-blur-sm min-w-[200px]">
|
||||
<div className="text-center md:text-right">
|
||||
<span className="text-4xl font-bold">$7.99</span>
|
||||
<span className="text-base opacity-80">/mo</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-blue-100 text-center md:text-right max-w-[180px] leading-tight">
|
||||
Rewards system based on company profits.
|
||||
</div>
|
||||
<button className="w-full py-3 px-6 rounded-lg bg-white text-blue-700 font-bold text-sm hover:bg-blue-50 transition-all hover:scale-[1.02] active:scale-95 shadow-lg">
|
||||
Get Enterprise Access
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Benefits Overlay */}
|
||||
<AnimatePresence>
|
||||
{showMobileBenefits && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: '100%' }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: '100%' }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
||||
className="fixed inset-0 z-[100] bg-aura-bg xl:hidden flex flex-col"
|
||||
>
|
||||
{/* Content - Full screen horizontal cards */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
onScroll={(e) => {
|
||||
if (isAutoScrolling.current) return;
|
||||
const container = e.currentTarget;
|
||||
if (container.clientWidth === 0) return;
|
||||
const index = Math.round(container.scrollLeft / container.clientWidth);
|
||||
if (index !== mobileCarouselIndex) {
|
||||
setMobileCarouselIndex(index);
|
||||
}
|
||||
}}
|
||||
className="flex-grow overflow-x-auto snap-x snap-mandatory no-scrollbar flex h-full"
|
||||
>
|
||||
{BENEFITS.map((benefit, idx) => (
|
||||
<div key={idx} className="flex-shrink-0 w-full h-full snap-center flex flex-col">
|
||||
<div className="p-8 bg-aura-surface flex flex-col h-full relative overflow-hidden">
|
||||
{/* Close Button inside card */}
|
||||
<div className="absolute top-6 right-6 z-50">
|
||||
<button
|
||||
onClick={() => setShowMobileBenefits(false)}
|
||||
className="p-3 rounded-2xl bg-aura-bg/50 backdrop-blur-xl border border-aura-border aura-text-main active:scale-90 transition-all"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Decorative Background for Card */}
|
||||
<div className="absolute top-0 right-0 w-48 h-48 bg-aura-accent-blue/10 blur-[100px] rounded-full -mr-24 -mt-24"></div>
|
||||
<div className="absolute bottom-0 left-0 w-32 h-32 bg-indigo-500/5 blur-[80px] rounded-full -ml-16 -mb-16"></div>
|
||||
|
||||
<div className="w-24 h-24 rounded-[2rem] bg-aura-bg border border-aura-border flex items-center justify-center mb-10 shadow-inner">
|
||||
{React.cloneElement(benefit.icon as React.ReactElement<{ className?: string }>, { className: "w-12 h-12 text-aura-accent-blue" })}
|
||||
</div>
|
||||
|
||||
<h4 className="text-5xl font-bold aura-text-main mb-8 leading-[0.85] tracking-tighter">{benefit.title}</h4>
|
||||
<p className="aura-text-muted text-xl leading-relaxed mb-12 flex-grow font-medium">
|
||||
{benefit.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-auto space-y-8">
|
||||
{benefit.title === "Privacy & Security" && (
|
||||
<button
|
||||
onClick={() => setShowPrivacyPolicy(true)}
|
||||
className="w-full py-5 rounded-2xl bg-aura-bg border border-aura-border text-sm font-black uppercase tracking-[0.2em] text-blue-600 dark:text-blue-400 shadow-sm active:scale-95 transition-all"
|
||||
>
|
||||
Privacy Protocol
|
||||
</button>
|
||||
)}
|
||||
|
||||
{benefit.title === "Social Impact" && (
|
||||
<div className="p-6 rounded-3xl bg-rose-500/5 border border-rose-500/10 backdrop-blur-sm">
|
||||
<p className="text-xs font-black text-rose-600 dark:text-rose-400 uppercase tracking-[0.2em] mb-2">PFG Initiative</p>
|
||||
<p className="text-sm aura-text-muted leading-tight font-medium">Your usage directly funds scholarships for students in the AI era.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Mobile Enterprise Plan Card */}
|
||||
<div className="flex-shrink-0 w-full h-full snap-center flex flex-col">
|
||||
<div className="p-8 bg-gradient-to-br from-blue-600 via-indigo-700 to-purple-800 text-white flex flex-col h-full relative overflow-hidden">
|
||||
{/* Close Button inside card */}
|
||||
<div className="absolute top-6 right-6 z-50">
|
||||
<button
|
||||
onClick={() => setShowMobileBenefits(false)}
|
||||
className="p-3 rounded-2xl bg-white/10 backdrop-blur-xl border border-white/20 text-white active:scale-90 transition-all"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-10">
|
||||
<div className="inline-block px-3 py-1 rounded-full bg-white/20 text-[10px] font-black uppercase tracking-[0.2em]">Standard for All</div>
|
||||
<h4 className="text-5xl font-bold leading-[0.85] tracking-tighter">Enterprise Access</h4>
|
||||
<p className="text-blue-100 text-lg font-medium">Premium features unlocked for every consumer.</p>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-4 mb-10">
|
||||
<li className="flex items-center gap-4 text-base font-medium"><CheckCircle2 className="w-6 h-6 text-blue-200" /> Bundled Usage</li>
|
||||
<li className="flex items-center gap-4 text-base font-medium"><CheckCircle2 className="w-6 h-6 text-blue-200" /> Pay-as-you-go</li>
|
||||
<li className="flex items-center gap-4 text-base font-medium"><CheckCircle2 className="w-6 h-6 text-blue-200" /> Never Expire</li>
|
||||
<li className="flex items-center gap-4 text-base font-medium"><CheckCircle2 className="w-6 h-6 text-blue-200" /> Profit-Sharing</li>
|
||||
</ul>
|
||||
|
||||
<div className="mt-auto bg-white/10 p-8 rounded-[2.5rem] border border-white/20 backdrop-blur-md flex flex-col items-center gap-6">
|
||||
<div className="text-center">
|
||||
<span className="text-5xl font-bold">$7.99</span>
|
||||
<span className="text-xl opacity-80">/mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Final CTA in Overlay removed to focus on information */}
|
||||
|
||||
{/* Navigation Arrows - Positioned at 2/3 marker */}
|
||||
<div className="absolute top-[66%] left-4 z-50 -translate-y-1/2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const totalItems = BENEFITS.length + 1;
|
||||
setMobileCarouselIndex((prev) => (prev - 1 + totalItems) % totalItems);
|
||||
}}
|
||||
className="p-4 rounded-full bg-aura-surface/60 backdrop-blur-2xl border border-aura-border text-aura-accent-blue shadow-[0_0_30px_rgba(0,0,0,0.2)] active:scale-90 transition-all"
|
||||
>
|
||||
<ChevronLeft className="w-8 h-8" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-[66%] right-4 z-50 -translate-y-1/2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const totalItems = BENEFITS.length + 1;
|
||||
setMobileCarouselIndex((prev) => (prev + 1) % totalItems);
|
||||
}}
|
||||
className="p-4 rounded-full bg-aura-surface/60 backdrop-blur-2xl border border-aura-border text-aura-accent-blue shadow-[0_0_30px_rgba(0,0,0,0.2)] active:scale-90 transition-all"
|
||||
>
|
||||
<ChevronRight className="w-8 h-8" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation Dots at Bottom */}
|
||||
<div className="absolute bottom-10 left-0 right-0 z-50 flex justify-center">
|
||||
<div className="flex gap-2">
|
||||
{Array.from({ length: BENEFITS.length + 1 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-1.5 rounded-full transition-all duration-500 ${i === mobileCarouselIndex ? 'w-8 bg-white' : 'w-2 bg-white/30'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'PRIVACY_POLICY':
|
||||
return null; // Handled as overlay
|
||||
|
||||
case 'COLLECT_INVITE_CODE':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
className="auth-step-container"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setStep('WELCOME')}
|
||||
className="p-3 rounded-2xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold text-[10px] uppercase tracking-widest">Invite Only</span>
|
||||
<h2 className="text-2xl font-bold aura-text-main">Enter your invite code</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="aura-text-muted text-xs font-bold uppercase tracking-wider">AI for Understanding, Reasoning, and Action</p>
|
||||
<p className="aura-text-muted">Aura is currently in private beta. Please enter your access code to begin.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleNext} className="auth-input-container">
|
||||
<KeyRound className="auth-input-icon" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="AURA-XXXX"
|
||||
value={inviteCode}
|
||||
onChange={(e) => setInviteCode(e.target.value)}
|
||||
className="auth-input-field uppercase tracking-widest"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!inviteCode || isProcessing}
|
||||
className="auth-submit-btn"
|
||||
>
|
||||
{isProcessing ? <RefreshCw className="w-5 h-5 animate-spin" /> : <ArrowRight className="w-5 h-5" />}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'COLLECT_EMAIL':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
className="auth-step-container"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setStep('COLLECT_INVITE_CODE')}
|
||||
className="p-3 rounded-2xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold text-[10px] uppercase tracking-widest">Step 01</span>
|
||||
<h2 className="text-2xl font-bold aura-text-main">What's your email?</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="aura-text-muted">We'll validate your invite code with this email address.</p>
|
||||
</div>
|
||||
<form onSubmit={handleNext} className="auth-input-container">
|
||||
<Mail className="auth-input-icon" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="auth-input-field"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!email || isProcessing}
|
||||
className="auth-submit-btn"
|
||||
>
|
||||
{isProcessing ? <RefreshCw className="w-5 h-5 animate-spin" /> : <ArrowRight className="w-5 h-5" />}
|
||||
</button>
|
||||
</form>
|
||||
<p className="text-xs text-slate-400 text-center italic">Demo: Use AURA-2026</p>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'COLLECT_NICKNAME':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
className="auth-step-container"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setStep('COLLECT_EMAIL')}
|
||||
className="p-3 rounded-2xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold text-[10px] uppercase tracking-widest">Step 02</span>
|
||||
<h2 className="text-2xl font-bold aura-text-main">What should we call you?</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="aura-text-muted">This is how Aura will address you.</p>
|
||||
</div>
|
||||
<form onSubmit={handleNext} className="auth-input-container">
|
||||
<User className="auth-input-icon" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Your nickname"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
className="auth-input-field"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!nickname || isProcessing}
|
||||
className="auth-submit-btn"
|
||||
>
|
||||
{isProcessing ? <RefreshCw className="w-5 h-5 animate-spin" /> : <ArrowRight className="w-5 h-5" />}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'COLLECT_FULL_NAME':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
className="auth-step-container"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setStep('COLLECT_NICKNAME')}
|
||||
className="p-3 rounded-2xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold text-[10px] uppercase tracking-widest">Step 03</span>
|
||||
<h2 className="text-2xl font-bold aura-text-main">And your full name?</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="aura-text-muted">For your official profile and documents.</p>
|
||||
</div>
|
||||
<form onSubmit={handleNext} className="auth-input-container">
|
||||
<User className="auth-input-icon" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="First Last"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
className="auth-input-field"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!fullName || isProcessing}
|
||||
className="auth-submit-btn"
|
||||
>
|
||||
{isProcessing ? <RefreshCw className="w-5 h-5 animate-spin" /> : <ArrowRight className="w-5 h-5" />}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'COLLECT_PHONE':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
className="auth-step-container"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setStep('COLLECT_FULL_NAME')}
|
||||
className="p-3 rounded-2xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold text-[10px] uppercase tracking-widest">Step 04</span>
|
||||
<h2 className="text-2xl font-bold aura-text-main">And your phone number?</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="aura-text-muted">We'll use this for secure verification.</p>
|
||||
</div>
|
||||
<form onSubmit={handleNext} className="auth-input-container">
|
||||
<Phone className="auth-input-icon" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="tel"
|
||||
placeholder="+1 (555) 000-0000"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="auth-input-field"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!phone || isProcessing}
|
||||
className="auth-submit-btn"
|
||||
>
|
||||
{isProcessing ? <RefreshCw className="w-5 h-5 animate-spin" /> : <ArrowRight className="w-5 h-5" />}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'VERIFY_EMAIL':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
className="space-y-8 w-full max-w-md"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setStep('COLLECT_PHONE')}
|
||||
className="p-3 rounded-2xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold text-[10px] uppercase tracking-widest">Verification</span>
|
||||
<h2 className="text-2xl font-bold aura-text-main">Check your email</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="aura-text-muted">We sent a 6-digit code to <span className="aura-text-main font-semibold">{email}</span></p>
|
||||
</div>
|
||||
<form onSubmit={handleNext} className="space-y-6">
|
||||
<div className="relative">
|
||||
<ShieldCheck className="absolute left-5 top-1/2 -translate-y-1/2 text-slate-400 w-6 h-6" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
maxLength={6}
|
||||
placeholder="000000"
|
||||
value={emailOtp}
|
||||
onChange={(e) => setEmailOtp(e.target.value)}
|
||||
className="aura-input py-5 pl-14 pr-6 text-3xl tracking-[0.5em] font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 text-center italic">Demo: Use 123456</p>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={emailOtp.length < 6 || isProcessing}
|
||||
className="w-full aura-btn-primary py-5 rounded-2xl text-lg shadow-xl disabled:opacity-50"
|
||||
>
|
||||
{isProcessing ? <RefreshCw className="w-5 h-5 animate-spin mx-auto" /> : 'Verify Email'}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'VERIFY_PHONE':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
className="space-y-8 w-full max-w-md"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setStep('VERIFY_EMAIL')}
|
||||
className="p-3 rounded-2xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold text-[10px] uppercase tracking-widest">Verification</span>
|
||||
<h2 className="text-2xl font-bold aura-text-main">One more code</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="aura-text-muted">We sent a text to <span className="aura-text-main font-semibold">{phone}</span></p>
|
||||
</div>
|
||||
<form onSubmit={handleNext} className="space-y-6">
|
||||
<div className="relative">
|
||||
<Smartphone className="absolute left-5 top-1/2 -translate-y-1/2 text-slate-400 w-6 h-6" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
maxLength={6}
|
||||
placeholder="000000"
|
||||
value={phoneOtp}
|
||||
onChange={(e) => setPhoneOtp(e.target.value)}
|
||||
className="aura-input py-5 pl-14 pr-6 text-3xl tracking-[0.5em] font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 text-center italic">Demo: Use 123456</p>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={phoneOtp.length < 6 || isProcessing}
|
||||
className="w-full aura-btn-primary py-5 rounded-2xl text-lg shadow-xl disabled:opacity-50"
|
||||
>
|
||||
{isProcessing ? <RefreshCw className="w-5 h-5 animate-spin mx-auto" /> : 'Verify Phone'}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'SETUP_TOTP':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 1.05 }}
|
||||
className="space-y-8 w-full max-w-md text-center"
|
||||
>
|
||||
<div className="flex items-center gap-4 text-left">
|
||||
<button
|
||||
onClick={() => setStep('VERIFY_PHONE')}
|
||||
className="p-3 rounded-2xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold text-[10px] uppercase tracking-widest">Security</span>
|
||||
<h2 className="text-2xl font-bold aura-text-main">Setup 2FA</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="aura-text-muted">Scan this QR code with Google Authenticator or Authy.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-aura-surface p-6 rounded-3xl border-2 border-aura-border inline-block shadow-xl">
|
||||
<QRCodeSVG
|
||||
value={`otpauth://totp/Aura:${email}?secret=JBSWY3DPEHPK3PXP&issuer=Aura`}
|
||||
size={200}
|
||||
level="H"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="aura-row-card text-left">
|
||||
<p className="text-xs aura-text-muted uppercase font-bold mb-1">Manual Secret</p>
|
||||
<code className="text-sm font-mono aura-text-main">JBSWY3DPEHPK3PXP</code>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleNext()}
|
||||
className="w-full aura-btn-primary py-5 rounded-2xl text-lg shadow-xl"
|
||||
>
|
||||
I've scanned it
|
||||
</button>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'VERIFY_TOTP':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
className="space-y-8 w-full max-w-md"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setStep('SETUP_TOTP')}
|
||||
className="p-3 rounded-2xl bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold text-[10px] uppercase tracking-widest">Final Step</span>
|
||||
<h2 className="text-2xl font-bold aura-text-main">Verify App Code</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="aura-text-muted">Enter the 6-digit code from your authenticator app.</p>
|
||||
</div>
|
||||
<form onSubmit={handleNext} className="space-y-6">
|
||||
<div className="relative">
|
||||
<KeyRound className="absolute left-5 top-1/2 -translate-y-1/2 text-slate-400 w-6 h-6" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
maxLength={6}
|
||||
placeholder="000000"
|
||||
value={totpCode}
|
||||
onChange={(e) => setTotpCode(e.target.value)}
|
||||
className="aura-input py-5 pl-14 pr-6 text-3xl tracking-[0.5em] font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 text-center italic">Demo: Use 123456</p>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={totpCode.length < 6 || isProcessing}
|
||||
className="w-full aura-btn-primary py-5 rounded-2xl text-lg shadow-xl disabled:opacity-50"
|
||||
>
|
||||
{isProcessing ? <RefreshCw className="w-5 h-5 animate-spin mx-auto" /> : 'Complete Setup'}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
case 'SUCCESS':
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center space-y-8"
|
||||
>
|
||||
<div className="w-24 h-24 bg-emerald-500 rounded-full flex items-center justify-center mx-auto shadow-2xl shadow-emerald-200">
|
||||
<CheckCircle2 className="text-white w-12 h-12" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold aura-text-main tracking-tight">You're all set!</h1>
|
||||
<p className="aura-text-muted text-lg">Welcome to the future of productivity, {nickname || fullName.split(' ')[0]}.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleNext()}
|
||||
className="aura-btn-primary px-12 py-5 rounded-2xl text-xl shadow-2xl"
|
||||
>
|
||||
Enter Aura
|
||||
</button>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={step === 'WELCOME' ? "min-h-screen flex relative overflow-hidden bg-aura-bg" : "auth-container"}>
|
||||
{/* Background Accents */}
|
||||
<div className="fixed top-0 left-0 w-full h-full pointer-events-none overflow-hidden z-0">
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-100 dark:bg-blue-900/20 rounded-full blur-[120px] opacity-50" />
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-emerald-100 dark:bg-emerald-900/20 rounded-full blur-[120px] opacity-50" />
|
||||
</div>
|
||||
|
||||
<div className={step === 'WELCOME' ? "w-full h-screen" : "auth-card"}>
|
||||
<AnimatePresence mode="wait">
|
||||
{renderStep()}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Privacy Policy Overlay */}
|
||||
<AnimatePresence>
|
||||
{showPrivacyPolicy && (
|
||||
<PrivacyPolicy onBack={() => setShowPrivacyPolicy(false)} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Progress Indicator */}
|
||||
{step !== 'WELCOME' && step !== 'SUCCESS' && (
|
||||
<div className="auth-progress-indicator">
|
||||
{['COLLECT_INVITE_CODE', 'COLLECT_EMAIL', 'COLLECT_NICKNAME', 'COLLECT_FULL_NAME', 'COLLECT_PHONE', 'VERIFY_EMAIL', 'VERIFY_PHONE', 'SETUP_TOTP', 'VERIFY_TOTP'].map((s, i) => (
|
||||
<div
|
||||
key={s}
|
||||
className={`auth-progress-dot ${
|
||||
step === s ? 'auth-progress-dot-active' : 'auth-progress-dot-inactive'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
90
src/components/PrivacyPolicy.tsx
Normal file
90
src/components/PrivacyPolicy.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { Shield, X, ArrowLeft } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
interface PrivacyPolicyProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const PrivacyPolicy: React.FC<PrivacyPolicyProps> = ({ onBack }) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: '100%' }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: '100%' }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
||||
className="fixed inset-0 z-[100] bg-aura-bg overflow-y-auto flex flex-col"
|
||||
>
|
||||
{/* Header with Close Button */}
|
||||
<div className="sticky top-0 z-50 bg-aura-bg/80 backdrop-blur-xl border-b border-aura-border px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
<span className="font-bold uppercase tracking-widest text-xs aura-text-main">Privacy Policy</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="p-2 rounded-full bg-aura-surface border border-aura-border aura-text-muted hover:aura-text-main transition-all active:scale-90"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow p-8 md:p-12 max-w-4xl mx-auto w-full">
|
||||
<div className="space-y-12">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-16 h-16 rounded-2xl bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||
<Shield className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold aura-text-main tracking-tight">Privacy Policy</h1>
|
||||
<p className="aura-text-muted font-medium mt-2">Last Updated: March 23, 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-slate dark:prose-invert max-w-none space-y-8">
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-2xl font-bold aura-text-main">1. Introduction</h2>
|
||||
<p className="aura-text-muted leading-relaxed">
|
||||
At Aura Central, we take your privacy seriously. This Privacy Policy explains how we collect, use, and protect your personal information when you use our AI-powered platform.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-2xl font-bold aura-text-main">2. Data Collection</h2>
|
||||
<p className="aura-text-muted leading-relaxed">
|
||||
We collect information you provide directly to us, such as your name, email address, and any data you input into our AI tools. We also collect technical data like your IP address and device information to improve our services.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-2xl font-bold aura-text-main">3. Data Usage</h2>
|
||||
<p className="aura-text-muted leading-relaxed">
|
||||
Your data is used solely to provide and improve the Aura Central experience. We do not sell your personal data to third parties. Your interactions with our AI are used to provide personalized responses and are protected by enterprise-grade encryption.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-2xl font-bold aura-text-main">4. Security</h2>
|
||||
<p className="aura-text-muted leading-relaxed">
|
||||
We implement industry-standard security measures to protect your data, including 256-bit encryption and secure data centers. Access to your data is strictly controlled and monitored.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-2xl font-bold aura-text-main">5. Your Rights</h2>
|
||||
<p className="aura-text-muted leading-relaxed">
|
||||
You have the right to access, correct, or delete your personal information at any time. If you have any questions about your data, please contact our privacy team.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="pt-12 border-t border-aura-border">
|
||||
<p className="text-sm aura-text-muted">
|
||||
By using Aura Central, you agree to the terms of this Privacy Policy. If you do not agree, please do not use the platform.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
202
src/components/SubscriptionFlow.tsx
Normal file
202
src/components/SubscriptionFlow.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
Zap,
|
||||
Check,
|
||||
CreditCard,
|
||||
ArrowRight,
|
||||
ShieldCheck,
|
||||
TrendingUp,
|
||||
RefreshCw,
|
||||
Coins
|
||||
} from 'lucide-react';
|
||||
import { db, doc, setDoc, OperationType, handleFirestoreError } from '../firebase';
|
||||
|
||||
interface SubscriptionData {
|
||||
plan: string;
|
||||
topUpAmount: number;
|
||||
rechargeLevel: number;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
interface SubscriptionFlowProps {
|
||||
uid: string;
|
||||
onComplete: (data: SubscriptionData) => void;
|
||||
}
|
||||
|
||||
export const SubscriptionFlow: React.FC<SubscriptionFlowProps> = ({ uid, onComplete }) => {
|
||||
const [topUpAmount, setTopUpAmount] = useState<number>(50);
|
||||
const [rechargeLevel, setRechargeLevel] = useState<number>(10);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const handleComplete = async () => {
|
||||
setIsProcessing(true);
|
||||
const subData = {
|
||||
plan: 'Aura Central Platform',
|
||||
topUpAmount,
|
||||
rechargeLevel,
|
||||
uid,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
try {
|
||||
await setDoc(doc(db, 'subscriptions', uid), subData);
|
||||
setIsProcessing(false);
|
||||
onComplete(subData);
|
||||
} catch (error) {
|
||||
handleFirestoreError(error, OperationType.CREATE, `subscriptions/${uid}`);
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-aura-bg flex items-center justify-center p-6 overflow-hidden transition-colors duration-300">
|
||||
{/* Background Accents */}
|
||||
<div className="fixed top-0 left-0 w-full h-full pointer-events-none overflow-hidden z-0">
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-100 dark:bg-blue-900/20 rounded-full blur-[120px] opacity-50" />
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-indigo-100 dark:bg-indigo-900/20 rounded-full blur-[120px] opacity-50" />
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="relative z-10 w-full max-w-4xl grid grid-cols-1 lg:grid-cols-2 gap-8"
|
||||
>
|
||||
{/* Left: Plan Details */}
|
||||
<div className="aura-glass-card p-8 space-y-8">
|
||||
<div className="space-y-2">
|
||||
<div className="aura-logo shadow-blue-200 dark:shadow-blue-900/30">
|
||||
<Zap className="text-white w-6 h-6" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold aura-text-main">Choose your Aura</h1>
|
||||
<p className="aura-text-muted">Power your productivity with intelligent credits.</p>
|
||||
</div>
|
||||
|
||||
<div className="aura-gradient-card">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10">
|
||||
<Zap className="w-24 h-24" />
|
||||
</div>
|
||||
<div className="relative z-10 space-y-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">Aura Central Platform</h3>
|
||||
<p className="text-blue-100 text-sm">Monthly Access Fee</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-3xl font-bold">$29</span>
|
||||
<span className="text-blue-200 text-sm">/mo</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
'Unified Communication Hub',
|
||||
'Omnichannel AI Assistant',
|
||||
'WhatsApp, Web, & Voice',
|
||||
'Priority Security & Privacy'
|
||||
].map((feature, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-sm">
|
||||
<Check className="w-4 h-4 text-blue-300" />
|
||||
<span>{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="aura-row-card border aura-border">
|
||||
<ShieldCheck className="text-emerald-500 w-6 h-6" />
|
||||
<div className="text-xs aura-text-muted">
|
||||
<p className="font-bold aura-text-main">Secure Billing</p>
|
||||
<p>Encrypted by industry-standard protocols.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Usage Configuration */}
|
||||
<div className="aura-glass-card p-8 flex flex-col justify-between border-2 border-aura-accent-blue/20">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-amber-100 dark:bg-amber-900/30 rounded-lg flex items-center justify-center">
|
||||
<Coins className="text-amber-600 dark:text-amber-400 w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold aura-text-main">Usage Configuration</h2>
|
||||
</div>
|
||||
<p className="aura-text-muted text-xs leading-relaxed">Set up your pay-as-you-go usage credits. You only pay for what you consume.</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="section-title">Select Amount</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[25, 50, 100].map((amount) => (
|
||||
<button
|
||||
key={amount}
|
||||
onClick={() => setTopUpAmount(amount)}
|
||||
className={`py-3 rounded-xl font-bold transition-all border-2 ${
|
||||
topUpAmount === amount
|
||||
? 'bg-aura-text text-aura-surface border-transparent shadow-lg'
|
||||
: 'bg-aura-surface text-aura-text-muted aura-border hover:border-blue-500/30'
|
||||
}`}
|
||||
>
|
||||
${amount}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg flex items-center justify-center">
|
||||
<TrendingUp className="text-indigo-600 dark:text-indigo-400 w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold aura-text-main">Auto-Recharge</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-end">
|
||||
<label className="section-title">Recharge Level</label>
|
||||
<span className="text-aura-accent-indigo font-bold text-lg">${rechargeLevel}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="50"
|
||||
step="5"
|
||||
value={rechargeLevel}
|
||||
onChange={(e) => setRechargeLevel(parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-slate-200 dark:bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-600"
|
||||
/>
|
||||
<p className="text-xs aura-text-muted italic">
|
||||
We'll automatically top up your account with ${topUpAmount} when your balance falls below ${rechargeLevel}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 space-y-4">
|
||||
<div className="flex justify-between items-center aura-text-main">
|
||||
<span className="font-medium">Total Due Today</span>
|
||||
<span className="text-2xl font-bold">${29 + topUpAmount}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleComplete}
|
||||
disabled={isProcessing}
|
||||
className="w-full aura-btn-primary py-5 rounded-2xl text-lg shadow-xl disabled:opacity-50"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<RefreshCw className="w-5 h-5 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Confirm Subscription
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
88
src/firebase.ts
Normal file
88
src/firebase.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getAuth, GoogleAuthProvider, signInWithPopup, onAuthStateChanged, User as FirebaseUser } from 'firebase/auth';
|
||||
import { getFirestore, doc, setDoc, getDoc, onSnapshot, getDocFromServer } from 'firebase/firestore';
|
||||
import firebaseConfig from '../firebase-applet-config.json';
|
||||
|
||||
// Initialize Firebase SDK
|
||||
const app = initializeApp(firebaseConfig);
|
||||
export const db = getFirestore(app, firebaseConfig.firestoreDatabaseId);
|
||||
export const auth = getAuth(app);
|
||||
export const googleProvider = new GoogleAuthProvider();
|
||||
|
||||
// Auth helpers
|
||||
export const signInWithGoogle = async () => {
|
||||
try {
|
||||
const result = await signInWithPopup(auth, googleProvider);
|
||||
return result.user;
|
||||
} catch (error) {
|
||||
console.error('Error signing in with Google:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Firestore Error Handling
|
||||
export enum OperationType {
|
||||
CREATE = 'create',
|
||||
UPDATE = 'update',
|
||||
DELETE = 'delete',
|
||||
LIST = 'list',
|
||||
GET = 'get',
|
||||
WRITE = 'write',
|
||||
}
|
||||
|
||||
export interface FirestoreErrorInfo {
|
||||
error: string;
|
||||
operationType: OperationType;
|
||||
path: string | null;
|
||||
authInfo: {
|
||||
userId: string | undefined;
|
||||
email: string | null | undefined;
|
||||
emailVerified: boolean | undefined;
|
||||
isAnonymous: boolean | undefined;
|
||||
tenantId: string | null | undefined;
|
||||
providerInfo: {
|
||||
providerId: string;
|
||||
displayName: string | null;
|
||||
email: string | null;
|
||||
photoUrl: string | null;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
export function handleFirestoreError(error: unknown, operationType: OperationType, path: string | null) {
|
||||
const errInfo: FirestoreErrorInfo = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
authInfo: {
|
||||
userId: auth.currentUser?.uid,
|
||||
email: auth.currentUser?.email,
|
||||
emailVerified: auth.currentUser?.emailVerified,
|
||||
isAnonymous: auth.currentUser?.isAnonymous,
|
||||
tenantId: auth.currentUser?.tenantId,
|
||||
providerInfo: auth.currentUser?.providerData.map(provider => ({
|
||||
providerId: provider.providerId,
|
||||
displayName: provider.displayName,
|
||||
email: provider.email,
|
||||
photoUrl: provider.photoURL
|
||||
})) || []
|
||||
},
|
||||
operationType,
|
||||
path
|
||||
}
|
||||
console.error('Firestore Error: ', JSON.stringify(errInfo));
|
||||
throw new Error(JSON.stringify(errInfo));
|
||||
}
|
||||
|
||||
// Connection test
|
||||
async function testConnection() {
|
||||
try {
|
||||
await getDocFromServer(doc(db, 'test', 'connection'));
|
||||
} catch (error) {
|
||||
if(error instanceof Error && error.message.includes('the client is offline')) {
|
||||
console.error("Please check your Firebase configuration. ");
|
||||
}
|
||||
}
|
||||
}
|
||||
testConnection();
|
||||
|
||||
export { doc, setDoc, getDoc, onSnapshot };
|
||||
export type { FirebaseUser };
|
||||
29
src/index.css
Normal file
29
src/index.css
Normal file
@@ -0,0 +1,29 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@utility no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
/* Theme & Variables */
|
||||
@import "./styles/theme.css";
|
||||
|
||||
/* Base Styles & Scrollbars */
|
||||
@import "./styles/base.css";
|
||||
|
||||
/* Reusable UI Components */
|
||||
@import "./styles/components.css";
|
||||
|
||||
/* Layout Styles */
|
||||
@import "./styles/layout.css";
|
||||
|
||||
/* Assistant Specific Styles */
|
||||
@import "./styles/aura.css";
|
||||
|
||||
/* Authentication Flow Styles */
|
||||
@import "./styles/auth.css";
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
67
src/styles/aura.css
Normal file
67
src/styles/aura.css
Normal file
@@ -0,0 +1,67 @@
|
||||
@layer components {
|
||||
.aura-chat-bubble-user {
|
||||
@apply max-w-[85%] p-4 rounded-[1rem] text-sm text-white rounded-tr-none shadow-sm;
|
||||
background: linear-gradient(135deg, var(--aura-primary), var(--aura-accent-indigo));
|
||||
}
|
||||
|
||||
.aura-chat-bubble-model {
|
||||
@apply max-w-[85%] p-4 rounded-[1rem] text-sm rounded-tl-none border border-aura-border shadow-sm;
|
||||
background-color: var(--aura-surface);
|
||||
color: var(--aura-text);
|
||||
}
|
||||
|
||||
.aura-message-actions {
|
||||
@apply flex items-center gap-1 mt-2 transition-opacity duration-200;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.aura-message-container:hover .aura-message-actions {
|
||||
@apply opacity-100;
|
||||
}
|
||||
.aura-message-actions {
|
||||
@apply opacity-0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.aura-message-actions {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
|
||||
.aura-suggestion-pill-blue {
|
||||
@apply flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-medium whitespace-nowrap transition-colors bg-blue-50 dark:bg-blue-900/20 border-blue-100 dark:border-blue-800/50 hover:bg-blue-100 dark:hover:bg-blue-900/30;
|
||||
color: var(--aura-accent-blue);
|
||||
}
|
||||
|
||||
.aura-suggestion-pill-orange {
|
||||
@apply flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-medium whitespace-nowrap transition-colors bg-orange-50 dark:bg-orange-900/20 border-orange-100 dark:border-orange-800/50 hover:bg-orange-100 dark:hover:bg-orange-900/30;
|
||||
color: var(--aura-accent-orange);
|
||||
}
|
||||
|
||||
.aura-suggestion-pill-indigo {
|
||||
@apply flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-medium whitespace-nowrap transition-colors bg-indigo-50 dark:bg-indigo-900/20 border-indigo-100 dark:border-indigo-800/50 hover:bg-indigo-100 dark:hover:bg-indigo-900/30;
|
||||
color: var(--aura-accent-indigo);
|
||||
}
|
||||
|
||||
.aura-suggestion-pill-red {
|
||||
@apply flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-medium whitespace-nowrap transition-colors bg-red-50 dark:bg-red-900/20 border-red-100 dark:border-red-800/50 hover:bg-red-100 dark:hover:bg-red-900/30;
|
||||
color: var(--aura-accent-red);
|
||||
}
|
||||
|
||||
.aura-voice-circle {
|
||||
@apply w-40 h-40 rounded-full flex items-center justify-center transition-all duration-500;
|
||||
}
|
||||
|
||||
.aura-voice-core {
|
||||
@apply w-24 h-24 rounded-full flex items-center justify-center shadow-xl transition-all duration-300;
|
||||
}
|
||||
|
||||
.aura-meet-circle {
|
||||
@apply w-32 h-32 rounded-full flex items-center justify-center transition-all duration-500;
|
||||
}
|
||||
|
||||
.aura-meet-core {
|
||||
@apply w-16 h-16 rounded-full flex items-center justify-center shadow-lg transition-all;
|
||||
}
|
||||
}
|
||||
45
src/styles/auth.css
Normal file
45
src/styles/auth.css
Normal file
@@ -0,0 +1,45 @@
|
||||
@layer components {
|
||||
.auth-container {
|
||||
@apply min-h-screen flex items-center justify-center p-6 bg-aura-bg relative overflow-hidden;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
@apply aura-glass-card p-12 w-full max-w-xl flex flex-col items-center text-center relative z-10;
|
||||
}
|
||||
|
||||
.auth-step-container {
|
||||
@apply space-y-8 w-full max-w-md;
|
||||
}
|
||||
|
||||
.auth-input-container {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.auth-input-icon {
|
||||
@apply absolute left-5 top-1/2 -translate-y-1/2 text-slate-400 w-6 h-6;
|
||||
}
|
||||
|
||||
.auth-input-field {
|
||||
@apply aura-input py-5 pl-14 pr-6 text-xl;
|
||||
}
|
||||
|
||||
.auth-submit-btn {
|
||||
@apply absolute right-3 top-1/2 -translate-y-1/2 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 p-3 rounded-xl hover:bg-blue-600 dark:hover:bg-blue-400 disabled:opacity-50 transition-all;
|
||||
}
|
||||
|
||||
.auth-progress-indicator {
|
||||
@apply fixed bottom-12 left-1/2 -translate-x-1/2 flex gap-2;
|
||||
}
|
||||
|
||||
.auth-progress-dot {
|
||||
@apply h-1.5 rounded-full transition-all duration-500;
|
||||
}
|
||||
|
||||
.auth-progress-dot-active {
|
||||
@apply w-8 bg-blue-600;
|
||||
}
|
||||
|
||||
.auth-progress-dot-inactive {
|
||||
@apply w-2 bg-slate-200 dark:bg-slate-800;
|
||||
}
|
||||
}
|
||||
49
src/styles/base.css
Normal file
49
src/styles/base.css
Normal file
@@ -0,0 +1,49 @@
|
||||
@layer base {
|
||||
body {
|
||||
@apply font-sans antialiased transition-colors duration-300;
|
||||
background: radial-gradient(circle at 50% 0%, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
color: var(--aura-text);
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
@apply mb-2 last:mb-0;
|
||||
}
|
||||
.markdown-body ul {
|
||||
@apply list-disc list-inside mb-2;
|
||||
}
|
||||
.markdown-body ol {
|
||||
@apply list-decimal list-inside mb-2;
|
||||
}
|
||||
.markdown-body strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
}
|
||||
108
src/styles/components.css
Normal file
108
src/styles/components.css
Normal file
@@ -0,0 +1,108 @@
|
||||
@utility aura-card {
|
||||
@apply backdrop-blur-md border rounded-xl transition-all duration-300 shadow-sm;
|
||||
background-color: color-mix(in srgb, var(--aura-surface), transparent 5%);
|
||||
border-color: var(--aura-border);
|
||||
}
|
||||
|
||||
@utility aura-input {
|
||||
@apply w-full rounded-xl py-3 px-4 transition-all outline-none border-2;
|
||||
background-color: var(--aura-surface);
|
||||
border-color: var(--aura-border);
|
||||
color: var(--aura-text);
|
||||
}
|
||||
|
||||
@utility aura-glass-card {
|
||||
@apply backdrop-blur-xl border rounded-3xl shadow-2xl;
|
||||
background-color: color-mix(in srgb, var(--aura-surface), transparent 20%);
|
||||
border-color: var(--aura-border);
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.aura-card-hover {
|
||||
@apply hover:shadow-md hover:border-blue-500/30 dark:hover:border-blue-400/30;
|
||||
}
|
||||
|
||||
.aura-btn-primary {
|
||||
@apply px-6 py-3 rounded-xl font-bold transition-all flex items-center justify-center gap-2 shadow-lg;
|
||||
background-color: var(--aura-text);
|
||||
color: var(--aura-surface);
|
||||
}
|
||||
|
||||
.aura-btn-primary:hover {
|
||||
@apply opacity-90 scale-[1.02];
|
||||
}
|
||||
|
||||
.aura-btn-secondary {
|
||||
@apply px-4 py-2 rounded-xl font-semibold transition-all flex items-center justify-center gap-2 border;
|
||||
background-color: var(--aura-surface);
|
||||
border-color: var(--aura-border);
|
||||
color: var(--aura-text);
|
||||
}
|
||||
|
||||
.aura-btn-secondary:hover {
|
||||
@apply bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600;
|
||||
}
|
||||
|
||||
.aura-input:focus {
|
||||
@apply border-blue-500/50 ring-4 ring-blue-500/10;
|
||||
}
|
||||
|
||||
.aura-badge {
|
||||
@apply px-2 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wider;
|
||||
}
|
||||
|
||||
.aura-logo {
|
||||
@apply w-10 h-10 rounded-xl flex items-center justify-center shadow-lg;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
|
||||
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.aura-toggle-group {
|
||||
@apply flex p-1 rounded-2xl border shadow-inner;
|
||||
background-color: color-mix(in srgb, var(--aura-bg), var(--aura-text) 5%);
|
||||
border-color: var(--aura-border);
|
||||
}
|
||||
|
||||
.aura-gradient-card {
|
||||
@apply rounded-3xl p-6 text-white shadow-2xl relative overflow-hidden;
|
||||
background: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%);
|
||||
box-shadow: 0 20px 25px -5px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.aura-progress-track {
|
||||
@apply h-1 w-full rounded-full overflow-hidden;
|
||||
background-color: var(--aura-border);
|
||||
}
|
||||
|
||||
.aura-avatar-container {
|
||||
@apply w-24 h-24 rounded-full overflow-hidden border-4 shadow-lg;
|
||||
border-color: var(--aura-border);
|
||||
}
|
||||
|
||||
.aura-row-card {
|
||||
@apply flex items-center gap-3 p-2 rounded-xl transition-colors;
|
||||
background-color: color-mix(in srgb, var(--aura-bg), var(--aura-text) 2%);
|
||||
}
|
||||
|
||||
.aura-row-card:hover {
|
||||
@apply bg-slate-100 dark:bg-slate-800;
|
||||
}
|
||||
|
||||
.aura-gradient-top-bar {
|
||||
@apply absolute top-0 left-0 w-full h-1;
|
||||
background: linear-gradient(to right, #60a5fa, #6366f1, #a855f7);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-[10px] font-bold uppercase tracking-widest mb-3;
|
||||
color: var(--aura-text-muted);
|
||||
}
|
||||
|
||||
.item-row {
|
||||
@apply flex items-center gap-3 p-2 rounded-lg transition-colors cursor-pointer;
|
||||
}
|
||||
|
||||
.item-row:hover {
|
||||
@apply bg-slate-100/50 dark:bg-slate-800/50;
|
||||
}
|
||||
}
|
||||
47
src/styles/layout.css
Normal file
47
src/styles/layout.css
Normal file
@@ -0,0 +1,47 @@
|
||||
@layer components {
|
||||
.aura-dashboard {
|
||||
@apply min-h-screen p-4 bg-aura-bg transition-colors duration-300 w-full;
|
||||
}
|
||||
|
||||
.aura-header {
|
||||
@apply flex items-center justify-between mb-6 px-4 gap-6;
|
||||
}
|
||||
|
||||
.aura-main-grid {
|
||||
@apply flex overflow-x-auto snap-x snap-mandatory xl:grid xl:grid-cols-12 gap-4 mb-4 h-[calc(100vh-120px)] min-h-[600px] xl:min-h-[750px] no-scrollbar;
|
||||
}
|
||||
|
||||
.aura-col-action {
|
||||
@apply w-full flex-shrink-0 snap-center xl:w-auto xl:flex-shrink xl:snap-none xl:col-span-2 aura-card p-4 flex flex-col h-full overflow-hidden;
|
||||
}
|
||||
|
||||
.aura-col-aura {
|
||||
@apply w-full flex-shrink-0 snap-center xl:w-auto xl:flex-shrink xl:snap-none xl:col-span-7 aura-card p-5 flex flex-col h-full relative overflow-hidden;
|
||||
}
|
||||
|
||||
.aura-col-info {
|
||||
@apply w-full flex-shrink-0 snap-center xl:w-auto xl:flex-shrink xl:snap-none xl:col-span-3 aura-card p-4 flex flex-col h-full overflow-hidden;
|
||||
}
|
||||
|
||||
.aura-bottom-grid {
|
||||
@apply grid grid-cols-1 xl:grid-cols-3 gap-4;
|
||||
}
|
||||
|
||||
.aura-toolbar {
|
||||
@apply flex items-center gap-1 backdrop-blur-sm p-1.5 rounded-2xl border shadow-sm;
|
||||
background-color: color-mix(in srgb, var(--aura-surface), transparent 50%);
|
||||
border-color: color-mix(in srgb, var(--aura-border), transparent 50%);
|
||||
}
|
||||
|
||||
.aura-toolbar-item {
|
||||
@apply flex items-center gap-2 rounded-xl cursor-pointer transition-all;
|
||||
}
|
||||
|
||||
.aura-toolbar-item-active {
|
||||
@apply bg-aura-surface shadow-sm text-aura-accent-blue;
|
||||
}
|
||||
|
||||
.aura-toolbar-item-inactive {
|
||||
@apply text-aura-text-muted hover:bg-aura-surface/50 hover:text-aura-text;
|
||||
}
|
||||
}
|
||||
62
src/styles/theme.css
Normal file
62
src/styles/theme.css
Normal file
@@ -0,0 +1,62 @@
|
||||
@theme {
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
|
||||
|
||||
--color-aura-bg: var(--aura-bg);
|
||||
--color-aura-surface: var(--aura-surface);
|
||||
--color-aura-primary: var(--aura-primary);
|
||||
--color-aura-text: var(--aura-text);
|
||||
--color-aura-text-muted: var(--aura-text-muted);
|
||||
--color-aura-border: var(--aura-border);
|
||||
--color-aura-accent: var(--aura-accent);
|
||||
--color-aura-accent-blue: var(--aura-accent-blue);
|
||||
--color-aura-accent-emerald: var(--aura-accent-emerald);
|
||||
--color-aura-accent-red: var(--aura-accent-red);
|
||||
--color-aura-accent-amber: var(--aura-accent-amber);
|
||||
--color-aura-accent-indigo: var(--aura-accent-indigo);
|
||||
--color-aura-accent-purple: var(--aura-accent-purple);
|
||||
--color-aura-accent-orange: var(--aura-accent-orange);
|
||||
}
|
||||
|
||||
:root {
|
||||
--aura-bg: #f1f5f9;
|
||||
--aura-surface: #ffffff;
|
||||
--aura-primary: #2563eb;
|
||||
--aura-primary-hover: #1d4ed8;
|
||||
--aura-text: #0f172a;
|
||||
--aura-text-muted: #475569;
|
||||
--aura-border: #cbd5e1;
|
||||
--aura-accent: #2563eb;
|
||||
--aura-accent-blue: #2563eb;
|
||||
--aura-accent-emerald: #059669;
|
||||
--aura-accent-red: #dc2626;
|
||||
--aura-accent-amber: #d97706;
|
||||
--aura-accent-indigo: #4f46e5;
|
||||
--aura-accent-purple: #9333ea;
|
||||
--aura-accent-orange: #ea580c;
|
||||
|
||||
--bg-gradient-start: #e2e8f0;
|
||||
--bg-gradient-end: #f1f5f9;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--aura-bg: #020617;
|
||||
--aura-surface: #0f172a;
|
||||
--aura-primary: #059669;
|
||||
--aura-primary-hover: #047857;
|
||||
--aura-text: #f8fafc;
|
||||
--aura-text-muted: #94a3b8;
|
||||
--aura-border: #1e293b;
|
||||
--aura-accent: #059669;
|
||||
--aura-accent-blue: #60a5fa;
|
||||
--aura-accent-emerald: #059669;
|
||||
--aura-accent-red: #f87171;
|
||||
--aura-accent-amber: #fbbf24;
|
||||
--aura-accent-indigo: #818cf8;
|
||||
--aura-accent-purple: #c084fc;
|
||||
--aura-accent-orange: #fb923c;
|
||||
|
||||
--bg-gradient-start: #0f172a;
|
||||
--bg-gradient-end: #020617;
|
||||
color-scheme: dark;
|
||||
}
|
||||
28
src/styles/utilities.css
Normal file
28
src/styles/utilities.css
Normal file
@@ -0,0 +1,28 @@
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
173
types.ts
Normal file
173
types.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'model';
|
||||
content: string;
|
||||
context?: string; // Stores full content (e.g., with file attachments) for AI context, while content is for display
|
||||
}
|
||||
|
||||
export type LogStatus = 'pending' | 'success' | 'error' | 'info';
|
||||
|
||||
export interface LogMessage {
|
||||
id: number;
|
||||
message: string;
|
||||
status: LogStatus;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
name: string;
|
||||
company_name?: string;
|
||||
auth_cookie?: string;
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
systemPrompt: string;
|
||||
}
|
||||
|
||||
export type Role = string;
|
||||
|
||||
export type Permission = string;
|
||||
|
||||
export interface AppInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
primary_domain?: string;
|
||||
base_url?: string;
|
||||
studio_app_url?: string;
|
||||
}
|
||||
|
||||
export interface PullRequest {
|
||||
id: number;
|
||||
number?: number;
|
||||
title: string;
|
||||
state: 'open' | 'closed';
|
||||
user: {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
head: string | {
|
||||
ref: string;
|
||||
};
|
||||
base: string | {
|
||||
ref: string;
|
||||
};
|
||||
html_url: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
merged: boolean;
|
||||
merged_at?: string | null;
|
||||
source?: 'gitea' | 'database';
|
||||
environment?: 'non-prod' | 'prod' | 'dev';
|
||||
envName?: string;
|
||||
hasConflicts?: boolean;
|
||||
}
|
||||
|
||||
export interface Organization {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
organizationId: number;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface ProjectComponent {
|
||||
id: number;
|
||||
projectId: number;
|
||||
name: string;
|
||||
title: string;
|
||||
type: 'UX' | 'API';
|
||||
description?: string;
|
||||
status?: 'Pending' | 'Active' | 'Inactive';
|
||||
createdAt?: string;
|
||||
additional_info?: {
|
||||
slug?: string;
|
||||
supported_domains?: string[];
|
||||
ai_studio_link?: string;
|
||||
github_repo?: string;
|
||||
github_owner?: string;
|
||||
hiq_repo?: string;
|
||||
unique_app_code?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export type RequirementType = 'Defect' | 'Feature';
|
||||
export type RequirementCategory = 'User' | 'System' | 'Non-Functional';
|
||||
// Updated to match backend API validation
|
||||
export type RequirementStatus = 'New' | 'Open' | 'InProgress' | 'Resolved' | 'Closed';
|
||||
|
||||
export interface Requirement {
|
||||
id?: number;
|
||||
componentId?: number;
|
||||
type: RequirementType;
|
||||
category?: RequirementCategory;
|
||||
title: string;
|
||||
description: string;
|
||||
status: RequirementStatus;
|
||||
tempId?: string; // For frontend tracking of new items before they have a DB ID
|
||||
unique_hash?: string;
|
||||
}
|
||||
|
||||
export interface ComponentPrompt {
|
||||
id?: number;
|
||||
componentId: number;
|
||||
title: string;
|
||||
description: string;
|
||||
type: 'System' | 'Other';
|
||||
content: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface ComponentContentItem {
|
||||
id?: number;
|
||||
componentId: number;
|
||||
textId: string;
|
||||
description: string;
|
||||
text: string;
|
||||
format: 'plain' | 'markdown';
|
||||
type: 'Title' | 'Body' | 'LinkText' | 'Heading';
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
assignee: string;
|
||||
assigned_date?: string;
|
||||
due_date: string;
|
||||
start_date?: string;
|
||||
close_date?: string;
|
||||
status: 'Open' | 'InProgress' | 'Closed';
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
additional_info?: any;
|
||||
}
|
||||
|
||||
// Added missing LLM and Attachment types
|
||||
export type ModelProvider = 'google' | 'openai';
|
||||
|
||||
export interface LLMConfig {
|
||||
provider: ModelProvider;
|
||||
model: string;
|
||||
apiKey?: string;
|
||||
systemInstruction?: string;
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
id: string;
|
||||
name: string;
|
||||
content: string;
|
||||
isProcessing: boolean;
|
||||
}
|
||||
24
vite.config.ts
Normal file
24
vite.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig, loadEnv} from 'vite';
|
||||
|
||||
export default defineConfig(({mode}) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
define: {
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
},
|
||||
};
|
||||
});
|
||||
4
wrangler.json
Normal file
4
wrangler.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "ai-studio-template",
|
||||
"compatibility_date": "2025-10-20",
|
||||
}
|
||||
Reference in New Issue
Block a user