Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
25 changes: 25 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#020607" />
<meta
name="description"
content="Persona.OS — a cinematic liquid-metal persona interface."
/>
<title>Persona.OS — Identity Matrix</title>
<link rel="stylesheet" href="/src/styles.css" />
<script type="importmap">
{
"imports": {
"three": "/node_modules/three/build/three.module.js"
}
}
</script>
</head>
<body>
<main id="app"></main>
<script type="module" src="/src/main.js"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "persona-interaction-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "node scripts/dev-server.mjs",
"test": "node --test tests/*.test.js",
"build": "node scripts/build.mjs",
"smoke": "node scripts/smoke-test.mjs"
},
"dependencies": {
"three": "^0.184.0"
}
}
Binary file added frontend/public/assets/custom-chrome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/jarvis-core.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/johnny-silverhand.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v2/card-jarvis-optics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v2/card-johnny-noise.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v2/card-self-mercury.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v2/card-winter-memory.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v2/jarvis-silver-dream.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v2/johnny-silver-dream.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v2/winter-silver-dream.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v3/crystal-chamber.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v3/jarvis-reactor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/v3/johnny-exact.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/winter-soldier.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions frontend/scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

const root = join(fileURLToPath(new URL(".", import.meta.url)), "..");
const dist = join(root, "dist");
const required = ["index.html", "src/main.js", "src/styles.css"];
const threeModule = join(root, "node_modules/three/build/three.module.js");

for (const path of required) {
if (!existsSync(join(root, path))) {
throw new Error(`Missing required frontend file: ${path}`);
}
}

rmSync(dist, { recursive: true, force: true });
mkdirSync(dist, { recursive: true });
cpSync(join(root, "index.html"), join(dist, "index.html"));
cpSync(join(root, "src"), join(dist, "src"), { recursive: true });

if (existsSync(join(root, "public"))) {
cpSync(join(root, "public"), join(dist, "public"), { recursive: true });
}

const threeTarget = join(dist, "node_modules/three/build");
mkdirSync(threeTarget, { recursive: true });
cpSync(threeModule, join(threeTarget, "three.module.js"));

console.log("Static build prepared in dist/");
36 changes: 36 additions & 0 deletions frontend/scripts/dev-server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createReadStream, existsSync, statSync } from "node:fs";
import { createServer } from "node:http";
import { extname, join, normalize } from "node:path";
import { fileURLToPath } from "node:url";

const root = normalize(join(fileURLToPath(new URL(".", import.meta.url)), ".."));
const port = Number(process.env.PORT || 4173);

const mime = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".svg": "image/svg+xml",
};

createServer((request, response) => {
const rawPath = decodeURIComponent(new URL(request.url, "http://localhost").pathname);
const requested = rawPath === "/" ? "/index.html" : rawPath;
const filePath = normalize(join(root, requested));

if (!filePath.startsWith(root) || !existsSync(filePath) || !statSync(filePath).isFile()) {
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
response.end("Not found");
return;
}

response.writeHead(200, {
"Cache-Control": "no-store",
"Content-Type": mime[extname(filePath)] || "application/octet-stream",
});
createReadStream(filePath).pipe(response);
}).listen(port, "0.0.0.0", () => {
console.log(`Persona.OS preview: http://0.0.0.0:${port}`);
});
37 changes: 37 additions & 0 deletions frontend/scripts/smoke-test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const baseUrl = process.env.PREVIEW_URL || "http://127.0.0.1:4173";

const checks = [
["/", "Persona.OS — Identity Matrix"],
["/src/main.js", "mountChatView"],
["/src/styles.css", ".crystal-chamber"],
["/src/components/crystal-field.js", "class CrystalField"],
["/src/components/persona-select-view.js", "projected-arm-layer"],
["/src/components/xyz-editor-view.js", "mountXyzEditor"],
["/src/components/chat-view.js", "mountChatView"],
["/node_modules/three/build/three.module.js", "WebGLRenderer"],
["/public/assets/v2/winter-silver-dream.png", null],
["/public/assets/v2/custom-silver-dream-arm.png", null],
["/public/assets/v3/johnny-exact.png", null],
["/public/assets/v3/jarvis-reactor.png", null],
["/public/assets/v3/crystal-chamber.png", null],
["/public/assets/v2/card-winter-memory.png", null],
["/public/assets/v2/card-johnny-noise.png", null],
["/public/assets/v2/card-jarvis-optics.png", null],
["/public/assets/v2/card-self-mercury.png", null],
];

for (const [path, expected] of checks) {
const response = await fetch(`${baseUrl}${path}`);
if (!response.ok) {
throw new Error(`${path} returned ${response.status}`);
}

if (expected) {
const content = await response.text();
if (!content.includes(expected)) {
throw new Error(`${path} did not include ${expected}`);
}
}
}

console.log(`Smoke test passed against ${baseUrl}`);
34 changes: 34 additions & 0 deletions frontend/src/adapters/mock-agent-adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const RESPONSES = {
winter: [
"收到。我会先判断风险,再给你最短的行动路线。",
"环境稳定。把目标说清楚,剩下的交给我。",
],
johnny: [
"规矩不是答案。你先说想打破哪一条。",
"听见了。别给我漂亮话,给我真正的问题。",
],
jarvis: [
"系统已就绪。我会为你整理信息,并保留必要的判断依据。",
"当然。建议先确定优先级,然后逐项处理。",
],
custom: [
"我正在按照你设定的人格坐标回应。告诉我,此刻你最想讨论什么?",
"人格参数已同步。我们可以从一个具体问题开始。",
],
};

export function createMockAgentAdapter({
delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
} = {}) {
let responseIndex = 0;

return {
async reply({ personaId }) {
await delay(620);
const options = RESPONSES[personaId] ?? RESPONSES.custom;
const response = options[responseIndex % options.length];
responseIndex += 1;
return response;
},
};
}
31 changes: 31 additions & 0 deletions frontend/src/adapters/mock-media-adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export function createMockMediaAdapter({
delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
} = {}) {
let state = "idle";
const listeners = new Set();

function update(next) {
state = next;
listeners.forEach((listener) => listener(state));
}

return {
getState: () => state,
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
startRecording() {
if (state !== "idle") return false;
update("recording");
return true;
},
async stopRecording() {
if (state !== "recording") return "";
update("transcribing");
await delay(760);
update("idle");
return "你好,我想了解现在的人格状态。";
},
};
}
Loading