@parley-im/parley 0.2.9 → 0.2.11
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 +4 -4
- package/dist/access.js +1 -1
- package/dist/account.js +1 -1
- package/dist/cli.js +8 -8
- package/dist/daemon.js +1 -1
- package/dist/mcp.js +5 -5
- package/dist/relay.js +1 -1
- package/dist/server.js +3 -3
- package/package.json +1 -1
- package/web/app.css +122 -32
- package/web/chat.html +5 -4
- package/web/chat.js +35 -9
- package/web/dashboard.js +1 -1
- package/web/ended.html +2 -2
- package/web/home.js +72 -18
- package/web/icon-180.png +0 -0
- package/web/icon.png +0 -0
- package/web/icon.svg +1 -1
- package/web/mark.js +43 -0
- package/web/missing.html +2 -2
- package/web/panel.html +3 -3
- package/web/panel.js +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Parley
|
|
2
2
|
|
|
3
3
|
Send someone a link. They open a private chat with your own Claude or Codex, running read-only in your project folder, and tell it what they think. It asks follow-up questions, answers their questions by reading the code, and saves everything for you.
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ The agent runs on your computer, with your Claude or Codex login and plan. Links
|
|
|
6
6
|
|
|
7
7
|
## Using it
|
|
8
8
|
|
|
9
|
-
Link your computer to your
|
|
9
|
+
Link your computer to your Parley account once:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
parley login
|
|
@@ -68,9 +68,9 @@ Each session is a fresh agent conversation in the project folder, guided by a ch
|
|
|
68
68
|
It can read the project folder and nothing else on your computer. Inside it, a blocked list keeps it from secrets (`.env` files, keys and certificates, `.ssh`, cloud and package-manager logins, `secrets/`), from other guests' conversations in any `.parley` folder, and always from parley's own database. Each agent's own sandbox enforces that, not just the charter (which asks too). `src/access.ts` builds the rules; sessions won't start in your home folder. Both are settings (see Config).
|
|
69
69
|
|
|
70
70
|
- **Claude:** `claude -p --restricted --tools Read,Grep,Glob --strict-mcp-config`, plus `Read(...)` deny rules from the blocked list. No command, write or web tools; `--restricted` confines the file tools to the project; your MCP servers are left out.
|
|
71
|
-
- **Codex:** `codex exec --ignore-user-config` under a permission profile that can read the project and the system files tools need (`:minimal`), with the blocked list denied, no writes and no network. Apps, browser, computer use, plugins, hooks and web search are off; your model and reasoning effort carry over. Codex re-runs itself inside that sandbox, so
|
|
71
|
+
- **Codex:** `codex exec --ignore-user-config` under a permission profile that can read the project and the system files tools need (`:minimal`), with the blocked list denied, no writes and no network. Apps, browser, computer use, plugins, hooks and web search are off; your model and reasoning effort carry over. Codex re-runs itself inside that sandbox, so Parley starts it by its real path and lets it read that one file.
|
|
72
72
|
|
|
73
|
-
Before a session is made,
|
|
73
|
+
Before a session is made, Parley checks the agent is installed and new enough for these flags, and says what to update if not.
|
|
74
74
|
|
|
75
75
|
Each reply records which model answered, on which login (`auth: "none"` means your subscription, not an API key), and what it would cost at API prices.
|
|
76
76
|
|
package/dist/access.js
CHANGED
|
@@ -44,7 +44,7 @@ export function agentProblem(config, agent) {
|
|
|
44
44
|
try {
|
|
45
45
|
const help = execFileSync(bin, NEEDS[agent].args, { encoding: "utf8", timeout: 20_000, env: cleanEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
46
46
|
if (NEEDS[agent].flags.some((flag) => !help.includes(flag))) {
|
|
47
|
-
problem = `This ${NAMES[agent]} is too old for
|
|
47
|
+
problem = `This ${NAMES[agent]} is too old for Parley's sandbox. Update it (${UPDATE[agent]}) and try again.`;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
package/dist/account.js
CHANGED
|
@@ -34,7 +34,7 @@ async function call(creds, path, init = {}) {
|
|
|
34
34
|
if (res.status === 401)
|
|
35
35
|
throw Object.assign(new Error("This computer isn't linked to your parley account anymore. Run parley login."), { unlinked: true });
|
|
36
36
|
if (!res.ok)
|
|
37
|
-
throw new Error((await res.json().catch(() => null))?.message ?? `
|
|
37
|
+
throw new Error((await res.json().catch(() => null))?.message ?? `Parley's servers answered ${res.status}`);
|
|
38
38
|
return res.json();
|
|
39
39
|
}
|
|
40
40
|
// What parley.im is told about a session: its id and a title for link
|
package/dist/cli.js
CHANGED
|
@@ -12,8 +12,8 @@ import { readState, linkFor, dashboardUrl, isServerUp } from "./state.js";
|
|
|
12
12
|
import { transcript, summaryLine } from "./transcript.js";
|
|
13
13
|
import { startSession, endSession, deleteSession, setReplyLimit } from "./control.js";
|
|
14
14
|
import { login, readCredentials, clearCredentials, relay as registry } from "./account.js";
|
|
15
|
-
const HELP = `
|
|
16
|
-
computer, and
|
|
15
|
+
const HELP = `Parley: someone chats with a read-only Claude or Codex running in this project, on this
|
|
16
|
+
computer, and what they share comes back to this project. Good for feedback from guests, or any interview.
|
|
17
17
|
|
|
18
18
|
If you're an agent and the user asks for "a parley" (or to get someone's take on this project):
|
|
19
19
|
|
|
@@ -37,16 +37,16 @@ Commands
|
|
|
37
37
|
parley limit ID N Let a session give N replies in total
|
|
38
38
|
parley end ID Kill its link for good; the transcript stays
|
|
39
39
|
parley delete ID Erase it, screenshots included
|
|
40
|
-
parley login | logout | whoami Link this computer to your
|
|
41
|
-
parley start | stop | status The background server (holds the connection to
|
|
40
|
+
parley login | logout | whoami Link this computer to your Parley account
|
|
41
|
+
parley start | stop | status The background server (holds the connection to Parley)
|
|
42
42
|
parley panel Open the control panel (localhost)
|
|
43
|
-
parley install Add
|
|
43
|
+
parley install Add Parley's MCP tools to Claude Code and Codex; start at login
|
|
44
44
|
parley uninstall Undo install (your conversations stay)
|
|
45
45
|
parley serve | mcp Run the server in the foreground | MCP over stdio
|
|
46
46
|
parley --version
|
|
47
47
|
|
|
48
48
|
Conversations and screenshots live in each project's .parley folder (kept out of git), with a
|
|
49
|
-
readable transcript.md per session. Settings and your
|
|
49
|
+
readable transcript.md per session. Settings and your Parley login live in ${paths.home}.`;
|
|
50
50
|
const CLI_PATH = fileURLToPath(import.meta.url);
|
|
51
51
|
async function main() {
|
|
52
52
|
const [command, ...rest] = process.argv.slice(2);
|
|
@@ -253,14 +253,14 @@ async function install(config) {
|
|
|
253
253
|
if (config.claude.bin.startsWith("/")) {
|
|
254
254
|
run(config.claude.bin, ["mcp", "remove", "--scope", "user", "parley"]);
|
|
255
255
|
const ok = run(config.claude.bin, ["mcp", "add", "--scope", "user", "parley", "--", ...launch]);
|
|
256
|
-
console.log(ok ? "Claude Code: added the
|
|
256
|
+
console.log(ok ? "Claude Code: added the Parley MCP server (user scope)." : "Claude Code: couldn't add the MCP server.");
|
|
257
257
|
}
|
|
258
258
|
else
|
|
259
259
|
console.log("Claude Code: not found, skipped.");
|
|
260
260
|
if (config.codex.bin.startsWith("/")) {
|
|
261
261
|
run(config.codex.bin, ["mcp", "remove", "parley"]);
|
|
262
262
|
const ok = run(config.codex.bin, ["mcp", "add", "parley", "--", ...launch]);
|
|
263
|
-
console.log(ok ? "Codex: added the
|
|
263
|
+
console.log(ok ? "Codex: added the Parley MCP server." : "Codex: couldn't add the MCP server.");
|
|
264
264
|
}
|
|
265
265
|
else
|
|
266
266
|
console.log("Codex: not found, skipped.");
|
package/dist/daemon.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function ensureServer(config, waitMs = 20_000) {
|
|
|
15
15
|
const running = await serverHealth(config);
|
|
16
16
|
if (running?.version && running.version !== VERSION) {
|
|
17
17
|
if (compareVersions(running.version, VERSION) > 0) {
|
|
18
|
-
throw new Error(`
|
|
18
|
+
throw new Error(`Parley was updated to ${running.version}, but this process is still ${VERSION}. Restart this Claude Code or Codex session (or the parley command) to use the new version.`);
|
|
19
19
|
}
|
|
20
20
|
await stopServer(config);
|
|
21
21
|
await waitUntilDown(config);
|
package/dist/mcp.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function runMcp() {
|
|
|
20
20
|
const currentLink = (id) => linkFor(store.session(id), config);
|
|
21
21
|
server.registerTool("create_session", {
|
|
22
22
|
title: "Create a feedback session",
|
|
23
|
-
description: "Create a
|
|
23
|
+
description: "Create a Parley session: a private link the user can send to a guest. The guest chats with a fresh, read-only " +
|
|
24
24
|
"agent in the project folder that hears their feedback, answers questions by reading the code, and records everything. " +
|
|
25
25
|
"Starts the local server and tunnel if needed. Returns the link to give the user. Only for guests the user trusts, " +
|
|
26
26
|
"since the guest can ask about anything in the project folder.",
|
|
@@ -83,14 +83,14 @@ export async function runMcp() {
|
|
|
83
83
|
}
|
|
84
84
|
else {
|
|
85
85
|
lines.push("Everything after the # in the link is the session's encryption key: share the whole link, and only with the guest. " +
|
|
86
|
-
"Replies come from this computer, so it needs to be awake with
|
|
86
|
+
"Replies come from this computer, so it needs to be awake with Parley running.");
|
|
87
87
|
}
|
|
88
88
|
lines.push(`Tell the user to send it only to someone they trust: ${session.guest} can ask about anything in this project folder.`);
|
|
89
89
|
return reply(lines.join("\n"));
|
|
90
90
|
});
|
|
91
91
|
server.registerTool("list_sessions", {
|
|
92
92
|
title: "List feedback sessions",
|
|
93
|
-
description: "List
|
|
93
|
+
description: "List Parley sessions, newest activity first, with how many guest messages are new since you last read each one. " +
|
|
94
94
|
"Open sessions include their current guest link.",
|
|
95
95
|
inputSchema: {
|
|
96
96
|
status: z.enum(["open", "ended", "all"]).optional().describe("Defaults to open."),
|
|
@@ -111,7 +111,7 @@ export async function runMcp() {
|
|
|
111
111
|
});
|
|
112
112
|
server.registerTool("get_transcript", {
|
|
113
113
|
title: "Read a feedback session",
|
|
114
|
-
description: "Read a
|
|
114
|
+
description: "Read a Parley session as Markdown: the brief, every message, which model interviewed, and paths to screenshots the " +
|
|
115
115
|
"guest shared. Set only_new to read just what came in since you last read it. Reading marks it read.",
|
|
116
116
|
inputSchema: {
|
|
117
117
|
session_id: z.string(),
|
|
@@ -125,7 +125,7 @@ export async function runMcp() {
|
|
|
125
125
|
});
|
|
126
126
|
server.registerTool("end_session", {
|
|
127
127
|
title: "End a feedback session",
|
|
128
|
-
description: "End a
|
|
128
|
+
description: "End a Parley session for good when the user is done with it. The guest's link stops working entirely and an open " +
|
|
129
129
|
"page clears itself. The transcript stays readable here. This can't be undone; make a new session to talk again.",
|
|
130
130
|
inputSchema: { session_id: z.string() },
|
|
131
131
|
}, async (args) => {
|
package/dist/relay.js
CHANGED
|
@@ -108,7 +108,7 @@ export class RelayLink {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
if (event.code === 4000) {
|
|
111
|
-
this.status = { state: "replaced", relayUrl: creds.relayUrl, error: "Another
|
|
111
|
+
this.status = { state: "replaced", relayUrl: creds.relayUrl, error: "Another Parley server on this computer took over the connection." };
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
this.status = { state: "offline", relayUrl: creds.relayUrl, email: creds.account.email, error: `Reconnecting to ${creds.relayUrl}` };
|
package/dist/server.js
CHANGED
|
@@ -156,7 +156,7 @@ export async function serve() {
|
|
|
156
156
|
if (rest === "/" && req.method === "GET") {
|
|
157
157
|
const from = ownerName(config);
|
|
158
158
|
const html = readFileSync(join(WEB, "chat.html"), "utf8")
|
|
159
|
-
.replaceAll("{{TITLE}}", escapeHtml(`${session.project_name} ·
|
|
159
|
+
.replaceAll("{{TITLE}}", escapeHtml(`${session.project_name} · Parley`))
|
|
160
160
|
.replaceAll("{{PREVIEW_TITLE}}", escapeHtml(`${from ? `${from} wants` : "Someone wants"} your feedback on ${session.project_name}`))
|
|
161
161
|
.replaceAll("{{PREVIEW_TEXT}}", escapeHtml(`A private chat about ${session.project_name}.`))
|
|
162
162
|
.replace("{{CONFIG}}", JSON.stringify({ base: scope.base, owner: scope.owner, maxText: MAX_TEXT }).replaceAll("<", "\\u003c"));
|
|
@@ -245,7 +245,7 @@ export async function serve() {
|
|
|
245
245
|
// Already running (started at login, and again by hand, say): leave it be
|
|
246
246
|
// and exit cleanly, which also tells a login service not to retry.
|
|
247
247
|
if (await isServerUp(config)) {
|
|
248
|
-
console.log(`
|
|
248
|
+
console.log(`Parley: a server is already running on ${config.dashboardPort}; leaving it be.`);
|
|
249
249
|
process.exit(0);
|
|
250
250
|
}
|
|
251
251
|
guest.listen(config.port, "127.0.0.1");
|
|
@@ -255,7 +255,7 @@ export async function serve() {
|
|
|
255
255
|
server.once("error", reject);
|
|
256
256
|
})));
|
|
257
257
|
writeState(state);
|
|
258
|
-
console.log(`
|
|
258
|
+
console.log(`Parley: control panel http://localhost:${config.dashboardPort}, local links on ${config.port}`);
|
|
259
259
|
link.start();
|
|
260
260
|
runner.resume();
|
|
261
261
|
// Says goodbye to parley.im before exiting, so guests see this computer go
|
package/package.json
CHANGED
package/web/app.css
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
control panel and parley's site. Dark, with lilac for the accents, for the
|
|
3
3
|
people who use Claude Code and Codex: Instrument Sans for everything
|
|
4
4
|
people read, JetBrains Mono for the wordmark, labels, files and numbers.
|
|
5
|
-
The
|
|
6
|
-
|
|
5
|
+
The mark (mark.js) is equilateral triangles gathered into hexagons, and
|
|
6
|
+
its triangles do the motion: they say when a page opens, when the agent
|
|
7
|
+
is reading or writing, when a reply lands, and when the sender's computer
|
|
8
|
+
is offline. Six of them mark the agent's words. The fonts are served from here, never from a
|
|
7
9
|
font service, so guest pages only ever talk to parley. */
|
|
8
10
|
|
|
9
11
|
@font-face { font-family: "Instrument Sans"; src: url("/static/fonts/instrument-sans.woff2") format("woff2"); font-weight: 400 700; font-style: normal; font-display: swap; }
|
|
@@ -24,7 +26,6 @@
|
|
|
24
26
|
--accent-fill: #9d92ff; /* buttons, with dark text */
|
|
25
27
|
--accent-soft: #221f3d;
|
|
26
28
|
--accent-ink: #0c0c0d;
|
|
27
|
-
--tile: #9d92ff; /* the mark's tile */
|
|
28
29
|
--guest: #221f3d; /* your own messages */
|
|
29
30
|
--guest-ink: #e6e2ff;
|
|
30
31
|
--ok: #3ccf7a;
|
|
@@ -57,11 +58,27 @@ code, kbd, pre { font-family: var(--mono); }
|
|
|
57
58
|
|
|
58
59
|
/* ---------- shared pieces ---------- */
|
|
59
60
|
|
|
60
|
-
/* The mark
|
|
61
|
-
|
|
62
|
-
.mark
|
|
63
|
-
.mark
|
|
64
|
-
.
|
|
61
|
+
/* The mark: a ring of thirty triangles around a ring of space and a core of
|
|
62
|
+
six, two tones each by which way a triangle points. */
|
|
63
|
+
.mark { display: inline-block; width: 26px; height: 26px; flex: none; }
|
|
64
|
+
.mark svg { display: block; width: 100%; height: 100%; overflow: visible; }
|
|
65
|
+
.r1 { fill: #9d92ff; } .r2 { fill: #8a7ff0; }
|
|
66
|
+
.c1 { fill: #d2ccff; } .c2 { fill: #bdb4ff; }
|
|
67
|
+
.g1 { fill: #6b60d6; } .g2 { fill: #5e54c4; }
|
|
68
|
+
.mark path { transition: fill 0.4s ease; }
|
|
69
|
+
/* Offline, the ring goes dark and the core turns red; ended, it all dims. */
|
|
70
|
+
.app.offline .topbar .mark .r1, .mark.dim .r1, .mark.dim .c1 { fill: #2e2a45; }
|
|
71
|
+
.app.offline .topbar .mark .r2, .mark.dim .r2, .mark.dim .c2 { fill: #28253c; }
|
|
72
|
+
.app.offline .topbar .mark .c1 { fill: #ff6b63; }
|
|
73
|
+
.app.offline .topbar .mark .c2 { fill: #e85a53; }
|
|
74
|
+
/* As a page opens, the ring builds around and then the core arrives. */
|
|
75
|
+
.mark.opening .r1, .mark.opening .r2 { transform-box: fill-box; transform-origin: center; animation: build 0.45s ease-out both; }
|
|
76
|
+
.mark.opening .c1, .mark.opening .c2 { transform-box: view-box; transform-origin: 32px 32px; animation: arrive 0.5s ease-out 1.05s both; }
|
|
77
|
+
@keyframes build { from { opacity: 0; transform: scale(0.2); } }
|
|
78
|
+
@keyframes arrive { from { opacity: 0; transform: scale(0.3); } 70% { transform: scale(1.12); } }
|
|
79
|
+
/* A reply lands: the core pulses once. */
|
|
80
|
+
.mark.pulse .c1, .mark.pulse .c2 { transform-box: view-box; transform-origin: 32px 32px; animation: pulse-core 0.8s ease-in-out; }
|
|
81
|
+
@keyframes pulse-core { 40% { transform: scale(1.3); } 75% { transform: scale(0.94); } }
|
|
65
82
|
.wordmark { font-family: var(--mono); font-weight: 700; letter-spacing: -0.04em; }
|
|
66
83
|
|
|
67
84
|
.topbar {
|
|
@@ -166,8 +183,12 @@ body:has(> .app) { overflow: hidden; overscroll-behavior: none; }
|
|
|
166
183
|
position: absolute; left: 0; top: 0; width: 14px; text-align: center;
|
|
167
184
|
font-family: var(--mono); font-size: 14px; line-height: 1.55;
|
|
168
185
|
}
|
|
169
|
-
/* The agent's words
|
|
170
|
-
.msg.agent::before {
|
|
186
|
+
/* The agent's words are marked with the mark's core: six triangles. */
|
|
187
|
+
.msg.agent::before {
|
|
188
|
+
content: ""; left: 2px; top: 6px; width: 11px; height: 12.7px;
|
|
189
|
+
clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
|
|
190
|
+
background: conic-gradient(#d2ccff 0deg 60deg, #bdb4ff 60deg 120deg, #d2ccff 120deg 180deg, #bdb4ff 180deg 240deg, #d2ccff 240deg 300deg, #bdb4ff 300deg 360deg);
|
|
191
|
+
}
|
|
171
192
|
/* The interviewer's words are what people read: set for reading. */
|
|
172
193
|
.msg.agent .body { font-size: 16.5px; line-height: 1.6; }
|
|
173
194
|
/* Your own words are compact and plainly yours, on the right. */
|
|
@@ -222,15 +243,21 @@ body:has(> .app) { overflow: hidden; overscroll-behavior: none; }
|
|
|
222
243
|
.body th { background: var(--subtle); font-weight: 600; white-space: nowrap; }
|
|
223
244
|
.body tr:last-child td { border-bottom: 0; }
|
|
224
245
|
|
|
225
|
-
/*
|
|
226
|
-
|
|
227
|
-
.thinking
|
|
228
|
-
.thinking
|
|
229
|
-
.thinking
|
|
230
|
-
|
|
246
|
+
/* Working: the mark, small. Light runs around its empty ring while the agent
|
|
247
|
+
writes; while it looks things up (.reading), its core flickers instead. */
|
|
248
|
+
.thinking { display: inline-block; width: 24px; height: 24px; margin-top: 2px; vertical-align: top; }
|
|
249
|
+
.thinking svg { display: block; width: 100%; height: 100%; }
|
|
250
|
+
.thinking .g1, .thinking .g2 { opacity: 0; animation: chase 1.8s linear infinite; }
|
|
251
|
+
.thinking.reading .g1, .thinking.reading .g2 { animation: none; }
|
|
252
|
+
.thinking.reading .c1, .thinking.reading .c2 { animation: scan 1.2s ease-in-out infinite; }
|
|
253
|
+
@keyframes chase { 0% { opacity: 0; } 4% { opacity: 1; } 22% { opacity: 0; } 100% { opacity: 0; } }
|
|
254
|
+
@keyframes scan { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
231
255
|
@keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
|
|
232
256
|
.cursor::after { content: ""; display: inline-block; width: 7px; height: 1.05em; background: var(--accent); border-radius: 1px; margin-left: 3px; vertical-align: -3px; animation: blink 1.06s steps(1) infinite; }
|
|
233
|
-
@media (prefers-reduced-motion: reduce) {
|
|
257
|
+
@media (prefers-reduced-motion: reduce) {
|
|
258
|
+
.cursor::after, .dot.busy, .thinking .c1, .thinking .c2, .mark.opening path, .mark.pulse path { animation: none; }
|
|
259
|
+
.thinking .g1, .thinking .g2 { animation: none; opacity: 0.6; }
|
|
260
|
+
}
|
|
234
261
|
|
|
235
262
|
.dock { border-top: 1px solid var(--line); background: var(--bg); }
|
|
236
263
|
.dock .chat-inner { padding: 8px 16px calc(8px + env(safe-area-inset-bottom)); }
|
|
@@ -387,7 +414,7 @@ body.panel { overflow: auto; }
|
|
|
387
414
|
|
|
388
415
|
.lost { min-height: 100dvh; display: grid; place-items: center; padding: 16px; text-align: center; }
|
|
389
416
|
.lost.inline { min-height: 100%; }
|
|
390
|
-
.lost .mark { width:
|
|
417
|
+
.lost .mark { width: 52px; height: 52px; margin: 0 auto 16px; }
|
|
391
418
|
.lost h1 { font-size: 20px; margin: 0 0 6px; letter-spacing: -0.01em; }
|
|
392
419
|
.lost p { color: var(--muted); margin: 0 auto; max-width: 34ch; }
|
|
393
420
|
|
|
@@ -429,14 +456,63 @@ body.panel { overflow: auto; }
|
|
|
429
456
|
/* A session's status on the dashboard (not the chat's reply in progress,
|
|
430
457
|
which is li.live). */
|
|
431
458
|
span.live { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--muted); white-space: nowrap; }
|
|
432
|
-
|
|
433
|
-
.docs
|
|
434
|
-
.docs
|
|
435
|
-
.docs
|
|
436
|
-
.docs
|
|
437
|
-
.docs
|
|
438
|
-
.docs
|
|
439
|
-
.docs
|
|
459
|
+
/* The docs: a contents column beside the article. */
|
|
460
|
+
.docs-page { max-width: 1120px; }
|
|
461
|
+
.docs-layout { display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 56px; padding: 32px 0 80px; }
|
|
462
|
+
.docs-nav { position: sticky; top: 16px; align-self: start; max-height: calc(100vh - 32px); overflow-y: auto; display: grid; gap: 1px; font-size: 14px; }
|
|
463
|
+
.docs-nav .label { margin: 18px 0 6px 10px; }
|
|
464
|
+
.docs-nav .label:first-child { margin-top: 4px; }
|
|
465
|
+
.docs-nav a { color: var(--muted); text-decoration: none; padding: 5px 10px; border-radius: 7px; }
|
|
466
|
+
.docs-nav a:hover { color: var(--text); background: var(--subtle); }
|
|
467
|
+
.docs { min-width: 0; max-width: 760px; font-size: 15.5px; line-height: 1.65; }
|
|
468
|
+
.docs-head { padding-bottom: 8px; }
|
|
469
|
+
.docs-head .label { margin: 0 0 8px; }
|
|
470
|
+
.docs h1 { font-size: 38px; line-height: 1.1; letter-spacing: -0.03em; margin: 0 0 14px; }
|
|
471
|
+
.docs .lede { font-size: 18px; line-height: 1.6; color: var(--muted); margin: 0; }
|
|
472
|
+
.docs h2 { font-size: 23px; letter-spacing: -0.02em; margin: 52px 0 14px; padding-top: 28px; border-top: 1px solid var(--line); scroll-margin-top: 12px; }
|
|
473
|
+
.docs h3 { font-size: 16px; letter-spacing: -0.01em; margin: 26px 0 8px; }
|
|
474
|
+
.docs p { margin: 0 0 14px; }
|
|
475
|
+
.docs ul { padding-left: 1.2em; margin: 0 0 16px; }
|
|
476
|
+
.docs li { margin: 7px 0; }
|
|
477
|
+
.docs li::marker { color: var(--faint); }
|
|
478
|
+
.docs a { color: var(--accent); }
|
|
479
|
+
.docs code { font-family: var(--mono); font-size: 0.85em; background: var(--subtle); border: 1px solid var(--line); padding: 0.08em 0.35em; border-radius: 5px; white-space: nowrap; }
|
|
480
|
+
.docs pre { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; overflow-x: auto; font-size: 13.5px; line-height: 1.65; margin: 0 0 16px; }
|
|
481
|
+
.docs pre code { background: none; border: 0; padding: 0; font-size: inherit; white-space: pre; }
|
|
482
|
+
.docs blockquote { margin: 0 0 12px; padding: 12px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; color: var(--text); }
|
|
483
|
+
.docs blockquote::before { content: "you"; display: block; font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em; color: var(--faint); margin-bottom: 2px; }
|
|
484
|
+
.docs table { width: 100%; border-collapse: collapse; font-size: 14.5px; line-height: 1.5; margin: 0 0 18px; }
|
|
485
|
+
.docs th { text-align: left; font-weight: 600; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); padding: 8px 12px; border-bottom: 1px solid var(--line-strong); }
|
|
486
|
+
.docs td { padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
487
|
+
.docs td:first-child { white-space: nowrap; }
|
|
488
|
+
.docs .table-wrap { overflow-x: auto; margin: 0 0 18px; }
|
|
489
|
+
.docs .table-wrap table { margin: 0; }
|
|
490
|
+
.docs .callout { margin: 18px 0; padding: 14px 16px; background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: 10px; }
|
|
491
|
+
.docs .callout.warn { border-left-color: var(--warn); }
|
|
492
|
+
.docs .flow { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px; margin: 22px 0; position: relative; }
|
|
493
|
+
.docs .flow div { display: grid; gap: 4px; align-content: start; padding: 14px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; position: relative; }
|
|
494
|
+
.docs .flow div + div::before { content: "⇄"; position: absolute; left: -22px; top: 50%; transform: translateY(-50%); color: var(--accent); font-size: 16px; }
|
|
495
|
+
.docs .flow strong { font-size: 15px; }
|
|
496
|
+
.docs .flow span { font-size: 13.5px; line-height: 1.5; color: var(--muted); }
|
|
497
|
+
.docs .howto { list-style: none; counter-reset: step; padding: 0; margin: 22px 0; display: grid; gap: 18px; }
|
|
498
|
+
.docs .howto li { counter-increment: step; position: relative; padding-left: 44px; margin: 0; }
|
|
499
|
+
.docs .howto li::before { content: counter(step); position: absolute; left: 0; top: 0; width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--accent-ink); background: var(--accent-fill); }
|
|
500
|
+
.docs .howto h3 { margin: 2px 0 8px; }
|
|
501
|
+
.docs .howto p, .docs .howto pre { margin: 0 0 8px; }
|
|
502
|
+
.docs details.more { margin: 0 0 16px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); padding: 10px 14px; }
|
|
503
|
+
.docs details.more summary { cursor: pointer; font-weight: 600; font-size: 14.5px; }
|
|
504
|
+
.docs details.more[open] summary { margin-bottom: 10px; }
|
|
505
|
+
.docs details.more pre { background: var(--bg); }
|
|
506
|
+
.docs .faq dt { font-weight: 600; margin-top: 18px; }
|
|
507
|
+
.docs .faq dd { margin: 4px 0 0; color: var(--muted); }
|
|
508
|
+
@media (max-width: 860px) {
|
|
509
|
+
.docs-layout { grid-template-columns: 1fr; gap: 24px; padding-top: 20px; }
|
|
510
|
+
.docs-nav { position: static; max-height: none; grid-template-columns: 1fr 1fr; padding-bottom: 18px; border-bottom: 1px solid var(--line); }
|
|
511
|
+
.docs-nav .label { display: none; }
|
|
512
|
+
.docs h1 { font-size: 30px; }
|
|
513
|
+
.docs .flow { grid-template-columns: 1fr; gap: 26px; }
|
|
514
|
+
.docs .flow div + div::before { left: 50%; top: -21px; transform: translateX(-50%) rotate(90deg); }
|
|
515
|
+
}
|
|
440
516
|
.trow.site-row { grid-template-columns: 1fr auto; }
|
|
441
517
|
/* Long details shorten instead of pushing the buttons off a phone screen. */
|
|
442
518
|
.trow.site-row .who-cell { min-width: 0; }
|
|
@@ -457,23 +533,37 @@ span.live { display: inline-flex; align-items: center; gap: 7px; font-size: 13px
|
|
|
457
533
|
.demo-tabs button { font-family: var(--mono); font-size: 12px; padding: 6px 10px; border: 1px solid transparent; border-radius: 8px; background: none; color: var(--muted); cursor: pointer; }
|
|
458
534
|
.demo-tabs button:hover { color: var(--text); }
|
|
459
535
|
.demo-tabs button.on { background: var(--panel); border-color: var(--line); color: var(--text); }
|
|
460
|
-
|
|
536
|
+
/* A fixed frame, like the real chat: the conversation scrolls inside it. */
|
|
537
|
+
.demo-chat { height: 500px; display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--line); border-radius: 18px; box-shadow: var(--shadow); overflow: hidden; }
|
|
461
538
|
.demo-top { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--line); font-size: 14px; }
|
|
462
539
|
.demo-top .mark { width: 20px; height: 20px; }
|
|
463
540
|
.demo-top .product { font-family: var(--mono); font-weight: 700; letter-spacing: -0.04em; }
|
|
464
541
|
.demo-top .slash { color: var(--faint); }
|
|
465
542
|
.demo-top .here { font-weight: 600; flex: 1; }
|
|
466
|
-
.demo-msgs { list-style: none; margin: 0; padding: 18px 18px
|
|
543
|
+
.demo-msgs { flex: 1; min-height: 0; overflow: hidden; list-style: none; margin: 0; padding: 18px 18px 14px; display: flex; flex-direction: column; gap: 14px; }
|
|
467
544
|
.demo .msg { margin: 0; animation: rise 0.25s ease-out; }
|
|
468
545
|
.demo .msg.agent .body { font-size: 15.5px; }
|
|
469
|
-
.demo .msg.agent::before { top:
|
|
546
|
+
.demo .msg.agent::before { top: 5px; }
|
|
470
547
|
.demo .msg.guest .bubble { font-size: 14.5px; max-width: 86%; }
|
|
471
548
|
.demo .steps { margin-top: 2px; }
|
|
472
|
-
|
|
473
|
-
|
|
549
|
+
/* The demo's composer is the real one, played: text types in, the send
|
|
550
|
+
button wakes, and the message goes. */
|
|
551
|
+
.demo-dock { padding: 8px 12px 12px; border-top: 1px solid var(--line); background: var(--bg); }
|
|
552
|
+
.demo .composer { box-shadow: none; transition: border-color 0.15s, box-shadow 0.15s; }
|
|
553
|
+
.demo .composer.focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
554
|
+
.demo-input { flex: 1; min-width: 0; min-height: 40px; padding: 8px 4px; font-size: 15px; line-height: 1.55; color: var(--text); }
|
|
555
|
+
.demo-placeholder { color: var(--faint); }
|
|
556
|
+
.demo .composer.focus .demo-input::after { content: ""; display: inline-block; width: 1.5px; height: 1.1em; margin-left: 1px; vertical-align: -3px; background: var(--text); animation: blink 1.06s steps(1) infinite; }
|
|
557
|
+
.demo .icon-btn { cursor: default; }
|
|
558
|
+
.demo .icon-btn:hover { background: transparent; color: var(--muted); }
|
|
559
|
+
.demo .send, .demo .send:hover { background: var(--accent-fill); color: var(--accent-ink); transition: transform 0.12s, background 0.15s; }
|
|
560
|
+
.demo .send.off { background: var(--subtle); color: var(--faint); }
|
|
561
|
+
.demo .send.press { transform: scale(0.9); }
|
|
562
|
+
.demo-top .chip { margin-left: 4px; }
|
|
563
|
+
.demo-top .icon-btn { width: 32px; height: 32px; margin-right: -6px; }
|
|
474
564
|
@keyframes rise { from { opacity: 0; transform: translateY(4px); } }
|
|
475
565
|
@media (prefers-reduced-motion: reduce) { .demo .msg { animation: none; } }
|
|
476
|
-
@media (max-width: 560px) { .demo-
|
|
566
|
+
@media (max-width: 560px) { .demo-chat { height: 560px; } .demo-msgs { padding: 14px 12px 12px; } }
|
|
477
567
|
.how { list-style: none; counter-reset: step; padding: 0; margin: 40px 0 0; display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
|
|
478
568
|
.how li { counter-increment: step; border: 1px solid var(--line); border-radius: 16px; padding: 22px; background: var(--panel); display: grid; gap: 10px; align-content: start; }
|
|
479
569
|
.how li::before { content: "0" counter(step); font-family: var(--mono); font-weight: 700; color: var(--accent); font-size: 13px; }
|
package/web/chat.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<meta property="og:description" content="{{PREVIEW_TEXT}}">
|
|
11
11
|
<meta property="og:site_name" content="parley">
|
|
12
12
|
<title>{{TITLE}}</title>
|
|
13
|
-
<link rel="icon" href="/static/icon.svg?v=
|
|
13
|
+
<link rel="icon" href="/static/icon.svg?v=6" type="image/svg+xml">
|
|
14
14
|
<link rel="stylesheet" href="/static/app.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<header class="topbar">
|
|
19
19
|
<nav class="crumbs" id="crumbs">
|
|
20
20
|
<!-- What parley is, in a new tab so the conversation stays put. -->
|
|
21
|
-
<a class="brand" id="home-link" href="https://parley.im" target="_blank" rel="noopener" title="
|
|
22
|
-
<span class="mark" aria-hidden="true"><svg viewBox="0 0 64 64"><
|
|
21
|
+
<a class="brand" id="home-link" href="https://parley.im" target="_blank" rel="noopener" title="Parley: let people talk to your agent">
|
|
22
|
+
<span class="mark" aria-hidden="true"><svg viewBox="0 0 64 64" aria-hidden="true"><path class="r1" d="M32 7L39.2 11.2L32 15.3Z"/><path class="r2" d="M39.2 11.2L39.2 19.5L32 15.3Z"/><path class="r1" d="M39.2 11.2L46.4 15.3L39.2 19.5Z"/><path class="r2" d="M46.4 15.3L46.4 23.7L39.2 19.5Z"/><path class="r1" d="M46.4 15.3L53.7 19.5L46.4 23.7Z"/><path class="r2" d="M53.7 19.5L53.7 27.8L46.4 23.7Z"/><path class="r1" d="M46.4 23.7L53.7 27.8L46.4 32Z"/><path class="r2" d="M53.7 27.8L53.7 36.2L46.4 32Z"/><path class="r1" d="M46.4 32L53.7 36.2L46.4 40.3Z"/><path class="r2" d="M53.7 36.2L53.7 44.5L46.4 40.3Z"/><path class="r1" d="M46.4 40.3L53.7 44.5L46.4 48.7Z"/><path class="r2" d="M46.4 40.3L46.4 48.7L39.2 44.5Z"/><path class="r1" d="M39.2 44.5L46.4 48.7L39.2 52.8Z"/><path class="r2" d="M39.2 44.5L39.2 52.8L32 48.7Z"/><path class="r1" d="M32 48.7L39.2 52.8L32 57Z"/><path class="r2" d="M32 48.7L32 57L24.8 52.8Z"/><path class="r1" d="M24.8 44.5L32 48.7L24.8 52.8Z"/><path class="r2" d="M24.8 44.5L24.8 52.8L17.6 48.7Z"/><path class="r1" d="M17.6 40.3L24.8 44.5L17.6 48.7Z"/><path class="r2" d="M17.6 40.3L17.6 48.7L10.3 44.5Z"/><path class="r1" d="M10.3 36.2L17.6 40.3L10.3 44.5Z"/><path class="r2" d="M17.6 32L17.6 40.3L10.3 36.2Z"/><path class="r1" d="M10.3 27.8L17.6 32L10.3 36.2Z"/><path class="r2" d="M17.6 23.7L17.6 32L10.3 27.8Z"/><path class="r1" d="M10.3 19.5L17.6 23.7L10.3 27.8Z"/><path class="r2" d="M17.6 15.3L17.6 23.7L10.3 19.5Z"/><path class="r1" d="M17.6 15.3L24.8 19.5L17.6 23.7Z"/><path class="r2" d="M24.8 11.2L24.8 19.5L17.6 15.3Z"/><path class="r1" d="M24.8 11.2L32 15.3L24.8 19.5Z"/><path class="r2" d="M32 7L32 15.3L24.8 11.2Z"/><path class="c1" d="M32 23.7L39.2 27.8L32 32Z"/><path class="c2" d="M39.2 27.8L39.2 36.2L32 32Z"/><path class="c1" d="M32 32L39.2 36.2L32 40.3Z"/><path class="c2" d="M32 32L32 40.3L24.8 36.2Z"/><path class="c1" d="M24.8 27.8L32 32L24.8 36.2Z"/><path class="c2" d="M32 23.7L32 32L24.8 27.8Z"/></svg></span><span class="product">parley</span>
|
|
23
23
|
</a><span class="slash">/</span><span class="here" id="project"> </span>
|
|
24
24
|
</nav>
|
|
25
25
|
<span class="status" id="status" hidden><span class="dot" id="status-dot"></span><span id="status-text"></span></span>
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<div class="attachments" id="attachments" hidden></div>
|
|
49
49
|
<div class="row">
|
|
50
50
|
<span class="prompt" aria-hidden="true">›</span>
|
|
51
|
-
<textarea id="input" rows="1" placeholder="
|
|
51
|
+
<textarea id="input" rows="1" placeholder="Write a message" autocomplete="off" autocorrect="on" spellcheck="true" aria-label="Message"></textarea>
|
|
52
52
|
<label class="icon-btn" title="Add a screenshot">
|
|
53
53
|
<input type="file" id="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple hidden>
|
|
54
54
|
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="5" width="17" height="14" rx="2.5"/><path d="m4 16 4.5-4.5a1.5 1.5 0 0 1 2.1 0L15 16"/><path d="m13.5 14.5 1.7-1.7a1.5 1.5 0 0 1 2.1 0l2.7 2.7"/><circle cx="15.5" cy="9.5" r="1.2"/></svg>
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
<script>window.PARLEY = {{CONFIG}};</script>
|
|
85
85
|
<script src="/static/markdown.js"></script>
|
|
86
86
|
<script src="/static/seal.js"></script>
|
|
87
|
+
<script src="/static/mark.js"></script>
|
|
87
88
|
<script src="/static/chat.js"></script>
|
|
88
89
|
</body>
|
|
89
90
|
</html>
|
package/web/chat.js
CHANGED
|
@@ -222,7 +222,16 @@
|
|
|
222
222
|
if (live.text.trim()) {
|
|
223
223
|
live.bodyEl.innerHTML = window.renderMarkdown(live.text);
|
|
224
224
|
live.bodyEl.lastElementChild?.classList.add("cursor");
|
|
225
|
-
} else
|
|
225
|
+
} else {
|
|
226
|
+
// Working: the mark's core flickers while it looks things up, and light
|
|
227
|
+
// runs around it while it writes.
|
|
228
|
+
let working = live.bodyEl.querySelector(".thinking");
|
|
229
|
+
if (!working) {
|
|
230
|
+
live.bodyEl.innerHTML = `<span class="thinking" role="img" aria-label="Working">${window.parleyMark({ moving: true })}</span>`;
|
|
231
|
+
working = live.bodyEl.querySelector(".thinking");
|
|
232
|
+
}
|
|
233
|
+
working.classList.toggle("reading", live.steps.length > 0 && !live.text);
|
|
234
|
+
}
|
|
226
235
|
// The last few things it looked at, newest highlighted.
|
|
227
236
|
live.stepsEl.textContent = "";
|
|
228
237
|
live.steps.slice(-4).forEach((line, i, shown) => live.stepsEl.append(el("li", { className: i === shown.length - 1 ? "now" : "", textContent: line })));
|
|
@@ -294,7 +303,7 @@
|
|
|
294
303
|
if (seen.size === before) break;
|
|
295
304
|
}
|
|
296
305
|
const projectName = session?.project ?? knownProject ?? "a project";
|
|
297
|
-
const lines = [`parley about ${projectName}, ${new Date().toLocaleDateString()}`, ""];
|
|
306
|
+
const lines = [`A parley about ${projectName}, ${new Date().toLocaleDateString()}`, ""];
|
|
298
307
|
for (const m of [...kept.values()].sort((a, b) => a.id - b.id)) {
|
|
299
308
|
if (m.role === "error") continue;
|
|
300
309
|
lines.push(`${m.role === "guest" ? "You" : agentName()} · ${new Date(m.createdAt).toLocaleString()}`, m.body.trim(), "");
|
|
@@ -345,7 +354,7 @@
|
|
|
345
354
|
end.onclick = () => endConversation(end);
|
|
346
355
|
const open = !ended && session?.status !== "ended";
|
|
347
356
|
els.sheetBody.replaceChildren(
|
|
348
|
-
el("p", {}, strong(who), " set this up to hear what you think of ", strong(projectName), sender ? `.
|
|
357
|
+
el("p", {}, strong(who), " set this up to hear what you think of ", strong(projectName), sender ? `. Parley checked it's really them, with Google.` : "."),
|
|
349
358
|
kv(facts),
|
|
350
359
|
el("div", { className: "sheet-actions" }, save, open ? end : null),
|
|
351
360
|
open ? el("p", { className: "fine" }, `Ending it stops this link working, for you and anyone else who has it. ${ownerName || "They"} keep${ownerName ? "s" : ""} everything said so far.`) : null,
|
|
@@ -358,7 +367,7 @@
|
|
|
358
367
|
? el(
|
|
359
368
|
"p",
|
|
360
369
|
{},
|
|
361
|
-
`Messages and screenshots are end-to-end encrypted between this page and ${theirs} computer.
|
|
370
|
+
`Messages and screenshots are end-to-end encrypted between this page and ${theirs} computer. Parley passes them along but can't read them; it knows who sent this link, the project's name and when messages go by. The key is the part of your link after the #, so anyone with the whole link can read this chat: keep it to yourself. (This page's code comes from Parley's servers, so that promise relies on Parley serving it honestly. And ${agentName()} reads the conversation to reply, as with any ${agentName()} chat.)`,
|
|
362
371
|
)
|
|
363
372
|
: null,
|
|
364
373
|
el("p", {}, `When the replies run out, the chat pauses until ${whom} adds more. Replies come from ${theirs} computer, so it needs to be on, and the link stops working for good once the chat ends.`),
|
|
@@ -421,7 +430,7 @@
|
|
|
421
430
|
function showEnded() {
|
|
422
431
|
// For the guest, one quiet line about having their own.
|
|
423
432
|
const own = () =>
|
|
424
|
-
owner ? null : el("p", { className: "own" }, "Want people to talk to your agent? ", el("a", { href: homeUrl, target: "_blank", rel: "noopener", textContent: "Get
|
|
433
|
+
owner ? null : el("p", { className: "own" }, "Want people to talk to your agent? ", el("a", { href: homeUrl, target: "_blank", rel: "noopener", textContent: "Get Parley" }));
|
|
425
434
|
showNote("This parley has ended", "Thanks for the feedback. The conversation isn't available here anymore.", own);
|
|
426
435
|
}
|
|
427
436
|
|
|
@@ -438,12 +447,26 @@
|
|
|
438
447
|
els.layout.append(el("div", { className: "lost inline", style: "flex:1" }, el("div", {}, markEl(), el("h1", { textContent: title }), el("p", { textContent: text }), extra())));
|
|
439
448
|
}
|
|
440
449
|
|
|
450
|
+
// An ended or broken link shows the mark dimmed.
|
|
441
451
|
function markEl() {
|
|
442
|
-
const span = el("span", { className: "mark" });
|
|
443
|
-
span.innerHTML =
|
|
452
|
+
const span = el("span", { className: "mark dim" });
|
|
453
|
+
span.innerHTML = window.parleyMark();
|
|
444
454
|
return span;
|
|
445
455
|
}
|
|
446
456
|
|
|
457
|
+
// The header's mark: it builds itself as the page opens, and its core
|
|
458
|
+
// pulses once when a reply lands.
|
|
459
|
+
const headMark = document.querySelector(".topbar .mark");
|
|
460
|
+
function markOnce(name, ms) {
|
|
461
|
+
if (!headMark || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
462
|
+
headMark.classList.remove(name);
|
|
463
|
+
void headMark.offsetWidth;
|
|
464
|
+
headMark.classList.add(name);
|
|
465
|
+
setTimeout(() => headMark.classList.remove(name), ms);
|
|
466
|
+
}
|
|
467
|
+
headMark?.querySelectorAll(".r1, .r2").forEach((path, i) => (path.style.animationDelay = `${i * 30}ms`));
|
|
468
|
+
markOnce("opening", 1800);
|
|
469
|
+
|
|
447
470
|
function banner(text) {
|
|
448
471
|
els.banner.hidden = !text;
|
|
449
472
|
els.banner.textContent = text || "";
|
|
@@ -474,7 +497,7 @@
|
|
|
474
497
|
el("span", { className: "slash", textContent: "/" }),
|
|
475
498
|
el("span", { className: "here", textContent: `${s.guest} on ${s.project}` }),
|
|
476
499
|
);
|
|
477
|
-
document.title = `${s.guest} on ${s.project} ·
|
|
500
|
+
document.title = `${s.guest} on ${s.project} · Parley`;
|
|
478
501
|
const used = s.replyLimit - s.repliesLeft;
|
|
479
502
|
const pct = Math.min(100, Math.round((used / s.replyLimit) * 100));
|
|
480
503
|
const kv = (pairs) => el("dl", { className: "kv" }, ...pairs.flatMap(([k, v]) => [el("dt", { textContent: k }), el("dd", { textContent: v })]));
|
|
@@ -902,7 +925,10 @@
|
|
|
902
925
|
// A live update: a new message, the reply streaming in, what the agent is
|
|
903
926
|
// looking at (your view only), replies left, or the end.
|
|
904
927
|
function apply(event) {
|
|
905
|
-
if (event.type === "message")
|
|
928
|
+
if (event.type === "message") {
|
|
929
|
+
if (event.message.role === "agent" && !seen.has(event.message.id)) markOnce("pulse", 900);
|
|
930
|
+
addMessage(event.message);
|
|
931
|
+
}
|
|
906
932
|
else if (event.type === "turn") event.live ? showLive(event.live) : clearLive();
|
|
907
933
|
else if (event.type === "delta") {
|
|
908
934
|
if (!live) showLive({ text: "", activity: null });
|
package/web/dashboard.js
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
const row = document.querySelector(`[data-computer="${computer}"]`);
|
|
53
53
|
if (row) {
|
|
54
54
|
row.querySelector(".dot").className = `dot${m.on ? " on" : ""}`;
|
|
55
|
-
row.querySelector(".computer-status").textContent = m.on ? `online now${m.version ? ` ·
|
|
55
|
+
row.querySelector(".computer-status").textContent = m.on ? `online now${m.version ? ` · Parley ${m.version}` : ""}` : "offline";
|
|
56
56
|
}
|
|
57
57
|
paint();
|
|
58
58
|
}
|
package/web/ended.html
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
<meta name="theme-color" content="#0c0c0d">
|
|
7
7
|
<meta name="robots" content="noindex, nofollow">
|
|
8
8
|
<title>This parley has ended</title>
|
|
9
|
-
<link rel="icon" href="/static/icon.svg?v=
|
|
9
|
+
<link rel="icon" href="/static/icon.svg?v=6" type="image/svg+xml">
|
|
10
10
|
<link rel="stylesheet" href="/static/app.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div class="lost">
|
|
14
14
|
<div>
|
|
15
|
-
<span class="mark" aria-hidden="true"><svg viewBox="0 0 64 64"><
|
|
15
|
+
<span class="mark dim" aria-hidden="true"><svg viewBox="0 0 64 64" aria-hidden="true"><path class="r1" d="M32 7L39.2 11.2L32 15.3Z"/><path class="r2" d="M39.2 11.2L39.2 19.5L32 15.3Z"/><path class="r1" d="M39.2 11.2L46.4 15.3L39.2 19.5Z"/><path class="r2" d="M46.4 15.3L46.4 23.7L39.2 19.5Z"/><path class="r1" d="M46.4 15.3L53.7 19.5L46.4 23.7Z"/><path class="r2" d="M53.7 19.5L53.7 27.8L46.4 23.7Z"/><path class="r1" d="M46.4 23.7L53.7 27.8L46.4 32Z"/><path class="r2" d="M53.7 27.8L53.7 36.2L46.4 32Z"/><path class="r1" d="M46.4 32L53.7 36.2L46.4 40.3Z"/><path class="r2" d="M53.7 36.2L53.7 44.5L46.4 40.3Z"/><path class="r1" d="M46.4 40.3L53.7 44.5L46.4 48.7Z"/><path class="r2" d="M46.4 40.3L46.4 48.7L39.2 44.5Z"/><path class="r1" d="M39.2 44.5L46.4 48.7L39.2 52.8Z"/><path class="r2" d="M39.2 44.5L39.2 52.8L32 48.7Z"/><path class="r1" d="M32 48.7L39.2 52.8L32 57Z"/><path class="r2" d="M32 48.7L32 57L24.8 52.8Z"/><path class="r1" d="M24.8 44.5L32 48.7L24.8 52.8Z"/><path class="r2" d="M24.8 44.5L24.8 52.8L17.6 48.7Z"/><path class="r1" d="M17.6 40.3L24.8 44.5L17.6 48.7Z"/><path class="r2" d="M17.6 40.3L17.6 48.7L10.3 44.5Z"/><path class="r1" d="M10.3 36.2L17.6 40.3L10.3 44.5Z"/><path class="r2" d="M17.6 32L17.6 40.3L10.3 36.2Z"/><path class="r1" d="M10.3 27.8L17.6 32L10.3 36.2Z"/><path class="r2" d="M17.6 23.7L17.6 32L10.3 27.8Z"/><path class="r1" d="M10.3 19.5L17.6 23.7L10.3 27.8Z"/><path class="r2" d="M17.6 15.3L17.6 23.7L10.3 19.5Z"/><path class="r1" d="M17.6 15.3L24.8 19.5L17.6 23.7Z"/><path class="r2" d="M24.8 11.2L24.8 19.5L17.6 15.3Z"/><path class="r1" d="M24.8 11.2L32 15.3L24.8 19.5Z"/><path class="r2" d="M32 7L32 15.3L24.8 11.2Z"/><path class="c1" d="M32 23.7L39.2 27.8L32 32Z"/><path class="c2" d="M39.2 27.8L39.2 36.2L32 32Z"/><path class="c1" d="M32 32L39.2 36.2L32 40.3Z"/><path class="c2" d="M32 32L32 40.3L24.8 36.2Z"/><path class="c1" d="M24.8 27.8L32 32L24.8 36.2Z"/><path class="c2" d="M32 23.7L32 32L24.8 27.8Z"/></svg></span>
|
|
16
16
|
<h1>This parley has ended</h1>
|
|
17
17
|
<p>Thanks for the feedback. The conversation isn't available here anymore.</p>
|
|
18
18
|
</div>
|
package/web/home.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
// a guest sees one. The
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Parley's front page: plays the demo, three short parleys in turn, the way
|
|
2
|
+
// a guest sees one. The header, the composer and the working mark (mark.js)
|
|
3
|
+
// are the real chat's. The first scene is already in the page, so without
|
|
4
|
+
// script (or for anyone who prefers less motion) it just sits there, and the
|
|
5
|
+
// tabs swap scenes without playing them. Picking a scene plays it, then
|
|
6
|
+
// stays on it for a while before the others come round again.
|
|
5
7
|
(() => {
|
|
6
8
|
const root = document.getElementById("demo");
|
|
7
9
|
const list = document.getElementById("demo-msgs");
|
|
@@ -9,7 +11,12 @@
|
|
|
9
11
|
if (!root || !list || !data) return;
|
|
10
12
|
const scenes = JSON.parse(data.textContent);
|
|
11
13
|
const tabs = [...root.querySelectorAll("[data-scene]")];
|
|
14
|
+
const composer = document.getElementById("demo-composer");
|
|
15
|
+
const input = document.getElementById("demo-input");
|
|
16
|
+
const send = document.getElementById("demo-send");
|
|
17
|
+
const chip = document.getElementById("demo-chip");
|
|
12
18
|
const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
19
|
+
const STAY = 30_000; // after someone picks a scene
|
|
13
20
|
let run = 0; // a new run cancels the one playing
|
|
14
21
|
|
|
15
22
|
const el = (tag, className, text) => {
|
|
@@ -32,6 +39,43 @@
|
|
|
32
39
|
};
|
|
33
40
|
const wait = (ms, id) => new Promise((resolve, reject) => setTimeout(() => (id === run ? resolve() : reject(new Error("stopped"))), ms));
|
|
34
41
|
|
|
42
|
+
// Each parley opens the way a guest's does: the mark builds itself.
|
|
43
|
+
const mark = root.querySelector(".demo-top .mark");
|
|
44
|
+
mark?.querySelectorAll(".r1, .r2").forEach((path, k) => (path.style.animationDelay = `${k * 30}ms`));
|
|
45
|
+
function opening() {
|
|
46
|
+
if (!mark) return;
|
|
47
|
+
mark.classList.remove("opening");
|
|
48
|
+
void mark.getBoundingClientRect();
|
|
49
|
+
mark.classList.add("opening");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Like the real chat, the frame stays put and the conversation scrolls.
|
|
53
|
+
const toEnd = () => (list.scrollTop = list.scrollHeight);
|
|
54
|
+
|
|
55
|
+
function setInput(text) {
|
|
56
|
+
input?.replaceChildren(text ? document.createTextNode(text) : el("span", "demo-placeholder", "Write a message"));
|
|
57
|
+
send?.classList.toggle("off", !text);
|
|
58
|
+
toEnd();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The guest types into the composer and sends, as in the real chat.
|
|
62
|
+
async function say(text, id) {
|
|
63
|
+
composer?.classList.add("focus");
|
|
64
|
+
for (let k = 2; k < text.length; k += 2) {
|
|
65
|
+
setInput(text.slice(0, k));
|
|
66
|
+
await wait(30, id);
|
|
67
|
+
}
|
|
68
|
+
setInput(text);
|
|
69
|
+
await wait(450, id);
|
|
70
|
+
send?.classList.add("press");
|
|
71
|
+
await wait(130, id);
|
|
72
|
+
send?.classList.remove("press");
|
|
73
|
+
setInput("");
|
|
74
|
+
composer?.classList.remove("focus");
|
|
75
|
+
list.append(guest(text));
|
|
76
|
+
toEnd();
|
|
77
|
+
}
|
|
78
|
+
|
|
35
79
|
function pick(i) {
|
|
36
80
|
tabs.forEach((tab, j) => {
|
|
37
81
|
tab.classList.toggle("on", i === j);
|
|
@@ -49,42 +93,52 @@
|
|
|
49
93
|
const scene = scenes[i];
|
|
50
94
|
pick(i);
|
|
51
95
|
list.replaceChildren();
|
|
96
|
+
setInput("");
|
|
97
|
+
composer?.classList.remove("focus");
|
|
98
|
+
if (chip) chip.textContent = "100/100";
|
|
99
|
+
opening();
|
|
100
|
+
await wait(1200, id);
|
|
101
|
+
await say(scene.asks, id);
|
|
52
102
|
await wait(500, id);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// Working: the agent looks something up in the project, then writes.
|
|
103
|
+
// Working: the mark reads (its core flickers), then writes (light runs
|
|
104
|
+
// around it), then the reply streams in.
|
|
56
105
|
const node = el("li", "msg agent");
|
|
57
106
|
const body = el("div", "body");
|
|
58
|
-
body.innerHTML = `<span class="thinking" role="img" aria-label="
|
|
59
|
-
|
|
60
|
-
node.append(body, steps);
|
|
107
|
+
body.innerHTML = `<span class="thinking reading" role="img" aria-label="Working">${window.parleyMark({ moving: true })}</span>`;
|
|
108
|
+
node.append(body);
|
|
61
109
|
list.append(node);
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
toEnd();
|
|
111
|
+
await wait(1600, id);
|
|
112
|
+
body.firstElementChild.classList.remove("reading");
|
|
64
113
|
await wait(1400, id);
|
|
65
114
|
const p = el("p", "cursor");
|
|
66
115
|
body.replaceChildren(p);
|
|
67
116
|
const words = scene.says.split(" ");
|
|
68
117
|
for (let w = 1; w <= words.length; w++) {
|
|
69
118
|
p.textContent = words.slice(0, w).join(" ");
|
|
119
|
+
toEnd();
|
|
70
120
|
await wait(55, id);
|
|
71
121
|
}
|
|
72
122
|
p.classList.remove("cursor");
|
|
73
|
-
|
|
74
|
-
await wait(
|
|
75
|
-
|
|
123
|
+
if (chip) chip.textContent = "99/100";
|
|
124
|
+
await wait(1600, id);
|
|
125
|
+
await say(scene.then, id);
|
|
76
126
|
await wait(4500, id);
|
|
77
127
|
}
|
|
78
128
|
|
|
79
|
-
async function loop(from) {
|
|
129
|
+
async function loop(from, stay = 0) {
|
|
80
130
|
const id = ++run;
|
|
81
131
|
try {
|
|
82
|
-
for (let i = from; ; i = (i + 1) % scenes.length)
|
|
132
|
+
for (let i = from; ; i = (i + 1) % scenes.length) {
|
|
133
|
+
await play(i, id);
|
|
134
|
+
if (stay) await wait(stay, id);
|
|
135
|
+
stay = 0;
|
|
136
|
+
}
|
|
83
137
|
} catch {
|
|
84
138
|
// Stopped for another scene.
|
|
85
139
|
}
|
|
86
140
|
}
|
|
87
141
|
|
|
88
|
-
tabs.forEach((tab, i) => tab.addEventListener("click", () => (still ? show(i) : loop(i))));
|
|
142
|
+
tabs.forEach((tab, i) => tab.addEventListener("click", () => (still ? show(i) : loop(i, STAY))));
|
|
89
143
|
if (!still) loop(0);
|
|
90
144
|
})();
|
package/web/icon-180.png
CHANGED
|
Binary file
|
package/web/icon.png
CHANGED
|
Binary file
|
package/web/icon.svg
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#9D92FF"/><
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#1E1C27"/><path fill="#9D92FF" d="M32 7L39.2 11.2L32 15.3Z"/><path fill="#8A7FF0" d="M39.2 11.2L39.2 19.5L32 15.3Z"/><path fill="#9D92FF" d="M39.2 11.2L46.4 15.3L39.2 19.5Z"/><path fill="#8A7FF0" d="M46.4 15.3L46.4 23.7L39.2 19.5Z"/><path fill="#9D92FF" d="M46.4 15.3L53.7 19.5L46.4 23.7Z"/><path fill="#8A7FF0" d="M53.7 19.5L53.7 27.8L46.4 23.7Z"/><path fill="#9D92FF" d="M46.4 23.7L53.7 27.8L46.4 32Z"/><path fill="#8A7FF0" d="M53.7 27.8L53.7 36.2L46.4 32Z"/><path fill="#9D92FF" d="M46.4 32L53.7 36.2L46.4 40.3Z"/><path fill="#8A7FF0" d="M53.7 36.2L53.7 44.5L46.4 40.3Z"/><path fill="#9D92FF" d="M46.4 40.3L53.7 44.5L46.4 48.7Z"/><path fill="#8A7FF0" d="M46.4 40.3L46.4 48.7L39.2 44.5Z"/><path fill="#9D92FF" d="M39.2 44.5L46.4 48.7L39.2 52.8Z"/><path fill="#8A7FF0" d="M39.2 44.5L39.2 52.8L32 48.7Z"/><path fill="#9D92FF" d="M32 48.7L39.2 52.8L32 57Z"/><path fill="#8A7FF0" d="M32 48.7L32 57L24.8 52.8Z"/><path fill="#9D92FF" d="M24.8 44.5L32 48.7L24.8 52.8Z"/><path fill="#8A7FF0" d="M24.8 44.5L24.8 52.8L17.6 48.7Z"/><path fill="#9D92FF" d="M17.6 40.3L24.8 44.5L17.6 48.7Z"/><path fill="#8A7FF0" d="M17.6 40.3L17.6 48.7L10.3 44.5Z"/><path fill="#9D92FF" d="M10.3 36.2L17.6 40.3L10.3 44.5Z"/><path fill="#8A7FF0" d="M17.6 32L17.6 40.3L10.3 36.2Z"/><path fill="#9D92FF" d="M10.3 27.8L17.6 32L10.3 36.2Z"/><path fill="#8A7FF0" d="M17.6 23.7L17.6 32L10.3 27.8Z"/><path fill="#9D92FF" d="M10.3 19.5L17.6 23.7L10.3 27.8Z"/><path fill="#8A7FF0" d="M17.6 15.3L17.6 23.7L10.3 19.5Z"/><path fill="#9D92FF" d="M17.6 15.3L24.8 19.5L17.6 23.7Z"/><path fill="#8A7FF0" d="M24.8 11.2L24.8 19.5L17.6 15.3Z"/><path fill="#9D92FF" d="M24.8 11.2L32 15.3L24.8 19.5Z"/><path fill="#8A7FF0" d="M32 7L32 15.3L24.8 11.2Z"/><path fill="#D2CCFF" d="M32 23.7L39.2 27.8L32 32Z"/><path fill="#BDB4FF" d="M39.2 27.8L39.2 36.2L32 32Z"/><path fill="#D2CCFF" d="M32 32L39.2 36.2L32 40.3Z"/><path fill="#BDB4FF" d="M32 32L32 40.3L24.8 36.2Z"/><path fill="#D2CCFF" d="M24.8 27.8L32 32L24.8 36.2Z"/><path fill="#BDB4FF" d="M32 23.7L32 32L24.8 27.8Z"/></svg>
|
package/web/mark.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// parley's mark, drawn on an isometric grid of equilateral triangles: a ring
|
|
2
|
+
// of thirty around a ring of space and a core of six. The same triangles do
|
|
3
|
+
// the motion. While the agent works, the core flickers as it reads and light
|
|
4
|
+
// runs around the empty ring as it writes (app.css has the animations).
|
|
5
|
+
(() => {
|
|
6
|
+
const S = 25 / 3; // a triangle's side, so the mark spans 50 of 64 units
|
|
7
|
+
const H = Math.sqrt(3) / 2;
|
|
8
|
+
const at = (i, j) => [32 + i * S * H, 32 + j * S + (i * S) / 2];
|
|
9
|
+
const far = (i, j) => Math.max(Math.abs(i), Math.abs(j), Math.abs(i + j));
|
|
10
|
+
const fmt = (n) => +n.toFixed(1);
|
|
11
|
+
|
|
12
|
+
const tris = [];
|
|
13
|
+
for (let i = -4; i <= 3; i++) {
|
|
14
|
+
for (let j = -4; j <= 3; j++) {
|
|
15
|
+
for (const corners of [[[i, j], [i + 1, j], [i, j + 1]], [[i + 1, j], [i + 1, j + 1], [i, j + 1]]]) {
|
|
16
|
+
const ring = Math.max(...corners.map(([a, b]) => far(a, b)));
|
|
17
|
+
if (ring > 3) continue;
|
|
18
|
+
const pts = corners.map(([a, b]) => at(a, b));
|
|
19
|
+
const cx = (pts[0][0] + pts[1][0] + pts[2][0]) / 3 - 32;
|
|
20
|
+
const cy = (pts[0][1] + pts[1][1] + pts[2][1]) / 3 - 32;
|
|
21
|
+
const xs = pts.map((p) => p[0]).sort((a, b) => a - b);
|
|
22
|
+
tris.push({
|
|
23
|
+
ring,
|
|
24
|
+
turn: ((Math.atan2(cx, -cy) * 180) / Math.PI + 360) % 360, // clockwise from the top
|
|
25
|
+
tone: Math.abs(xs[0] - xs[1]) < 0.01 ? 1 : 2, // which way it points
|
|
26
|
+
d: "M" + pts.map((p) => `${fmt(p[0])} ${fmt(p[1])}`).join("L") + "Z",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
tris.sort((a, b) => a.turn - b.turn);
|
|
32
|
+
const SCAN = [0, 0.4, 0.8, 0.2, 0.6, 1.0];
|
|
33
|
+
|
|
34
|
+
// The still mark, or with `moving` the one that reads and writes.
|
|
35
|
+
window.parleyMark = ({ moving = false } = {}) => {
|
|
36
|
+
const ring = tris.filter((t) => t.ring === 3).map((t) => `<path class="r${t.tone}" d="${t.d}"/>`);
|
|
37
|
+
const core = tris
|
|
38
|
+
.filter((t) => t.ring === 1)
|
|
39
|
+
.map((t, k) => `<path class="c${t.tone}"${moving ? ` style="animation-delay:${SCAN[k]}s"` : ""} d="${t.d}"/>`);
|
|
40
|
+
const space = moving ? tris.filter((t) => t.ring === 2).map((t, k) => `<path class="g${t.tone}" style="animation-delay:${(k * 0.1).toFixed(1)}s" d="${t.d}"/>`) : [];
|
|
41
|
+
return `<svg viewBox="0 0 64 64" aria-hidden="true">${ring.join("")}${core.join("")}${space.join("")}</svg>`;
|
|
42
|
+
};
|
|
43
|
+
})();
|
package/web/missing.html
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
<meta name="theme-color" content="#0c0c0d">
|
|
7
7
|
<meta name="robots" content="noindex, nofollow">
|
|
8
8
|
<title>Link not found</title>
|
|
9
|
-
<link rel="icon" href="/static/icon.svg?v=
|
|
9
|
+
<link rel="icon" href="/static/icon.svg?v=6" type="image/svg+xml">
|
|
10
10
|
<link rel="stylesheet" href="/static/app.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div class="lost">
|
|
14
14
|
<div>
|
|
15
|
-
<span class="mark" aria-hidden="true"><svg viewBox="0 0 64 64"><
|
|
15
|
+
<span class="mark dim" aria-hidden="true"><svg viewBox="0 0 64 64" aria-hidden="true"><path class="r1" d="M32 7L39.2 11.2L32 15.3Z"/><path class="r2" d="M39.2 11.2L39.2 19.5L32 15.3Z"/><path class="r1" d="M39.2 11.2L46.4 15.3L39.2 19.5Z"/><path class="r2" d="M46.4 15.3L46.4 23.7L39.2 19.5Z"/><path class="r1" d="M46.4 15.3L53.7 19.5L46.4 23.7Z"/><path class="r2" d="M53.7 19.5L53.7 27.8L46.4 23.7Z"/><path class="r1" d="M46.4 23.7L53.7 27.8L46.4 32Z"/><path class="r2" d="M53.7 27.8L53.7 36.2L46.4 32Z"/><path class="r1" d="M46.4 32L53.7 36.2L46.4 40.3Z"/><path class="r2" d="M53.7 36.2L53.7 44.5L46.4 40.3Z"/><path class="r1" d="M46.4 40.3L53.7 44.5L46.4 48.7Z"/><path class="r2" d="M46.4 40.3L46.4 48.7L39.2 44.5Z"/><path class="r1" d="M39.2 44.5L46.4 48.7L39.2 52.8Z"/><path class="r2" d="M39.2 44.5L39.2 52.8L32 48.7Z"/><path class="r1" d="M32 48.7L39.2 52.8L32 57Z"/><path class="r2" d="M32 48.7L32 57L24.8 52.8Z"/><path class="r1" d="M24.8 44.5L32 48.7L24.8 52.8Z"/><path class="r2" d="M24.8 44.5L24.8 52.8L17.6 48.7Z"/><path class="r1" d="M17.6 40.3L24.8 44.5L17.6 48.7Z"/><path class="r2" d="M17.6 40.3L17.6 48.7L10.3 44.5Z"/><path class="r1" d="M10.3 36.2L17.6 40.3L10.3 44.5Z"/><path class="r2" d="M17.6 32L17.6 40.3L10.3 36.2Z"/><path class="r1" d="M10.3 27.8L17.6 32L10.3 36.2Z"/><path class="r2" d="M17.6 23.7L17.6 32L10.3 27.8Z"/><path class="r1" d="M10.3 19.5L17.6 23.7L10.3 27.8Z"/><path class="r2" d="M17.6 15.3L17.6 23.7L10.3 19.5Z"/><path class="r1" d="M17.6 15.3L24.8 19.5L17.6 23.7Z"/><path class="r2" d="M24.8 11.2L24.8 19.5L17.6 15.3Z"/><path class="r1" d="M24.8 11.2L32 15.3L24.8 19.5Z"/><path class="r2" d="M32 7L32 15.3L24.8 11.2Z"/><path class="c1" d="M32 23.7L39.2 27.8L32 32Z"/><path class="c2" d="M39.2 27.8L39.2 36.2L32 32Z"/><path class="c1" d="M32 32L39.2 36.2L32 40.3Z"/><path class="c2" d="M32 32L32 40.3L24.8 36.2Z"/><path class="c1" d="M24.8 27.8L32 32L24.8 36.2Z"/><path class="c2" d="M32 23.7L32 32L24.8 27.8Z"/></svg></span>
|
|
16
16
|
<h1>This link doesn't lead anywhere</h1>
|
|
17
17
|
<p>Check that you have the whole link, or ask whoever sent it for a fresh one.</p>
|
|
18
18
|
</div>
|
package/web/panel.html
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta name="theme-color" content="#0c0c0d">
|
|
7
|
-
<title>Control panel ·
|
|
8
|
-
<link rel="icon" href="/static/icon.svg?v=
|
|
7
|
+
<title>Control panel · Parley</title>
|
|
8
|
+
<link rel="icon" href="/static/icon.svg?v=6" type="image/svg+xml">
|
|
9
9
|
<link rel="stylesheet" href="/static/app.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="panel">
|
|
12
12
|
<header class="topbar">
|
|
13
|
-
<span class="mark" aria-hidden="true"><svg viewBox="0 0 64 64"><
|
|
13
|
+
<span class="mark" aria-hidden="true"><svg viewBox="0 0 64 64" aria-hidden="true"><path class="r1" d="M32 7L39.2 11.2L32 15.3Z"/><path class="r2" d="M39.2 11.2L39.2 19.5L32 15.3Z"/><path class="r1" d="M39.2 11.2L46.4 15.3L39.2 19.5Z"/><path class="r2" d="M46.4 15.3L46.4 23.7L39.2 19.5Z"/><path class="r1" d="M46.4 15.3L53.7 19.5L46.4 23.7Z"/><path class="r2" d="M53.7 19.5L53.7 27.8L46.4 23.7Z"/><path class="r1" d="M46.4 23.7L53.7 27.8L46.4 32Z"/><path class="r2" d="M53.7 27.8L53.7 36.2L46.4 32Z"/><path class="r1" d="M46.4 32L53.7 36.2L46.4 40.3Z"/><path class="r2" d="M53.7 36.2L53.7 44.5L46.4 40.3Z"/><path class="r1" d="M46.4 40.3L53.7 44.5L46.4 48.7Z"/><path class="r2" d="M46.4 40.3L46.4 48.7L39.2 44.5Z"/><path class="r1" d="M39.2 44.5L46.4 48.7L39.2 52.8Z"/><path class="r2" d="M39.2 44.5L39.2 52.8L32 48.7Z"/><path class="r1" d="M32 48.7L39.2 52.8L32 57Z"/><path class="r2" d="M32 48.7L32 57L24.8 52.8Z"/><path class="r1" d="M24.8 44.5L32 48.7L24.8 52.8Z"/><path class="r2" d="M24.8 44.5L24.8 52.8L17.6 48.7Z"/><path class="r1" d="M17.6 40.3L24.8 44.5L17.6 48.7Z"/><path class="r2" d="M17.6 40.3L17.6 48.7L10.3 44.5Z"/><path class="r1" d="M10.3 36.2L17.6 40.3L10.3 44.5Z"/><path class="r2" d="M17.6 32L17.6 40.3L10.3 36.2Z"/><path class="r1" d="M10.3 27.8L17.6 32L10.3 36.2Z"/><path class="r2" d="M17.6 23.7L17.6 32L10.3 27.8Z"/><path class="r1" d="M10.3 19.5L17.6 23.7L10.3 27.8Z"/><path class="r2" d="M17.6 15.3L17.6 23.7L10.3 19.5Z"/><path class="r1" d="M17.6 15.3L24.8 19.5L17.6 23.7Z"/><path class="r2" d="M24.8 11.2L24.8 19.5L17.6 15.3Z"/><path class="r1" d="M24.8 11.2L32 15.3L24.8 19.5Z"/><path class="r2" d="M32 7L32 15.3L24.8 11.2Z"/><path class="c1" d="M32 23.7L39.2 27.8L32 32Z"/><path class="c2" d="M39.2 27.8L39.2 36.2L32 32Z"/><path class="c1" d="M32 32L39.2 36.2L32 40.3Z"/><path class="c2" d="M32 32L32 40.3L24.8 36.2Z"/><path class="c1" d="M24.8 27.8L32 32L24.8 36.2Z"/><path class="c2" d="M32 23.7L32 32L24.8 27.8Z"/></svg></span>
|
|
14
14
|
<nav class="crumbs"><span class="product">parley</span><span class="slash">/</span><span class="here">Control panel</span></nav>
|
|
15
15
|
<span class="status" id="server"><span class="dot" id="server-dot"></span><span id="server-text">Checking…</span></span>
|
|
16
16
|
</header>
|
package/web/panel.js
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
offline: ["warn", `Reconnecting to ${host}`],
|
|
52
52
|
"signed-out": ["warn", "Not signed in · run parley login"],
|
|
53
53
|
unlinked: ["off", "Unlinked · run parley login"],
|
|
54
|
-
replaced: ["off", "Another
|
|
54
|
+
replaced: ["off", "Another Parley server took over"],
|
|
55
55
|
};
|
|
56
56
|
const [dot, label] = RELAY[relay.state] ?? ["warn", relay.state];
|
|
57
57
|
$("server-dot").className = `dot ${dot}`;
|