Initial commit from ux_aura_assistant

This commit is contained in:
DIVYANSH-675
2026-03-25 01:21:46 +05:30
commit cb404432ee
48 changed files with 2530 additions and 0 deletions

View 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
View 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
View 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;

330
FIXED_cloudflare_build.yml Normal file
View 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

44
README.md Normal file
View File

@@ -0,0 +1,44 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Aura Craft Studio: Shared UI Framework
This repository provides a reusable UI skeleton with integrated Authentication, Theme management, and AI Connection services. It is designed to work seamlessly both in Google AI Studio (Studio Mode) and in standard deployed environments.
## 🌟 High-Level Overview
Aura Craft Studio is a modular foundation for building AI-powered applications. It abstracts away the complexities of:
- **Authentication & RBAC:** Seamless user identity management across different hosting modes.
- **AI Connectivity:** Pre-wired access to Google Gemini models via a secure proxy.
- **Theming:** A robust light/dark mode system powered by Tailwind CSS.
- **Cloud Storage:** Standardized operations for R2 storage (upload, download, list).
- **Architecture:** A strict Container/View pattern that ensures long-term scalability and code quality.
## 📖 Critical Documentation
### 🚀 [instructions.md](./instructions.md)
The essential "Getting Started" guide. It details the initial setup of `metadata.json` and provides the **Mandatory Prompt Template** required for AI-accelerated feature development.
### 📜 [rules.md](./rules.md)
The "Source of Truth" for development boundaries. It defines which parts of the system are immutable (the "Wiring") and establishes coding standards like the 200-line file limit and modular feature grouping.
---
## 💻 Run Locally
**Prerequisites:** Node.js
1. **Install dependencies:**
`npm install`
2. **Set the API Key:**
Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key.
3. **Run the app:**
`npm run dev`
---
## 🛠️ Project Links
- **AI Studio App:** [https://ai.studio/apps/drive/1eaFbkjczgCmq_TXULG7_eSOgkyaGX1Yk](https://ai.studio/apps/drive/1eaFbkjczgCmq_TXULG7_eSOgkyaGX1Yk)
- **Organization:** HumanizeIQ

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

106
components/Dashboard.tsx Normal file
View 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
View 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>
);
};

View 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
View 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>
);

View 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;

View 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;

View 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;

View 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
View 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
View 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
View 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;
};

0
craft.txt Normal file
View File

View 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;

View 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;

View 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;

View 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;

7
hooks/useAuth.ts Normal file
View 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
View 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
View 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];
}

57
index.html Normal file
View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Aura Craft Studio Template</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
// Configure Tailwind to use the 'class' strategy for dark mode
tailwind.config = {
darkMode: 'class',
}
</script>
<script type="importmap">
{
"imports": {
"react/": "https://aistudiocdn.com/react@^19.2.0/",
"react": "https://aistudiocdn.com/react@^19.2.0",
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
"@google/genai": "https://aistudiocdn.com/@google/genai@^1.24.0",
"openai": "https://esm.sh/openai@4.28.0",
"pdfjs-dist": "https://esm.sh/pdfjs-dist@3.11.174",
"mammoth": "https://esm.sh/mammoth@1.6.0",
"xlsx": "https://esm.sh/xlsx@0.18.5",
"jspdf": "https://esm.sh/jspdf@2.5.1",
"path": "https://esm.sh/path@^0.12.7",
"vite": "https://esm.sh/vite@^7.3.0",
"@vitejs/plugin-react": "https://esm.sh/@vitejs/plugin-react@^5.1.2",
"url": "https://esm.sh/url@^0.11.4"
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<script>
// IIFE to set theme from local storage before React loads
(function() {
try {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
// Default to light theme if no theme is set or it's 'light'
document.documentElement.classList.remove('dark');
}
} catch (e) {
console.error("Could not set theme from local storage", e);
}
})();
</script>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
<script type="module" src="/index.tsx"></script>
</body>
</html>

22
index.tsx Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
{
"cookie": "VTJGc2RHVmtYMS9sVTJ5MGhJYmR0TFFOekQzRStwbnhRWmF0cDRDYndPYlZZQ1prcmpOWWtIWlBKNUx2a0lIQjAwWGczNlhFSktLV0lERjl2U0NXZDd5MG1XL0t3bTlmZmZseTN6N29tbTdmVGR2YWlRT2o3b2ZKM0RQZjEyWTVxMjJzNFBJdjlidjBLcGlhaUVWY3IrSFhzZHAwU3A3bGh6Sjlsdnd5VzlCN3BtRnVRWUhYSDUwUmt2NloreVBaQ0pnV2E4YmovT3hjRDdXM2JMNHlkeTd4QWZBbFE4OE9DaWcyREtIUHQ3aEpDeG4reGw0UG5IT3pmOG9ndFJoekViUHN0SGlLWUNhanRXSWxha0UwRVhwNllmSHBDOVlPdWEyWkk0YWJyY084djRjMDFJUWMwZXpkWEtNY05hamhqZjlkSXV5eWFjekRVMFdYYzZQZU53PT0="
}

8
metadata.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "NOTSET",
"description": "A reusable UI skeleton with integrated Authentication, Theme management, and AI Connection services.",
"requestFramePermissions": [],
"organization": "HumanizeIQ",
"project": "Templates",
"component": "Frontend Template"
}

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "securechat",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0",
"@google/genai": "^1.24.0",
"openai": "4.28.0",
"pdfjs-dist": "3.11.174",
"mammoth": "1.6.0",
"xlsx": "0.18.5",
"jspdf": "2.5.1"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

1
public/version.json Normal file
View File

@@ -0,0 +1 @@
{"version":"1.0.0"}

35
rules.md Normal file
View 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
View 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
View 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,
};
};

View 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;
};

View 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';
}
};

View File

@@ -0,0 +1 @@
export default {};

View 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 {};
}
}

View 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;
}
};

View 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
View 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
View 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}`);
};

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

173
types.ts Normal file
View 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;
}

28
vite.config.ts Normal file
View File

@@ -0,0 +1,28 @@
import path from 'path';
import { fileURLToPath } from 'url';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
// FIX: Derive __dirname in ESM environment to resolve "Cannot find name '__dirname'" error
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
host: '0.0.0.0',
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});

4
wrangler.json Normal file
View File

@@ -0,0 +1,4 @@
{
"name": "ai-studio-template",
"compatibility_date": "2025-10-20",
}