Initial commit from ux_aura_assistant
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;
|
||||||
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
|
||||||
44
README.md
Normal file
44
README.md
Normal 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
|
||||||
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;
|
||||||
|
};
|
||||||
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;
|
||||||
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];
|
||||||
|
}
|
||||||
57
index.html
Normal file
57
index.html
Normal 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
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="
|
||||||
|
}
|
||||||
|
|
||||||
8
metadata.json
Normal file
8
metadata.json
Normal 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
27
package.json
Normal 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
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}`);
|
||||||
|
};
|
||||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal 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
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;
|
||||||
|
}
|
||||||
28
vite.config.ts
Normal file
28
vite.config.ts
Normal 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
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