agello 0.4.2 → 0.4.3
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 -2
- package/package.json +1 -1
- package/src/player.ts +10 -0
- package/src/server.ts +3 -2
- package/web/agent-bridge.js +103 -26
package/README.md
CHANGED
|
@@ -113,7 +113,8 @@ The xterm.js renderer and its styles are served locally; no terminal CDN is requ
|
|
|
113
113
|
- agent replies appear over it as bubbles that fade after 10s
|
|
114
114
|
- `agello present stop` returns to the normal layout; the replies are in the chat as usual
|
|
115
115
|
- the viewer can ask a question with the raise-hand button (its bubble fades after 10s too) or end the presentation with the end button; the agent then receives `[browser] action=present-stop`
|
|
116
|
-
- raising the hand tells the agent at once (`action=hand-raise`) so it can pause; closing
|
|
116
|
+
- raising the hand tells the agent at once (`action=hand-raise`) so it can pause; closing it with X (or Esc) before asking sends `action=hand-lower`
|
|
117
|
+
- the input stays open for the whole question: after the agent answers, follow-up questions go in the same input (no need to raise the hand again); X (or Esc) then sends `action=hand-done`, and the agent resumes
|
|
117
118
|
|
|
118
119
|
Run `present` again to move the crop. User control is off while presenting.
|
|
119
120
|
|
|
@@ -130,7 +131,8 @@ Write the talk ahead so each line appears the moment its screen does:
|
|
|
130
131
|
|
|
131
132
|
- step: `go` brings the screen there (`#n` sets `location.hash`, anything else is JS run in the page; agello knows nothing about the deck), `say` lines are one bubble each, `hold` seconds a line stays on screen (default from length, 10 to 20s; never below 10s). Pacing: screen change, 1s, line, fade out, 0.3s, next line; after a step's last line 0.7s before the next screen
|
|
132
133
|
- `present resume` plays from where it stopped (starts presentation mode with the script's `rect`); it first re-runs the current step's `go`, so the agent may move the screen freely while answering
|
|
133
|
-
- raising a hand pauses at once; the agent gets `action=hand-raise` with the position (`마지막 표시 2.1 · 다음 2.
|
|
134
|
+
- raising a hand pauses at once and rewinds one line, so `present resume` replays the interrupted line; the agent gets `action=hand-raise` with the position (`마지막 표시 2.1 · 다음 2.1`)
|
|
135
|
+
- raising, cancelling (X before asking) and finishing a question (X after an answer) show a small light event bubble for 5s right away, apart from the conversation bubbles; sending a question shows none
|
|
134
136
|
- `script load` again after editing keeps the position; `present goto 2.2` then `present resume` replays from a fixed line
|
|
135
137
|
- at the end the agent gets `action=present-done`; while the script is paused and the agent is working, the page shows a small loader
|
|
136
138
|
|
package/package.json
CHANGED
package/src/player.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// line is sent and held for its reading time (see PACE). pause() stops at once and keeps
|
|
10
10
|
// the position, resume() re-runs the current step's `go` (the agent may have
|
|
11
11
|
// moved the screen meanwhile) and continues from the next unshown line.
|
|
12
|
+
// rewind() (after a raised hand) moves back so the last shown line plays again.
|
|
12
13
|
// goto() moves the position and shows that step without playing.
|
|
13
14
|
//
|
|
14
15
|
// Positions are "step.line", 1-based, in everything that leaves this module.
|
|
@@ -201,6 +202,15 @@ export function createPlayer(deps: {
|
|
|
201
202
|
set("paused");
|
|
202
203
|
},
|
|
203
204
|
|
|
205
|
+
// Paused by an interruption (raised hand): the next resume replays the
|
|
206
|
+
// last shown line, since the viewer may have missed it.
|
|
207
|
+
rewind() {
|
|
208
|
+
if (state !== "paused" || !last) return;
|
|
209
|
+
cur = { ...last };
|
|
210
|
+
entered = false;
|
|
211
|
+
deps.changed();
|
|
212
|
+
},
|
|
213
|
+
|
|
204
214
|
// Move to a step (and line) and show its screen. Does not play; if it was
|
|
205
215
|
// playing, it keeps playing from there.
|
|
206
216
|
async goto(pos: { step: number; line: number }): Promise<string | null> {
|
package/src/server.ts
CHANGED
|
@@ -530,10 +530,11 @@ export async function startServer(opts: ServerOptions) {
|
|
|
530
530
|
if (!t || !text.trim()) return Response.json({ ok: false, error: "invalid_target" }, { status: 400 });
|
|
531
531
|
text = t;
|
|
532
532
|
}
|
|
533
|
-
// Raised hand: stop the script at once (no agent round trip)
|
|
534
|
-
// agent where it stopped.
|
|
533
|
+
// Raised hand: stop the script at once (no agent round trip), rewind so
|
|
534
|
+
// resume replays the interrupted line, and tell the agent where it stopped.
|
|
535
535
|
if (action === "hand-raise") {
|
|
536
536
|
player.pause();
|
|
537
|
+
player.rewind();
|
|
537
538
|
const at = where();
|
|
538
539
|
if (at) text += ` (대본 정지: ${at})`;
|
|
539
540
|
}
|
package/web/agent-bridge.js
CHANGED
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
// A raise-hand button next to the bubbles opens a small input so the viewer
|
|
43
43
|
// can ask the agent something mid-presentation (sent as action=message).
|
|
44
44
|
// Opening it tells the agent right away (action=hand-raise) so it can pause;
|
|
45
|
-
// closing it
|
|
45
|
+
// closing it with X (or Esc) before asking sends action=hand-lower so it can go on.
|
|
46
|
+
// The input stays open while the agent answers, for follow-up questions; X
|
|
47
|
+
// (or Esc) sends action=hand-done so the agent resumes the presentation.
|
|
46
48
|
// The viewer's questions show as bubbles too, with the same 10s lifetime.
|
|
47
49
|
// An end button above it stops the presentation for every viewer.
|
|
48
50
|
// Script lines (`agello script` + `present resume`) arrive as agent messages
|
|
@@ -81,7 +83,7 @@ const SCREEN_REASONS = {
|
|
|
81
83
|
};
|
|
82
84
|
const ACTIONS = {
|
|
83
85
|
message: "메시지", approve: "승인", reject: "거절",
|
|
84
|
-
"present-stop": "발표 종료", "hand-raise": "손들기", "hand-lower": "손 내림", annotate: "주석",
|
|
86
|
+
"present-stop": "발표 종료", "hand-raise": "손들기", "hand-lower": "손 내림", "hand-done": "질문 완료", annotate: "주석",
|
|
85
87
|
};
|
|
86
88
|
|
|
87
89
|
const CSS = `
|
|
@@ -276,6 +278,9 @@ h1 { font-size: 14px; margin: 0; font-weight: 600; }
|
|
|
276
278
|
box-shadow: 0 8px 30px rgba(0, 0, 0, .35); animation: capIn .3s ease; }
|
|
277
279
|
.caption.plain { white-space: pre-wrap; }
|
|
278
280
|
.caption.user { background: color-mix(in srgb, var(--_accent) 88%, transparent); }
|
|
281
|
+
/* system event (hand raised / lowered / done): small, light, not part of the conversation */
|
|
282
|
+
.caption.event { padding: 6px 14px; border-radius: 999px; font-size: 13px; color: #1d1d1f;
|
|
283
|
+
background: rgba(255, 255, 255, .9); box-shadow: 0 4px 16px rgba(0,0,0,.25); }
|
|
279
284
|
.caption.out { opacity: 0; translate: 0 6px; transition: opacity .5s ease, translate .5s ease; }
|
|
280
285
|
.caption > :first-child { margin-top: 0; } .caption > :last-child { margin-bottom: 0; }
|
|
281
286
|
.caption p, .caption ul, .caption ol { margin: .35em 0; }
|
|
@@ -317,6 +322,13 @@ h1 { font-size: 14px; margin: 0; font-weight: 600; }
|
|
|
317
322
|
.ask textarea::placeholder { color: rgba(255,255,255,.5); }
|
|
318
323
|
.ask .ask-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
|
|
319
324
|
.ask .ask-hint { font-size: 12px; color: rgba(255,255,255,.6); flex: 1; }
|
|
325
|
+
.ask .ask-row .primary { margin-left: 0; }
|
|
326
|
+
.ask .ask-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; color: rgba(255,255,255,.75); }
|
|
327
|
+
.ask .ask-title { flex: 1; }
|
|
328
|
+
.ask .ask-done { width: 28px; height: 28px; padding: 0; border-radius: 50%; display: grid; place-items: center;
|
|
329
|
+
background: transparent; color: #fff; border-color: rgba(255,255,255,.22); }
|
|
330
|
+
.ask .ask-done:hover { background: rgba(255,255,255,.14); }
|
|
331
|
+
.ask .ask-done svg { width: 16px; height: 16px; }
|
|
320
332
|
@media (prefers-reduced-motion: reduce) { .hand, .ask { transition: none; animation: none; } }
|
|
321
333
|
@media (prefers-reduced-motion: reduce) { .caption { animation: none; } .caption.out { transition: none; } }
|
|
322
334
|
@container (max-width: 820px) {
|
|
@@ -447,7 +459,10 @@ const TEMPLATE = `
|
|
|
447
459
|
<div class="note-row"><span class="note-hint"></span><button type="submit" class="primary">보내기</button></div></form><div class="captions" part="captions" aria-live="polite"></div>
|
|
448
460
|
<div class="busy" part="busy" role="status"><span class="spinner"></span><span>답변 준비 중</span></div>
|
|
449
461
|
<div class="hand-wrap">
|
|
450
|
-
<form class="ask" part="ask" hidden
|
|
462
|
+
<form class="ask" part="ask" hidden>
|
|
463
|
+
<div class="ask-head"><span class="ask-title"></span><button type="button" class="ask-done">
|
|
464
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button></div>
|
|
465
|
+
<textarea aria-label="에이전트에게 질문"></textarea>
|
|
451
466
|
<div class="ask-row"><span class="ask-hint"></span><button type="submit" class="primary">보내기</button></div></form>
|
|
452
467
|
<button class="end" part="end" type="button" aria-label="발표 종료" title="발표 종료">
|
|
453
468
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
@@ -491,6 +506,7 @@ class AgentBridge extends HTMLElement {
|
|
|
491
506
|
#present = { on: false };
|
|
492
507
|
static HISTORY_LIMIT = 500;
|
|
493
508
|
static CAPTION_MS = 10000; // presentation bubble lifetime
|
|
509
|
+
static EVENT_MS = 5000; // system event bubble lifetime
|
|
494
510
|
static CAPTION_MAX = 3; // bubbles on screen at once (oldest leaves early)
|
|
495
511
|
|
|
496
512
|
constructor() {
|
|
@@ -505,6 +521,7 @@ class AgentBridge extends HTMLElement {
|
|
|
505
521
|
controlBtn: q(".control-btn"), kbd: q(".kbd"),
|
|
506
522
|
body: q(".body"), screen: q(".screen"), captions: q(".captions"),
|
|
507
523
|
busy: q(".busy"), hand: q(".hand"), end: q(".end"), ask: q(".ask"), askBox: q(".ask textarea"), askHint: q(".ask-hint"),
|
|
524
|
+
askTitle: q(".ask-title"), askDone: q(".ask-done"),
|
|
508
525
|
annotBtn: q(".annot-btn"), hlHover: q(".hl.hover"), hlSel: q(".hl.sel"), hlLabel: q(".hl-label"),
|
|
509
526
|
note: q(".note"), noteBox: q(".note textarea"), noteHint: q(".note-hint"), noteTarget: q(".note-target"),
|
|
510
527
|
};
|
|
@@ -873,6 +890,10 @@ class AgentBridge extends HTMLElement {
|
|
|
873
890
|
// script lines carry their own on-screen time (>= 10s), others use CAPTION_MS
|
|
874
891
|
if (this.#present.on && m.role === "assistant") this.#caption(m.text, "agent", m.hold * 1000 || undefined);
|
|
875
892
|
if (this.#present.on && m.role === "browser" && m.action === "message") this.#caption(m.text, "user");
|
|
893
|
+
// a reply to the viewer's question (status polling may miss a short working phase)
|
|
894
|
+
// (the agent took the question once it is echoed in the transcript; earlier work was the hand-raise turn)
|
|
895
|
+
if (this.#qa === "asked" && m.role === "browser" && m.action === "message") this.#qaTaken = true;
|
|
896
|
+
if (this.#qa === "asked" && this.#qaTaken && m.role === "assistant" && !m.script) { this.#qaWorked = true; this.#checkAnswered(); }
|
|
876
897
|
this.#emit("agent-message", m);
|
|
877
898
|
});
|
|
878
899
|
on("present", (p) => this.#setPresent(p));
|
|
@@ -921,7 +942,7 @@ class AgentBridge extends HTMLElement {
|
|
|
921
942
|
this.#renderBusy();
|
|
922
943
|
if (on === was) return;
|
|
923
944
|
if (on) { this.#showTerminal(false); this.#setControl(false); this.#setAnnotate(false); }
|
|
924
|
-
else { this.$.captions.replaceChildren(); this.#openAsk(false); this.#
|
|
945
|
+
else { this.$.captions.replaceChildren(); this.#openAsk(false); this.#qa = null; }
|
|
925
946
|
this.#syncScreen();
|
|
926
947
|
// FLIP: grow from (or shrink back to) the side panel
|
|
927
948
|
const { screen, body } = this.$;
|
|
@@ -940,20 +961,31 @@ class AgentBridge extends HTMLElement {
|
|
|
940
961
|
const s = this.#status;
|
|
941
962
|
const busy = this.#present.on && s?.alive && s.status === "working" && this.#present.player?.state !== "playing";
|
|
942
963
|
this.$.busy.classList.toggle("on", !!busy);
|
|
964
|
+
this.#checkAnswered();
|
|
943
965
|
}
|
|
944
966
|
|
|
945
|
-
// Raise hand:
|
|
967
|
+
// Raise hand: a question session over the presentation.
|
|
968
|
+
// The input stays open for the whole session; X / Esc / the hand button close it.
|
|
969
|
+
// raised: nothing asked yet. close -> hand-lower (agent resumes)
|
|
970
|
+
// asked: question sent, agent answering (follow-ups can be typed already)
|
|
971
|
+
// follow: answered, waiting for a follow-up. close -> hand-done (agent resumes)
|
|
972
|
+
// The session ends when the script plays again or the presentation stops.
|
|
973
|
+
#qa = null; // null | "raised" | "asked" | "follow"
|
|
974
|
+
#qaTaken = false; // the last question reached the agent (echoed in the transcript)
|
|
975
|
+
#qaWorked = false; // agent seen working on it
|
|
976
|
+
|
|
946
977
|
#bindHand() {
|
|
947
978
|
const { hand, ask, askBox, askHint } = this.$;
|
|
948
979
|
hand.addEventListener("click", () => {
|
|
949
|
-
if (
|
|
950
|
-
else this.#
|
|
980
|
+
if (!this.#qa) this.#raiseHand();
|
|
981
|
+
else this.#closeHand();
|
|
951
982
|
});
|
|
983
|
+
this.$.askDone.addEventListener("click", () => { this.#closeHand(); hand.focus(); });
|
|
952
984
|
this.$.end.addEventListener("click", async () => {
|
|
953
985
|
this.$.end.disabled = true;
|
|
954
986
|
const res = await this.stopPresent();
|
|
955
987
|
this.$.end.disabled = false;
|
|
956
|
-
if (!res.ok) { this.#openAsk(
|
|
988
|
+
if (!res.ok) { this.#openAsk(this.#qa ?? "raised"); askHint.textContent = `발표 종료 실패: ${res.error}`; }
|
|
957
989
|
});
|
|
958
990
|
ask.addEventListener("submit", async (e) => {
|
|
959
991
|
e.preventDefault();
|
|
@@ -963,40 +995,85 @@ class AgentBridge extends HTMLElement {
|
|
|
963
995
|
const res = await this.send(text, "message");
|
|
964
996
|
if (!res.ok) { askHint.textContent = `전송 실패: ${res.error}`; return; }
|
|
965
997
|
askBox.value = "";
|
|
966
|
-
this.#
|
|
967
|
-
this.#
|
|
998
|
+
this.#qa = "asked";
|
|
999
|
+
this.#qaTaken = this.#qaWorked = false;
|
|
1000
|
+
this.#openAsk("asked");
|
|
968
1001
|
hand.classList.add("sent");
|
|
969
1002
|
setTimeout(() => hand.classList.remove("sent"), 1500);
|
|
970
1003
|
});
|
|
971
1004
|
askBox.addEventListener("keydown", (e) => {
|
|
972
1005
|
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); ask.requestSubmit(); }
|
|
973
|
-
if (e.key === "Escape") { e.preventDefault(); this.#
|
|
1006
|
+
if (e.key === "Escape") { e.preventDefault(); this.#closeHand(); hand.focus(); }
|
|
974
1007
|
});
|
|
975
1008
|
}
|
|
976
1009
|
|
|
977
|
-
// Raise: open the input and tell the agent to pause.
|
|
978
|
-
// asking): tell it to go on. Failures only show a hint; asking still works.
|
|
979
|
-
#handUp = false;
|
|
1010
|
+
// Raise: open the input and tell the agent to pause. Failures only show a hint; asking still works.
|
|
980
1011
|
async #raiseHand() {
|
|
981
|
-
this.#
|
|
982
|
-
|
|
983
|
-
this.#
|
|
1012
|
+
this.#event("손들기 확인 · 발표 일시정지");
|
|
1013
|
+
this.#qa = "raised";
|
|
1014
|
+
this.#openAsk("raised");
|
|
984
1015
|
const res = await this.send("사용자가 손을 듦: 질문 입력 중", "hand-raise");
|
|
985
1016
|
if (!res.ok && !this.$.ask.hidden) this.$.askHint.textContent = `손들기 알림 실패: ${res.error}`;
|
|
986
1017
|
}
|
|
1018
|
+
// X / Esc / hand button: before asking = cancel (hand-lower), after = resolved (hand-done).
|
|
1019
|
+
#closeHand() {
|
|
1020
|
+
if (this.#qa === "raised") this.#lowerHand(); else this.#doneHand();
|
|
1021
|
+
}
|
|
1022
|
+
// Cancel before asking: tell the agent to go on.
|
|
987
1023
|
async #lowerHand() {
|
|
988
1024
|
this.#openAsk(false);
|
|
989
|
-
if (
|
|
990
|
-
this.#
|
|
991
|
-
|
|
1025
|
+
if (this.#qa !== "raised") return;
|
|
1026
|
+
this.#qa = null;
|
|
1027
|
+
this.#event("손들기 취소 · 곧 발표 재개");
|
|
1028
|
+
await this.send("사용자가 질문 없이 손을 내림: 발표 계속", "hand-lower");
|
|
1029
|
+
}
|
|
1030
|
+
// X after an answer: the question is resolved, tell the agent to go on.
|
|
1031
|
+
async #doneHand() {
|
|
1032
|
+
this.#openAsk(false);
|
|
1033
|
+
if (!this.#qa || this.#qa === "raised") return;
|
|
1034
|
+
this.#qa = null;
|
|
1035
|
+
this.#event("질문 완료 · 곧 발표 재개");
|
|
1036
|
+
await this.send("사용자가 질문 완료 (추가 질문 없음): 발표 계속", "hand-done");
|
|
992
1037
|
}
|
|
993
1038
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1039
|
+
// Answer finished (agent was working, now is not): reopen the input for a follow-up.
|
|
1040
|
+
// If the script plays again (agent resumed on its own), the session is over.
|
|
1041
|
+
#checkAnswered() {
|
|
1042
|
+
if (!this.#qa) return;
|
|
1043
|
+
if (this.#present.player?.state === "playing") {
|
|
1044
|
+
if (this.#qa !== "raised") { this.#qa = null; this.#openAsk(false); }
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (this.#qa !== "asked") return;
|
|
1048
|
+
const s = this.#status;
|
|
1049
|
+
if (s?.alive && s.status === "working") { if (this.#qaTaken) this.#qaWorked = true; return; }
|
|
1050
|
+
if (this.#qaWorked && s?.alive) { this.#qa = "follow"; this.#openAsk("follow"); }
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// The input stays open for the whole session. mode: false (close) | "raised" | "asked" | "follow"
|
|
1054
|
+
static ASK_TEXT = {
|
|
1055
|
+
raised: ["질문 입력", "에이전트에게 질문 (Enter 전송, Esc·X 취소)", "손들기 취소: 발표 계속"],
|
|
1056
|
+
asked: ["답변 준비 중…", "추가 질문 (Enter 전송, Esc·X 질문 완료)", "질문 완료: 발표 계속"],
|
|
1057
|
+
follow: ["추가 질문이 있으면 입력", "추가 질문 (Enter 전송, Esc·X 질문 완료)", "질문 완료: 발표 계속"],
|
|
1058
|
+
};
|
|
1059
|
+
#openAsk(mode) {
|
|
1060
|
+
const { hand, ask, askBox, askHint, askTitle, askDone } = this.$;
|
|
1061
|
+
const wasOpen = !ask.hidden;
|
|
1062
|
+
ask.hidden = !mode;
|
|
1063
|
+
hand.setAttribute("aria-expanded", String(!!mode));
|
|
998
1064
|
askHint.textContent = "";
|
|
999
|
-
if (
|
|
1065
|
+
if (!mode) return;
|
|
1066
|
+
const [title, placeholder, done] = AgentBridge.ASK_TEXT[mode];
|
|
1067
|
+
askTitle.textContent = title;
|
|
1068
|
+
askBox.placeholder = placeholder;
|
|
1069
|
+
askDone.setAttribute("aria-label", done);
|
|
1070
|
+
askDone.title = done;
|
|
1071
|
+
if (!wasOpen || mode !== "asked") askBox.focus();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Instant local feedback for a hand interaction (no agent round trip).
|
|
1075
|
+
#event(text) {
|
|
1076
|
+
this.#caption(text, "event", AgentBridge.EVENT_MS);
|
|
1000
1077
|
}
|
|
1001
1078
|
|
|
1002
1079
|
// Agent reply as a short-lived bubble over the presented screen.
|
|
@@ -1006,7 +1083,7 @@ class AgentBridge extends HTMLElement {
|
|
|
1006
1083
|
const el = document.createElement("div");
|
|
1007
1084
|
el.className = `caption ${kind}`;
|
|
1008
1085
|
el.part = "caption";
|
|
1009
|
-
if (kind === "user") { el.textContent = text; el.classList.add("plain"); }
|
|
1086
|
+
if (kind === "user" || kind === "event") { el.textContent = text; el.classList.add("plain"); }
|
|
1010
1087
|
else if (this.#md) el.innerHTML = this.#md(text);
|
|
1011
1088
|
else { el.textContent = text; el.classList.add("plain"); }
|
|
1012
1089
|
const box = this.$.captions;
|