@parley-im/parley 0.2.8 → 0.2.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parley-im/parley",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Send someone a link. They talk to a read-only Claude or Codex in your project, on your computer, and you get everything they said.",
5
5
  "keywords": [
6
6
  "claude",
package/web/app.css CHANGED
@@ -1,9 +1,11 @@
1
1
  /* parley: one stylesheet for the guest chat, your session view, your
2
- control panel and parley's site. Near black, with deep, murky ocean green
3
- for accents, for the people who use Claude Code and Codex: Instrument Sans for everything
2
+ control panel and parley's site. Dark, with lilac for the accents, for the
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 agent's words are marked with a dot, as in Claude Code, and while it
6
- writes, P goes by in Morse. The fonts are served from here, never from a
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; }
@@ -11,25 +13,24 @@
11
13
  @font-face { font-family: "JetBrains Mono"; src: url("/static/fonts/jetbrains-mono.woff2") format("woff2"); font-weight: 100 800; font-style: normal; font-display: swap; }
12
14
 
13
15
  :root {
14
- --bg: #070a09; /* near black, the deep */
15
- --panel: #0d1311;
16
- --subtle: #121a17;
17
- --hover: #18221e;
18
- --line: #1a2420;
19
- --line-strong: #2a3833;
20
- --text: #e6ece9;
21
- --muted: #95a39d;
22
- --faint: #5d6b65;
23
- --accent: #6db39c; /* sea glass, for links and small marks */
24
- --accent-fill: #1f5c50; /* deep ocean green, for buttons and fills */
25
- --accent-soft: #11241f;
26
- --accent-ink: #ffffff;
27
- --tile: #1f5c50; /* the mark's tile */
28
- --guest: #12302a; /* your own messages */
29
- --guest-ink: #d5e7e0;
30
- --ok: #4fb88a;
31
- --warn: #d9a24a;
32
- --danger: #f2615d;
16
+ --bg: #0c0c0d;
17
+ --panel: #131315;
18
+ --subtle: #19191c;
19
+ --hover: #212125;
20
+ --line: #26262b;
21
+ --line-strong: #36363c;
22
+ --text: #ececef;
23
+ --muted: #9b9ba4;
24
+ --faint: #66666f;
25
+ --accent: #9d92ff; /* lilac */
26
+ --accent-fill: #9d92ff; /* buttons, with dark text */
27
+ --accent-soft: #221f3d;
28
+ --accent-ink: #0c0c0d;
29
+ --guest: #221f3d; /* your own messages */
30
+ --guest-ink: #e6e2ff;
31
+ --ok: #3ccf7a;
32
+ --warn: #e3a948;
33
+ --danger: #ff6b63;
33
34
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 12px 32px -16px rgba(0, 0, 0, 0.8);
34
35
  --sans: "Instrument Sans", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
35
36
  --display: var(--sans);
@@ -57,11 +58,27 @@ code, kbd, pre { font-family: var(--mono); }
57
58
 
58
59
  /* ---------- shared pieces ---------- */
59
60
 
60
- /* The mark, for now: a p on a deep green tile. */
61
- .mark { display: inline-block; width: 24px; height: 24px; flex: none; }
62
- .mark svg { display: block; width: 100%; height: 100%; }
63
- .mark .tile { fill: var(--tile); }
64
- .mark .glyph { fill: #ffffff; }
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 get a dot, as in Claude Code. */
170
- .msg.agent::before { content: ""; left: 3px; top: 9px; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
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,17 +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
- /* Writing: P in Morse (dot, dash, dash, dot), passing by. */
226
- .thinking { display: inline-flex; align-items: center; gap: 4px; height: 26px; }
227
- .thinking span { width: 7px; height: 7px; border-radius: 4px; background: var(--accent); opacity: 0.25; animation: morse 1.6s ease-in-out infinite; }
228
- .thinking span:nth-child(2), .thinking span:nth-child(3) { width: 17px; }
229
- .thinking span:nth-child(2) { animation-delay: 0.16s; }
230
- .thinking span:nth-child(3) { animation-delay: 0.32s; }
231
- .thinking span:nth-child(4) { animation-delay: 0.48s; }
232
- @keyframes morse { 0%, 70%, 100% { opacity: 0.25; } 30% { opacity: 1; } }
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; } }
233
255
  @keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
234
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; }
235
- @media (prefers-reduced-motion: reduce) { .thinking span, .cursor::after, .dot.busy { animation: none; } .thinking span { opacity: 0.7; } }
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
+ }
236
261
 
237
262
  .dock { border-top: 1px solid var(--line); background: var(--bg); }
238
263
  .dock .chat-inner { padding: 8px 16px calc(8px + env(safe-area-inset-bottom)); }
@@ -389,7 +414,7 @@ body.panel { overflow: auto; }
389
414
 
390
415
  .lost { min-height: 100dvh; display: grid; place-items: center; padding: 16px; text-align: center; }
391
416
  .lost.inline { min-height: 100%; }
392
- .lost .mark { width: 40px; height: 40px; margin: 0 auto 16px; }
417
+ .lost .mark { width: 52px; height: 52px; margin: 0 auto 16px; }
393
418
  .lost h1 { font-size: 20px; margin: 0 0 6px; letter-spacing: -0.01em; }
394
419
  .lost p { color: var(--muted); margin: 0 auto; max-width: 34ch; }
395
420
 
@@ -468,7 +493,7 @@ span.live { display: inline-flex; align-items: center; gap: 7px; font-size: 13px
468
493
  .demo-msgs { list-style: none; margin: 0; padding: 18px 18px 6px; min-height: 340px; display: flex; flex-direction: column; gap: 14px; }
469
494
  .demo .msg { margin: 0; animation: rise 0.25s ease-out; }
470
495
  .demo .msg.agent .body { font-size: 15.5px; }
471
- .demo .msg.agent::before { top: 8px; }
496
+ .demo .msg.agent::before { top: 5px; }
472
497
  .demo .msg.guest .bubble { font-size: 14.5px; max-width: 86%; }
473
498
  .demo .steps { margin-top: 2px; }
474
499
  .demo-say { margin: 0 14px 14px; padding: 8px 8px 8px 14px; border: 1.5px solid var(--line-strong); border-radius: 13px; color: var(--faint); font-size: 15px; display: flex; align-items: center; justify-content: space-between; }
package/web/chat.html CHANGED
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
6
6
  <meta name="robots" content="noindex, nofollow">
7
7
  <meta name="referrer" content="no-referrer">
8
- <meta name="theme-color" content="#070a09">
8
+ <meta name="theme-color" content="#0c0c0d">
9
9
  <meta property="og:title" content="{{PREVIEW_TITLE}}">
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=5" type="image/svg+xml">
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>
@@ -19,7 +19,7 @@
19
19
  <nav class="crumbs" id="crumbs">
20
20
  <!-- What parley is, in a new tab so the conversation stays put. -->
21
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"><rect class="tile" width="64" height="64" rx="14"/><rect class="glyph" x="19.5" y="16" width="7" height="32" rx="1.5"/><path class="glyph" fill-rule="evenodd" d="M44.5 25.5A10 10 0 1 1 24.5 25.5A10 10 0 1 1 44.5 25.5ZM38.5 25.5A4 4 0 1 0 30.5 25.5A4 4 0 1 0 38.5 25.5Z"/></svg></span><span class="product">parley</span>
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">&nbsp;</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>
@@ -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 live.bodyEl.innerHTML = `<span class="thinking" role="img" aria-label="Writing"><span></span><span></span><span></span><span></span></span>`;
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 })));
@@ -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 = `<svg viewBox="0 0 64 64"><rect class="tile" width="64" height="64" rx="14"/><rect class="glyph" x="19.5" y="16" width="7" height="32" rx="1.5"/><path class="glyph" fill-rule="evenodd" d="M44.5 25.5A10 10 0 1 1 24.5 25.5A10 10 0 1 1 44.5 25.5ZM38.5 25.5A4 4 0 1 0 30.5 25.5A4 4 0 1 0 38.5 25.5Z"/></svg>`;
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 || "";
@@ -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") addMessage(event.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/ended.html CHANGED
@@ -3,16 +3,16 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <meta name="theme-color" content="#070a09">
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=5" type="image/svg+xml">
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"><rect class="tile" width="64" height="64" rx="14"/><rect class="glyph" x="19.5" y="16" width="7" height="32" rx="1.5"/><path class="glyph" fill-rule="evenodd" d="M44.5 25.5A10 10 0 1 1 24.5 25.5A10 10 0 1 1 44.5 25.5ZM38.5 25.5A4 4 0 1 0 30.5 25.5A4 4 0 1 0 38.5 25.5Z"/></svg></span>
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,5 +1,5 @@
1
1
  // parley's front page: plays the demo, three short parleys in turn, the way
2
- // a guest sees one. The first is already in the page, so without script (or
2
+ // a guest sees one (mark.js draws the working mark). The first is already in the page, so without script (or
3
3
  // for anyone who prefers less motion) it just sits there, and the tabs swap
4
4
  // scenes without playing them.
5
5
  (() => {
@@ -32,6 +32,16 @@
32
32
  };
33
33
  const wait = (ms, id) => new Promise((resolve, reject) => setTimeout(() => (id === run ? resolve() : reject(new Error("stopped"))), ms));
34
34
 
35
+ // Each parley opens the way a guest's does: the mark builds itself.
36
+ const mark = root.querySelector(".demo-top .mark");
37
+ mark?.querySelectorAll(".r1, .r2").forEach((path, k) => (path.style.animationDelay = `${k * 30}ms`));
38
+ function opening() {
39
+ if (!mark) return;
40
+ mark.classList.remove("opening");
41
+ void mark.getBoundingClientRect();
42
+ mark.classList.add("opening");
43
+ }
44
+
35
45
  function pick(i) {
36
46
  tabs.forEach((tab, j) => {
37
47
  tab.classList.toggle("on", i === j);
@@ -49,18 +59,19 @@
49
59
  const scene = scenes[i];
50
60
  pick(i);
51
61
  list.replaceChildren();
62
+ opening();
52
63
  await wait(500, id);
53
64
  list.append(guest(scene.asks));
54
65
  await wait(900, id);
55
- // Working: the agent looks something up in the project, then writes.
66
+ // Working: the mark reads (its core flickers), then writes (light runs
67
+ // around it), then the reply streams in.
56
68
  const node = el("li", "msg agent");
57
69
  const body = el("div", "body");
58
- body.innerHTML = `<span class="thinking" role="img" aria-label="Writing"><span></span><span></span><span></span><span></span></span>`;
59
- const steps = el("ul", "steps");
60
- node.append(body, steps);
70
+ body.innerHTML = `<span class="thinking reading" role="img" aria-label="Working">${window.parleyMark({ moving: true })}</span>`;
71
+ node.append(body);
61
72
  list.append(node);
62
- await wait(700, id);
63
- steps.append(el("li", "now", scene.step));
73
+ await wait(1600, id);
74
+ body.firstElementChild.classList.remove("reading");
64
75
  await wait(1400, id);
65
76
  const p = el("p", "cursor");
66
77
  body.replaceChildren(p);
@@ -70,7 +81,6 @@
70
81
  await wait(55, id);
71
82
  }
72
83
  p.classList.remove("cursor");
73
- steps.remove();
74
84
  await wait(1500, id);
75
85
  list.append(guest(scene.then));
76
86
  await wait(4500, id);
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="#22574C"/><rect x="19.5" y="16" width="7" height="32" rx="1.5" fill="#FFFFFF"/><path fill="#FFFFFF" fill-rule="evenodd" d="M44.5 25.5A10 10 0 1 1 24.5 25.5A10 10 0 1 1 44.5 25.5ZM38.5 25.5A4 4 0 1 0 30.5 25.5A4 4 0 1 0 38.5 25.5Z"/></svg>
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
@@ -3,16 +3,16 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <meta name="theme-color" content="#070a09">
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=5" type="image/svg+xml">
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"><rect class="tile" width="64" height="64" rx="14"/><rect class="glyph" x="19.5" y="16" width="7" height="32" rx="1.5"/><path class="glyph" fill-rule="evenodd" d="M44.5 25.5A10 10 0 1 1 24.5 25.5A10 10 0 1 1 44.5 25.5ZM38.5 25.5A4 4 0 1 0 30.5 25.5A4 4 0 1 0 38.5 25.5Z"/></svg></span>
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
@@ -3,14 +3,14 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <meta name="theme-color" content="#070a09">
6
+ <meta name="theme-color" content="#0c0c0d">
7
7
  <title>Control panel · parley</title>
8
- <link rel="icon" href="/static/icon.svg?v=5" type="image/svg+xml">
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"><rect class="tile" width="64" height="64" rx="14"/><rect class="glyph" x="19.5" y="16" width="7" height="32" rx="1.5"/><path class="glyph" fill-rule="evenodd" d="M44.5 25.5A10 10 0 1 1 24.5 25.5A10 10 0 1 1 44.5 25.5ZM38.5 25.5A4 4 0 1 0 30.5 25.5A4 4 0 1 0 38.5 25.5Z"/></svg></span>
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>