oomi-ai 0.2.17 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -202
- package/agent_instructions.md +209 -186
- package/bin/oomi-ai.js +3989 -3460
- package/bin/sessionBridgeState.js +78 -78
- package/lib/channelPluginClient.js +119 -0
- package/lib/personaApiClient.js +221 -0
- package/lib/personaJobExecutor.js +115 -0
- package/lib/personaJobPoller.js +112 -0
- package/lib/personaRuntimeProcess.js +152 -0
- package/lib/scaffold.js +108 -0
- package/lib/template.js +45 -0
- package/openclaw.extension.js +602 -602
- package/openclaw.plugin.json +17 -17
- package/package.json +67 -65
- package/skills/oomi/SKILL.md +191 -191
- package/skills/oomi/agent_instructions.md +80 -80
- package/skills/oomi/config.json +2 -2
- package/skills/oomi/scripts/get_avatar_capabilities.py +40 -40
- package/skills/oomi/scripts/get_data.py +49 -49
- package/skills/oomi/scripts/install_agent_instructions.py +78 -78
- package/skills/oomi/scripts/send_goal.py +53 -53
- package/skills/oomi/scripts/sync.py +46 -46
- package/skills/oomi/setup.py +41 -41
- package/templates/persona-app/.env.example +8 -0
- package/templates/persona-app/README.md +35 -0
- package/templates/persona-app/eslint.config.js +28 -0
- package/templates/persona-app/index.html +18 -0
- package/templates/persona-app/oomi.runtime.json +13 -0
- package/templates/persona-app/package.json +42 -0
- package/templates/persona-app/persona/brief.md +14 -0
- package/templates/persona-app/persona.json +14 -0
- package/templates/persona-app/public/manifest.webmanifest +8 -0
- package/templates/persona-app/public/oomi.health.json +6 -0
- package/templates/persona-app/src/App.css +180 -0
- package/templates/persona-app/src/App.tsx +14 -0
- package/templates/persona-app/src/index.css +32 -0
- package/templates/persona-app/src/main.tsx +10 -0
- package/templates/persona-app/src/pages/HomePage.tsx +73 -0
- package/templates/persona-app/src/pages/ScenePage.tsx +18 -0
- package/templates/persona-app/src/persona/config.ts +6 -0
- package/templates/persona-app/src/persona/notes.ts +5 -0
- package/templates/persona-app/src/vite-env.d.ts +3 -0
- package/templates/persona-app/template.json +13 -0
- package/templates/persona-app/tsconfig.app.json +23 -0
- package/templates/persona-app/tsconfig.json +7 -0
- package/templates/persona-app/tsconfig.node.json +21 -0
- package/templates/persona-app/vite.config.ts +18 -0
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import urllib.request
|
|
2
|
-
import urllib.error
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
|
|
7
|
-
# Load config
|
|
8
|
-
try:
|
|
9
|
-
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
|
|
10
|
-
with open(config_path, 'r') as f:
|
|
11
|
-
config = json.load(f)
|
|
12
|
-
BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
|
|
13
|
-
except:
|
|
14
|
-
BASE_URL = 'http://localhost:3000/api/skill'
|
|
15
|
-
|
|
16
|
-
API_URL = f"{BASE_URL}/sync"
|
|
17
|
-
|
|
18
|
-
def sync_context():
|
|
19
|
-
# In a real scenario, this might read from stdin or a file provided by the agent
|
|
20
|
-
# For now, we send a dummy context
|
|
21
|
-
payload = {
|
|
22
|
-
"agent_id": "nemu-agent",
|
|
23
|
-
"context_summary": "User is actively working on coding tasks.",
|
|
24
|
-
"suggested_mode": "working"
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try:
|
|
28
|
-
req = urllib.request.Request(
|
|
29
|
-
API_URL,
|
|
30
|
-
data=json.dumps(payload).encode('utf-8'),
|
|
31
|
-
headers={'Content-Type': 'application/json'}
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
with urllib.request.urlopen(req) as response:
|
|
35
|
-
result = json.loads(response.read().decode())
|
|
36
|
-
|
|
37
|
-
print(json.dumps(result, indent=2))
|
|
38
|
-
|
|
39
|
-
except urllib.error.URLError as e:
|
|
40
|
-
print(json.dumps({
|
|
41
|
-
"error": f"Failed to sync: {str(e)}"
|
|
42
|
-
}))
|
|
43
|
-
sys.exit(1)
|
|
44
|
-
|
|
45
|
-
if __name__ == "__main__":
|
|
46
|
-
sync_context()
|
|
1
|
+
import urllib.request
|
|
2
|
+
import urllib.error
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Load config
|
|
8
|
+
try:
|
|
9
|
+
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
|
|
10
|
+
with open(config_path, 'r') as f:
|
|
11
|
+
config = json.load(f)
|
|
12
|
+
BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
|
|
13
|
+
except:
|
|
14
|
+
BASE_URL = 'http://localhost:3000/api/skill'
|
|
15
|
+
|
|
16
|
+
API_URL = f"{BASE_URL}/sync"
|
|
17
|
+
|
|
18
|
+
def sync_context():
|
|
19
|
+
# In a real scenario, this might read from stdin or a file provided by the agent
|
|
20
|
+
# For now, we send a dummy context
|
|
21
|
+
payload = {
|
|
22
|
+
"agent_id": "nemu-agent",
|
|
23
|
+
"context_summary": "User is actively working on coding tasks.",
|
|
24
|
+
"suggested_mode": "working"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
req = urllib.request.Request(
|
|
29
|
+
API_URL,
|
|
30
|
+
data=json.dumps(payload).encode('utf-8'),
|
|
31
|
+
headers={'Content-Type': 'application/json'}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
with urllib.request.urlopen(req) as response:
|
|
35
|
+
result = json.loads(response.read().decode())
|
|
36
|
+
|
|
37
|
+
print(json.dumps(result, indent=2))
|
|
38
|
+
|
|
39
|
+
except urllib.error.URLError as e:
|
|
40
|
+
print(json.dumps({
|
|
41
|
+
"error": f"Failed to sync: {str(e)}"
|
|
42
|
+
}))
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
sync_context()
|
package/skills/oomi/setup.py
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
|
|
5
|
-
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.json')
|
|
6
|
-
|
|
7
|
-
def setup():
|
|
8
|
-
print("Oomi Skill Setup")
|
|
9
|
-
print("================")
|
|
10
|
-
|
|
11
|
-
# Load existing config or defaults
|
|
12
|
-
config = {"api_url": "http://localhost:3000/api/skill"}
|
|
13
|
-
if os.path.exists(CONFIG_FILE):
|
|
14
|
-
try:
|
|
15
|
-
with open(CONFIG_FILE, 'r') as f:
|
|
16
|
-
config.update(json.load(f))
|
|
17
|
-
except:
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
# Prompt user
|
|
21
|
-
print(f"\nCurrent API URL: {config.get('api_url')}")
|
|
22
|
-
new_url = input("Enter new API URL (press Enter to keep current): ").strip()
|
|
23
|
-
|
|
24
|
-
if new_url:
|
|
25
|
-
# Remove trailing slash if present
|
|
26
|
-
if new_url.endswith('/'):
|
|
27
|
-
new_url = new_url[:-1]
|
|
28
|
-
config['api_url'] = new_url
|
|
29
|
-
|
|
30
|
-
# Save
|
|
31
|
-
try:
|
|
32
|
-
with open(CONFIG_FILE, 'w') as f:
|
|
33
|
-
json.dump(config, f, indent=2)
|
|
34
|
-
print(f"\nConfiguration saved to {CONFIG_FILE}")
|
|
35
|
-
print("Setup complete!")
|
|
36
|
-
except Exception as e:
|
|
37
|
-
print(f"\nError saving configuration: {e}")
|
|
38
|
-
sys.exit(1)
|
|
39
|
-
|
|
40
|
-
if __name__ == "__main__":
|
|
41
|
-
setup()
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.json')
|
|
6
|
+
|
|
7
|
+
def setup():
|
|
8
|
+
print("Oomi Skill Setup")
|
|
9
|
+
print("================")
|
|
10
|
+
|
|
11
|
+
# Load existing config or defaults
|
|
12
|
+
config = {"api_url": "http://localhost:3000/api/skill"}
|
|
13
|
+
if os.path.exists(CONFIG_FILE):
|
|
14
|
+
try:
|
|
15
|
+
with open(CONFIG_FILE, 'r') as f:
|
|
16
|
+
config.update(json.load(f))
|
|
17
|
+
except:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
# Prompt user
|
|
21
|
+
print(f"\nCurrent API URL: {config.get('api_url')}")
|
|
22
|
+
new_url = input("Enter new API URL (press Enter to keep current): ").strip()
|
|
23
|
+
|
|
24
|
+
if new_url:
|
|
25
|
+
# Remove trailing slash if present
|
|
26
|
+
if new_url.endswith('/'):
|
|
27
|
+
new_url = new_url[:-1]
|
|
28
|
+
config['api_url'] = new_url
|
|
29
|
+
|
|
30
|
+
# Save
|
|
31
|
+
try:
|
|
32
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
33
|
+
json.dump(config, f, indent=2)
|
|
34
|
+
print(f"\nConfiguration saved to {CONFIG_FILE}")
|
|
35
|
+
print("Setup complete!")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"\nError saving configuration: {e}")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
setup()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# __OOMI_PERSONA_NAME__
|
|
2
|
+
|
|
3
|
+
This project was scaffolded by `oomi personas scaffold`.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This app is intended to run inside the Oomi client as a managed persona surface and uses the WebSpatial quick-example pattern as its base.
|
|
8
|
+
|
|
9
|
+
## Editable Zones
|
|
10
|
+
|
|
11
|
+
Only customize files in these zones unless Oomi explicitly changes the scaffold contract:
|
|
12
|
+
|
|
13
|
+
- `src/persona/`
|
|
14
|
+
- `persona/`
|
|
15
|
+
|
|
16
|
+
## Runtime Contract
|
|
17
|
+
|
|
18
|
+
- Template version: `__OOMI_TEMPLATE_VERSION__`
|
|
19
|
+
- Health document: `/oomi.health.json`
|
|
20
|
+
- Runtime metadata document: `/oomi.runtime.json`
|
|
21
|
+
- Manifest: `/manifest.webmanifest`
|
|
22
|
+
- Default dev port: `4789`
|
|
23
|
+
|
|
24
|
+
## Local Development
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install
|
|
28
|
+
npm run dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Notes
|
|
32
|
+
|
|
33
|
+
- Preserve the WebSpatial/Vite shell and the files in `public/`.
|
|
34
|
+
- Keep the app safe to embed inside Oomi and future XR clients.
|
|
35
|
+
- Customize persona behavior in `src/persona/`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
4
|
+
import reactRefresh from "eslint-plugin-react-refresh";
|
|
5
|
+
import tseslint from "typescript-eslint";
|
|
6
|
+
|
|
7
|
+
export default tseslint.config(
|
|
8
|
+
{ ignores: ["dist"] },
|
|
9
|
+
{
|
|
10
|
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
11
|
+
files: ["**/*.{ts,tsx}"],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
ecmaVersion: 2020,
|
|
14
|
+
globals: globals.browser,
|
|
15
|
+
},
|
|
16
|
+
plugins: {
|
|
17
|
+
"react-hooks": reactHooks,
|
|
18
|
+
"react-refresh": reactRefresh,
|
|
19
|
+
},
|
|
20
|
+
rules: {
|
|
21
|
+
...reactHooks.configs.recommended.rules,
|
|
22
|
+
"react-refresh/only-export-components": [
|
|
23
|
+
"warn",
|
|
24
|
+
{ allowConstantExport: true },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<%- XR_ENV === 'avp' ? `
|
|
3
|
+
<html lang="en" class="is-spatial">
|
|
4
|
+
` : `
|
|
5
|
+
<html lang="en">
|
|
6
|
+
` %>
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8" />
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
11
|
+
<title>__OOMI_PERSONA_NAME__</title>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="root"></div>
|
|
15
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"templateVersion": "__OOMI_TEMPLATE_VERSION__",
|
|
3
|
+
"appKind": "oomi-persona-app",
|
|
4
|
+
"healthPath": "/oomi.health.json",
|
|
5
|
+
"defaultPort": 4789,
|
|
6
|
+
"supportsRuntimeRegistration": true,
|
|
7
|
+
"renderMode": "webspatial",
|
|
8
|
+
"entryDocument": "/index.html",
|
|
9
|
+
"editableZones": [
|
|
10
|
+
"src/persona",
|
|
11
|
+
"persona"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oomi-persona-__OOMI_PERSONA_SLUG__",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"install:clean": "npm install",
|
|
8
|
+
"dev": "vite --host 127.0.0.1 --port 4789",
|
|
9
|
+
"dev:avp": "cross-env XR_ENV=avp vite --host 127.0.0.1 --port 4789",
|
|
10
|
+
"build": "vite build && cross-env XR_ENV=avp vite build",
|
|
11
|
+
"preview": "vite preview --host 127.0.0.1 --port 4789",
|
|
12
|
+
"lint": "eslint ."
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@webspatial/core-sdk": "^0.1.16",
|
|
16
|
+
"@webspatial/react-sdk": "^0.1.16",
|
|
17
|
+
"react": "^19.0.0",
|
|
18
|
+
"react-dom": "^19.0.0",
|
|
19
|
+
"react-router-dom": "^7.4.0",
|
|
20
|
+
"three": "^0.170.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/js": "^9.21.0",
|
|
24
|
+
"@types/react": "^19.0.10",
|
|
25
|
+
"@types/react-dom": "^19.0.4",
|
|
26
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
27
|
+
"@webspatial/builder": "^0.1.16",
|
|
28
|
+
"@webspatial/platform-visionos": "^0.1.16",
|
|
29
|
+
"@webspatial/vite-plugin": "^0.1.7",
|
|
30
|
+
"cross-env": "^7.0.3",
|
|
31
|
+
"dotenv-cli": "^8.0.0",
|
|
32
|
+
"eslint": "^9.21.0",
|
|
33
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
34
|
+
"eslint-plugin-react-refresh": "^0.4.19",
|
|
35
|
+
"globals": "^15.15.0",
|
|
36
|
+
"typescript": "~5.7.2",
|
|
37
|
+
"typescript-eslint": "^8.24.1",
|
|
38
|
+
"vite": "^6.2.0",
|
|
39
|
+
"vite-plugin-html": "^3.2.2"
|
|
40
|
+
},
|
|
41
|
+
"packageManager": "pnpm@10.6.4"
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Persona Brief
|
|
2
|
+
|
|
3
|
+
Name: __OOMI_PERSONA_NAME__
|
|
4
|
+
Slug: __OOMI_PERSONA_SLUG__
|
|
5
|
+
|
|
6
|
+
## User Intent
|
|
7
|
+
|
|
8
|
+
__OOMI_PERSONA_DESCRIPTION__
|
|
9
|
+
|
|
10
|
+
## Notes For The Agent
|
|
11
|
+
|
|
12
|
+
- Start from the scaffolded shell.
|
|
13
|
+
- Keep the runtime and health documents intact.
|
|
14
|
+
- Add persona-specific data flows inside `src/persona/` or `persona/`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "__OOMI_PERSONA_SLUG__",
|
|
3
|
+
"name": "__OOMI_PERSONA_NAME__",
|
|
4
|
+
"summary": "__OOMI_PERSONA_DESCRIPTION__",
|
|
5
|
+
"status": "inactive",
|
|
6
|
+
"type": "persona",
|
|
7
|
+
"templateType": "persona-app",
|
|
8
|
+
"promptTemplateVersion": "__OOMI_TEMPLATE_VERSION__",
|
|
9
|
+
"capabilities": [],
|
|
10
|
+
"dataSources": [],
|
|
11
|
+
"tags": [
|
|
12
|
+
"managed"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#root {
|
|
2
|
+
width: 100%;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.persona-shell {
|
|
6
|
+
width: min(1120px, calc(100% - 48px));
|
|
7
|
+
margin: 0 auto;
|
|
8
|
+
padding: 32px 0 48px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.persona-header {
|
|
12
|
+
display: grid;
|
|
13
|
+
grid-template-columns: 1.4fr 0.8fr;
|
|
14
|
+
gap: 20px;
|
|
15
|
+
align-items: start;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.persona-panel {
|
|
19
|
+
border: 1px solid rgba(117, 94, 59, 0.2);
|
|
20
|
+
background: rgba(255, 250, 242, 0.88);
|
|
21
|
+
backdrop-filter: blur(20px);
|
|
22
|
+
border-radius: 28px;
|
|
23
|
+
box-shadow: 0 24px 80px rgba(63, 48, 23, 0.1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.persona-hero {
|
|
27
|
+
padding: 28px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.persona-eyebrow {
|
|
31
|
+
margin: 0;
|
|
32
|
+
font-size: 11px;
|
|
33
|
+
letter-spacing: 0.28em;
|
|
34
|
+
text-transform: uppercase;
|
|
35
|
+
color: #6d6256;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.persona-title {
|
|
39
|
+
margin: 14px 0 0;
|
|
40
|
+
font-size: clamp(2.6rem, 5vw, 4.6rem);
|
|
41
|
+
line-height: 0.94;
|
|
42
|
+
letter-spacing: -0.04em;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.persona-description {
|
|
46
|
+
margin: 16px 0 0;
|
|
47
|
+
max-width: 52rem;
|
|
48
|
+
color: #4f463b;
|
|
49
|
+
font-size: 1rem;
|
|
50
|
+
line-height: 1.8;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.persona-runtime {
|
|
54
|
+
padding: 24px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.runtime-list {
|
|
58
|
+
display: grid;
|
|
59
|
+
gap: 10px;
|
|
60
|
+
margin-top: 14px;
|
|
61
|
+
color: #4f463b;
|
|
62
|
+
font-size: 0.96rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.persona-grid {
|
|
66
|
+
display: grid;
|
|
67
|
+
grid-template-columns: 1.2fr 0.8fr;
|
|
68
|
+
gap: 20px;
|
|
69
|
+
margin-top: 20px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.persona-card {
|
|
73
|
+
padding: 24px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.persona-card h2 {
|
|
77
|
+
margin: 0;
|
|
78
|
+
font-size: 1rem;
|
|
79
|
+
text-transform: uppercase;
|
|
80
|
+
letter-spacing: 0.2em;
|
|
81
|
+
color: #6d6256;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.persona-card p,
|
|
85
|
+
.persona-card li {
|
|
86
|
+
color: #51483d;
|
|
87
|
+
line-height: 1.8;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.persona-actions {
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-wrap: wrap;
|
|
93
|
+
gap: 12px;
|
|
94
|
+
margin-top: 18px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.persona-button,
|
|
98
|
+
.persona-link {
|
|
99
|
+
display: inline-flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
min-height: 48px;
|
|
103
|
+
padding: 0 18px;
|
|
104
|
+
border-radius: 14px;
|
|
105
|
+
border: 1px solid rgba(117, 94, 59, 0.22);
|
|
106
|
+
background: rgba(250, 242, 228, 0.96);
|
|
107
|
+
color: #2f291f;
|
|
108
|
+
text-decoration: none;
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.persona-button:hover,
|
|
113
|
+
.persona-link:hover {
|
|
114
|
+
border-color: rgba(117, 94, 59, 0.42);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.persona-scene-card {
|
|
118
|
+
min-height: 240px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.scene-shell {
|
|
122
|
+
display: grid;
|
|
123
|
+
place-items: center;
|
|
124
|
+
min-height: 100vh;
|
|
125
|
+
padding: 32px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.scene-panel {
|
|
129
|
+
width: min(720px, 100%);
|
|
130
|
+
padding: 28px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.scene-panel h1 {
|
|
134
|
+
margin: 0;
|
|
135
|
+
font-size: clamp(2rem, 4vw, 3.2rem);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.scene-panel p {
|
|
139
|
+
color: #51483d;
|
|
140
|
+
line-height: 1.8;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
html.is-spatial {
|
|
144
|
+
background: transparent;
|
|
145
|
+
--xr-background-material: transparent;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
html.is-spatial .persona-panel {
|
|
149
|
+
--xr-background-material: thick;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
html.is-spatial .persona-runtime {
|
|
153
|
+
--xr-background-material: translucent;
|
|
154
|
+
--xr-back: 60;
|
|
155
|
+
transform: translateZ(18px) rotateX(12deg);
|
|
156
|
+
transform-origin: top right;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
html.is-spatial .persona-scene-card {
|
|
160
|
+
--xr-background-material: thin;
|
|
161
|
+
--xr-back: 36;
|
|
162
|
+
transform: translateZ(24px) rotateX(14deg);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
html.is-spatial .persona-link,
|
|
166
|
+
html.is-spatial .persona-button {
|
|
167
|
+
--xr-background-material: thick;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@media (max-width: 900px) {
|
|
171
|
+
.persona-shell {
|
|
172
|
+
width: min(100%, calc(100% - 28px));
|
|
173
|
+
padding-top: 18px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.persona-header,
|
|
177
|
+
.persona-grid {
|
|
178
|
+
grid-template-columns: 1fr;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
|
2
|
+
import { HomePage } from "./pages/HomePage";
|
|
3
|
+
import { ScenePage } from "./pages/ScenePage";
|
|
4
|
+
|
|
5
|
+
export default function App() {
|
|
6
|
+
return (
|
|
7
|
+
<Router basename={__XR_ENV_BASE__}>
|
|
8
|
+
<Routes>
|
|
9
|
+
<Route path="/" element={<HomePage />} />
|
|
10
|
+
<Route path="/scene" element={<ScenePage />} />
|
|
11
|
+
</Routes>
|
|
12
|
+
</Router>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: "Segoe UI", system-ui, sans-serif;
|
|
3
|
+
line-height: 1.5;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
color: #2a251f;
|
|
6
|
+
background:
|
|
7
|
+
radial-gradient(circle at top, rgba(205, 183, 143, 0.32), transparent 36%),
|
|
8
|
+
linear-gradient(180deg, #f7f2e8 0%, #efe7d8 100%);
|
|
9
|
+
font-synthesis: none;
|
|
10
|
+
text-rendering: optimizeLegibility;
|
|
11
|
+
-webkit-font-smoothing: antialiased;
|
|
12
|
+
-moz-osx-font-smoothing: grayscale;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
* {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
min-width: 320px;
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
button,
|
|
26
|
+
a {
|
|
27
|
+
font: inherit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
button {
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { initScene } from "@webspatial/react-sdk";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import "../App.css";
|
|
4
|
+
import { personaConfig } from "../persona/config";
|
|
5
|
+
import { personaNotes } from "../persona/notes";
|
|
6
|
+
|
|
7
|
+
export function HomePage() {
|
|
8
|
+
const openSpatialScene = () => {
|
|
9
|
+
initScene("personaScene", prevConfig => ({
|
|
10
|
+
...prevConfig,
|
|
11
|
+
defaultSize: {
|
|
12
|
+
width: 720,
|
|
13
|
+
height: 720,
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
window.open(`${__XR_ENV_BASE__}/scene`, "personaScene");
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<main className="persona-shell">
|
|
21
|
+
<section className="persona-header">
|
|
22
|
+
<div className="persona-panel persona-hero">
|
|
23
|
+
<p className="persona-eyebrow">WebSpatial Persona</p>
|
|
24
|
+
<h1 className="persona-title">{personaConfig.name}</h1>
|
|
25
|
+
<p className="persona-description">{personaConfig.description}</p>
|
|
26
|
+
|
|
27
|
+
<div className="persona-actions">
|
|
28
|
+
<button className="persona-button" onClick={openSpatialScene} enable-xr>
|
|
29
|
+
Open Spatial Scene
|
|
30
|
+
</button>
|
|
31
|
+
<Link className="persona-link" to="/scene" target="_blank" enable-xr>
|
|
32
|
+
Open Scene Route
|
|
33
|
+
</Link>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<aside className="persona-panel persona-runtime">
|
|
38
|
+
<p className="persona-eyebrow">Runtime</p>
|
|
39
|
+
<div className="runtime-list">
|
|
40
|
+
<div>Slug: {personaConfig.slug}</div>
|
|
41
|
+
<div>Template: {personaConfig.templateVersion}</div>
|
|
42
|
+
<div>Engine: WebSpatial + React + Vite</div>
|
|
43
|
+
<div>Mode: local/private</div>
|
|
44
|
+
</div>
|
|
45
|
+
</aside>
|
|
46
|
+
</section>
|
|
47
|
+
|
|
48
|
+
<section className="persona-grid">
|
|
49
|
+
<article className="persona-panel persona-card">
|
|
50
|
+
<h2>Editable Persona Notes</h2>
|
|
51
|
+
<p>
|
|
52
|
+
This scaffold follows the WebSpatial quick-example structure so the app can render
|
|
53
|
+
well in both the browser and future XR clients. Keep persona-specific logic in
|
|
54
|
+
<code> src/persona/</code>.
|
|
55
|
+
</p>
|
|
56
|
+
<ul>
|
|
57
|
+
{personaNotes.map(note => (
|
|
58
|
+
<li key={note}>{note}</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
</article>
|
|
62
|
+
|
|
63
|
+
<article className="persona-panel persona-card persona-scene-card" enable-xr>
|
|
64
|
+
<h2>Scene Surface</h2>
|
|
65
|
+
<p enable-xr>
|
|
66
|
+
Use this card as the main spatial handoff. It already uses WebSpatial router
|
|
67
|
+
basenames, scene launching, and `enable-xr` affordances taken from the quick example.
|
|
68
|
+
</p>
|
|
69
|
+
</article>
|
|
70
|
+
</section>
|
|
71
|
+
</main>
|
|
72
|
+
);
|
|
73
|
+
}
|