@artooi/ag-ui-web-component 0.33.0 → 0.34.0
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/CHANGELOG.md +95 -1
- package/README.md +47 -3
- package/dist/ag-ui-web-component.bundle.js +119 -47
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.js +449 -100
- package/dist/index.js.map +4 -4
- package/dist/ui/chart_block.d.ts +18 -0
- package/dist/ui/chart_block.d.ts.map +1 -1
- package/dist/ui/clamp_panel.d.ts +13 -0
- package/dist/ui/clamp_panel.d.ts.map +1 -0
- package/dist/ui/launcher_placement.d.ts.map +1 -1
- package/dist/ui/panel_drag.d.ts +40 -0
- package/dist/ui/panel_drag.d.ts.map +1 -0
- package/dist/ui/place_widget.d.ts +23 -0
- package/dist/ui/place_widget.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +8 -0
- package/src/core/ag_ui_chat.ts +207 -9
- package/src/ui/chart_block.ts +222 -57
- package/src/ui/clamp_panel.ts +24 -0
- package/src/ui/launcher_placement.ts +18 -54
- package/src/ui/panel_drag.ts +134 -0
- package/src/ui/place_widget.ts +56 -0
- package/src/ui/styles.ts +75 -3
- package/src/version.ts +1 -1
package/src/constants.ts
CHANGED
|
@@ -380,6 +380,14 @@ export const ICON_FILE_TEXT = `<svg class="glyph" viewBox="0 0 24 24" aria-hidde
|
|
|
380
380
|
*/
|
|
381
381
|
export const CHART_ACTIVITY_TYPE = "chart";
|
|
382
382
|
|
|
383
|
+
/**
|
|
384
|
+
* The gutter a floating panel keeps from the viewport edge.
|
|
385
|
+
*
|
|
386
|
+
* It matches the default `--ag-ui-inset`, so an undragged widget resolves to
|
|
387
|
+
* exactly the placement it already had. Changing it moves every clamped panel.
|
|
388
|
+
*/
|
|
389
|
+
export const EDGE_MARGIN = 24;
|
|
390
|
+
|
|
383
391
|
/**
|
|
384
392
|
* How long a screen-reader status stays in the announcer before it is emptied.
|
|
385
393
|
*
|
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -58,6 +58,7 @@ import { chartSpecFrom } from "../ui/chart_spec_from.js";
|
|
|
58
58
|
import { CHART_TOOL_NAME, createChartTool } from "../ui/chart_tool.js";
|
|
59
59
|
import { CheckpointMenu, type CheckpointVerb } from "../ui/checkpoint_menu.js";
|
|
60
60
|
import { clampLauncher } from "../ui/clamp_launcher.js";
|
|
61
|
+
import { clampPanel } from "../ui/clamp_panel.js";
|
|
61
62
|
import { type ConfirmationRequest, requestConfirmation } from "../ui/confirmation_card.js";
|
|
62
63
|
import { copyPayload } from "../ui/copy_payload.js";
|
|
63
64
|
import { enableLauncherDrag } from "../ui/launcher_drag.js";
|
|
@@ -73,6 +74,8 @@ import {
|
|
|
73
74
|
messageActionButton,
|
|
74
75
|
} from "../ui/message_actions.js";
|
|
75
76
|
import { attachQuoteOffer, type PageQuoteOffer } from "../ui/page_quote_offer.js";
|
|
77
|
+
import { enablePanelDrag } from "../ui/panel_drag.js";
|
|
78
|
+
import { placeWidget } from "../ui/place_widget.js";
|
|
76
79
|
import { prettifyToolName } from "../ui/prettify_tool_name.js";
|
|
77
80
|
import {
|
|
78
81
|
type QuestionRenderer,
|
|
@@ -316,6 +319,15 @@ const THEME_KEY = "ag-ui-chat:theme";
|
|
|
316
319
|
/** Per-tab persistence key for a dragged launcher position. */
|
|
317
320
|
const LAUNCHER_KEY = "ag-ui-chat:launcher";
|
|
318
321
|
|
|
322
|
+
/** A stored `{ left, top }` pair, or null for anything that is not one. */
|
|
323
|
+
function asPoint(value: unknown): { readonly left: number; readonly top: number } | null {
|
|
324
|
+
if (typeof value !== "object" || value === null) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const { left, top } = value as { left?: unknown; top?: unknown };
|
|
328
|
+
return typeof left === "number" && typeof top === "number" ? { left, top } : null;
|
|
329
|
+
}
|
|
330
|
+
|
|
319
331
|
/**
|
|
320
332
|
* Placements whose launcher can be dragged. The rest have nowhere to put it:
|
|
321
333
|
* a sidebar collapses to a full-height edge rail, "embedded" and "page" hide
|
|
@@ -818,6 +830,19 @@ export class AgUiChat extends HTMLElement {
|
|
|
818
830
|
*/
|
|
819
831
|
#launcherPos: { readonly left: number; readonly top: number } | null = null;
|
|
820
832
|
|
|
833
|
+
/**
|
|
834
|
+
* Where the user dragged the *panel*, in viewport coordinates, or null while
|
|
835
|
+
* its position is still derived from the launcher's.
|
|
836
|
+
*
|
|
837
|
+
* The two gestures state different things and are restored differently. A
|
|
838
|
+
* launcher drag says where the bubble goes and leaves the panel to open into
|
|
839
|
+
* whatever space the viewport has, so it is re-derived every time -- which is
|
|
840
|
+
* what lets a widget re-decide its direction when the window changes under
|
|
841
|
+
* it. A header drag states the panel's own position, and re-deriving that
|
|
842
|
+
* from the launcher would move the panel the user just placed.
|
|
843
|
+
*/
|
|
844
|
+
#panelPos: { readonly left: number; readonly top: number } | null = null;
|
|
845
|
+
|
|
821
846
|
/**
|
|
822
847
|
* The corner the panel opens away from, once this element is placing itself.
|
|
823
848
|
* Null means the host's layout still decides, and the anchor is measured.
|
|
@@ -2444,6 +2469,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
2444
2469
|
return;
|
|
2445
2470
|
}
|
|
2446
2471
|
this.#launcherPos = at;
|
|
2472
|
+
// Dropping the bubble hands the panel's position back to the placement.
|
|
2473
|
+
// Keeping a stated one would pin the panel where it was dragged and leave
|
|
2474
|
+
// the launcher deriving nothing, which is the gesture doing half its job.
|
|
2475
|
+
this.#panelPos = null;
|
|
2447
2476
|
// The host box keeps its expanded size while collapsed, so its own rect is
|
|
2448
2477
|
// the panel's size in either state and needs no separate bookkeeping.
|
|
2449
2478
|
const panel = this.getBoundingClientRect();
|
|
@@ -2471,13 +2500,146 @@ export class AgUiChat extends HTMLElement {
|
|
|
2471
2500
|
this.#storeLauncherPosition();
|
|
2472
2501
|
}
|
|
2473
2502
|
|
|
2474
|
-
/**
|
|
2503
|
+
/**
|
|
2504
|
+
* Move the panel live during a header drag, without persisting.
|
|
2505
|
+
*
|
|
2506
|
+
* Only the host box is written, and that is the whole trick: the launcher is
|
|
2507
|
+
* positioned *inside* that box, so leaving its own inset alone carries it
|
|
2508
|
+
* along by exactly the distance the panel travelled -- which is what a person
|
|
2509
|
+
* dragging a window expects of the thing it collapses into. Placing it on the
|
|
2510
|
+
* panel's pinned corner instead, as an earlier version did, sent it leaping
|
|
2511
|
+
* across the panel the moment the drag re-picked that corner.
|
|
2512
|
+
*
|
|
2513
|
+
* The corner is therefore held for the length of the gesture. Both insets are
|
|
2514
|
+
* measured from it, and rewriting one of them from a new corner while the
|
|
2515
|
+
* other still names the old one would move the launcher for no reason.
|
|
2516
|
+
*/
|
|
2517
|
+
#movePanel(box: PanelRect): PanelRect {
|
|
2518
|
+
if (!this.#launcherDraggable()) {
|
|
2519
|
+
return box;
|
|
2520
|
+
}
|
|
2521
|
+
// Where the launcher rests, recorded before the first move writes anything.
|
|
2522
|
+
// From here on the DOM shows it mid-gesture, so this is the last moment it
|
|
2523
|
+
// can be read rather than derived.
|
|
2524
|
+
if (this.#launcherPos === null) {
|
|
2525
|
+
const resting = this.#launcherBox();
|
|
2526
|
+
this.#launcherPos = { left: resting.left, top: resting.top };
|
|
2527
|
+
}
|
|
2528
|
+
const held = clampPanel(box, this.#viewport());
|
|
2529
|
+
const corner = this.#expandCorner ?? this.#anchor;
|
|
2530
|
+
const viewport = this.#viewport();
|
|
2531
|
+
this.style.setProperty(
|
|
2532
|
+
"--ag-ui-inset",
|
|
2533
|
+
[
|
|
2534
|
+
corner.y === "top" ? `${Math.round(held.top)}px` : "auto",
|
|
2535
|
+
corner.x === "right" ? `${Math.round(viewport.width - held.right)}px` : "auto",
|
|
2536
|
+
corner.y === "bottom" ? `${Math.round(viewport.height - held.bottom)}px` : "auto",
|
|
2537
|
+
corner.x === "left" ? `${Math.round(held.left)}px` : "auto",
|
|
2538
|
+
].join(" "),
|
|
2539
|
+
);
|
|
2540
|
+
this.#panelPos = { left: held.left, top: held.top };
|
|
2541
|
+
return held;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/**
|
|
2545
|
+
* Finish a header drag: settle where both halves ended up, and remember it.
|
|
2546
|
+
*
|
|
2547
|
+
* The launcher travels the distance the panel actually travelled, which is
|
|
2548
|
+
* the clamped distance rather than the pointer's -- a panel held against the
|
|
2549
|
+
* viewport margin stops, and so does the bubble attached to it. Measured from
|
|
2550
|
+
* the box the press started on, so a long drag cannot accumulate the rounding
|
|
2551
|
+
* each move writes into the inset.
|
|
2552
|
+
*
|
|
2553
|
+
* Only now is the corner re-picked, from where the launcher has ended up, so
|
|
2554
|
+
* the panel opens into clear space next time. Re-picking it moves nothing:
|
|
2555
|
+
* both insets are rewritten from positions that are already decided.
|
|
2556
|
+
*/
|
|
2557
|
+
#commitPanel(box: PanelRect, from: PanelRect): void {
|
|
2558
|
+
const held = this.#movePanel(box);
|
|
2559
|
+
const start = this.#launcherPos;
|
|
2560
|
+
if (!this.#launcherDraggable() || start === null) {
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
const carried = {
|
|
2564
|
+
...this.#launcherBox(),
|
|
2565
|
+
left: start.left + (held.left - from.left),
|
|
2566
|
+
top: start.top + (held.top - from.top),
|
|
2567
|
+
};
|
|
2568
|
+
// The screen is the last word: a bubble dragged past the edge is one nobody
|
|
2569
|
+
// can click, and it is the only way back to a collapsed conversation.
|
|
2570
|
+
this.#placePanelAndLauncher(held, {
|
|
2571
|
+
...carried,
|
|
2572
|
+
...clampLauncher(carried, this.#viewport()),
|
|
2573
|
+
});
|
|
2574
|
+
this.#storeLauncherPosition();
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
/**
|
|
2578
|
+
* Write both insets for a panel and launcher that are already positioned,
|
|
2579
|
+
* re-picking the corner they are measured from.
|
|
2580
|
+
*/
|
|
2581
|
+
#placePanelAndLauncher(host: PanelRect, launcher: LauncherBox): void {
|
|
2582
|
+
const viewport = this.#viewport();
|
|
2583
|
+
const size = { width: host.right - host.left, height: host.bottom - host.top };
|
|
2584
|
+
const { corner } = launcherPlacement(launcher, size, viewport);
|
|
2585
|
+
const insets = placeWidget(host, launcher, corner, viewport);
|
|
2586
|
+
this.style.setProperty("--ag-ui-inset", insets.hostInset);
|
|
2587
|
+
this.style.setProperty("--ag-ui-launcher-inset", insets.launcherInset);
|
|
2588
|
+
this.#launcherPos = { left: launcher.left, top: launcher.top };
|
|
2589
|
+
this.#panelPos = { left: host.left, top: host.top };
|
|
2590
|
+
this.#expandCorner = corner;
|
|
2591
|
+
this.setAttribute("data-expand-corner", `${corner.y}-${corner.x}`);
|
|
2592
|
+
this.#syncResizeAnchor();
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
/**
|
|
2596
|
+
* Re-apply a panel position the user stated, against the current viewport.
|
|
2597
|
+
*
|
|
2598
|
+
* The launcher keeps its offset from the panel through the clamp -- it was
|
|
2599
|
+
* put where it is relative to the panel, and a viewport that has since shrunk
|
|
2600
|
+
* is no reason to move one without the other -- and is then held on screen in
|
|
2601
|
+
* its own right.
|
|
2602
|
+
*/
|
|
2603
|
+
#restorePanelPosition(at: { readonly left: number; readonly top: number }): void {
|
|
2604
|
+
if (!this.#launcherDraggable()) {
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
const rect = this.getBoundingClientRect();
|
|
2608
|
+
const held = clampPanel(
|
|
2609
|
+
{ left: at.left, top: at.top, right: at.left + rect.width, bottom: at.top + rect.height },
|
|
2610
|
+
this.#viewport(),
|
|
2611
|
+
);
|
|
2612
|
+
const launcher = this.#launcherBox();
|
|
2613
|
+
const carried = {
|
|
2614
|
+
...launcher,
|
|
2615
|
+
left: launcher.left + (held.left - at.left),
|
|
2616
|
+
top: launcher.top + (held.top - at.top),
|
|
2617
|
+
};
|
|
2618
|
+
this.#placePanelAndLauncher(held, {
|
|
2619
|
+
...carried,
|
|
2620
|
+
...clampLauncher(carried, this.#viewport()),
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
/**
|
|
2625
|
+
* Write the current position, if this element owns one.
|
|
2626
|
+
*
|
|
2627
|
+
* The panel's own position rides along only when the user stated it, because
|
|
2628
|
+
* its presence is what tells a restore which of the two gestures to honour:
|
|
2629
|
+
* with it, the panel goes back where it was put; without it, the panel is
|
|
2630
|
+
* re-derived from the launcher and opens into whatever room the viewport has
|
|
2631
|
+
* now.
|
|
2632
|
+
*/
|
|
2475
2633
|
#storeLauncherPosition(): void {
|
|
2476
2634
|
const position = this.#launcherPos;
|
|
2477
2635
|
if (position === null) {
|
|
2478
2636
|
return;
|
|
2479
2637
|
}
|
|
2480
|
-
|
|
2638
|
+
const panel = this.#panelPos;
|
|
2639
|
+
writeStoredItem(
|
|
2640
|
+
this.#storageKey(LAUNCHER_KEY),
|
|
2641
|
+
JSON.stringify(panel === null ? position : { ...position, panel }),
|
|
2642
|
+
);
|
|
2481
2643
|
}
|
|
2482
2644
|
|
|
2483
2645
|
/**
|
|
@@ -2487,18 +2649,32 @@ export class AgUiChat extends HTMLElement {
|
|
|
2487
2649
|
* unreachable -- it is the only way back to a collapsed conversation.
|
|
2488
2650
|
*/
|
|
2489
2651
|
#restoreLauncherPosition(): void {
|
|
2490
|
-
const stored = this.#
|
|
2491
|
-
|
|
2652
|
+
const stored = this.#readLauncherPosition();
|
|
2653
|
+
const launcher = this.#launcherPos ?? stored;
|
|
2654
|
+
if (launcher === null) {
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
const panel = this.#panelPos ?? stored?.panel ?? null;
|
|
2658
|
+
if (panel !== null) {
|
|
2659
|
+
// Stated rather than derived: the launcher is only read here to keep the
|
|
2660
|
+
// offset the two were left with, so it has to be seeded before the panel
|
|
2661
|
+
// is placed around it.
|
|
2662
|
+
this.#launcherPos = { left: launcher.left, top: launcher.top };
|
|
2663
|
+
this.#restorePanelPosition(panel);
|
|
2492
2664
|
return;
|
|
2493
2665
|
}
|
|
2494
2666
|
const box = this.#launcherBox();
|
|
2495
2667
|
this.#applyLauncherPlacement(
|
|
2496
|
-
clampLauncher({ ...box, left:
|
|
2668
|
+
clampLauncher({ ...box, left: launcher.left, top: launcher.top }, this.#viewport()),
|
|
2497
2669
|
);
|
|
2498
2670
|
}
|
|
2499
2671
|
|
|
2500
|
-
/** The persisted
|
|
2501
|
-
#readLauncherPosition(): {
|
|
2672
|
+
/** The persisted position for this instance, or null. */
|
|
2673
|
+
#readLauncherPosition(): {
|
|
2674
|
+
readonly left: number;
|
|
2675
|
+
readonly top: number;
|
|
2676
|
+
readonly panel?: { readonly left: number; readonly top: number };
|
|
2677
|
+
} | null {
|
|
2502
2678
|
const raw = this.#readScopedItem(LAUNCHER_KEY);
|
|
2503
2679
|
if (raw === null) {
|
|
2504
2680
|
return null;
|
|
@@ -2508,8 +2684,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
2508
2684
|
if (typeof parsed !== "object" || parsed === null) {
|
|
2509
2685
|
return null;
|
|
2510
2686
|
}
|
|
2511
|
-
const { left, top } = parsed as { left?: unknown; top?: unknown };
|
|
2512
|
-
|
|
2687
|
+
const { left, top, panel } = parsed as { left?: unknown; top?: unknown; panel?: unknown };
|
|
2688
|
+
if (typeof left !== "number" || typeof top !== "number") {
|
|
2689
|
+
return null;
|
|
2690
|
+
}
|
|
2691
|
+
// A record written before the panel could be dragged has no panel half,
|
|
2692
|
+
// and one written by a launcher drag never will -- both restore by
|
|
2693
|
+
// deriving the panel, which is what they meant.
|
|
2694
|
+
const at = asPoint(panel);
|
|
2695
|
+
return at === null ? { left, top } : { left, top, panel: at };
|
|
2513
2696
|
} catch {
|
|
2514
2697
|
// A corrupt entry is not worth failing a mount over; fall back to the
|
|
2515
2698
|
// placement's own corner.
|
|
@@ -2527,6 +2710,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2527
2710
|
return;
|
|
2528
2711
|
}
|
|
2529
2712
|
this.#launcherPos = null;
|
|
2713
|
+
this.#panelPos = null;
|
|
2530
2714
|
this.#expandCorner = null;
|
|
2531
2715
|
this.style.removeProperty("--ag-ui-inset");
|
|
2532
2716
|
this.style.removeProperty("--ag-ui-launcher-inset");
|
|
@@ -2575,6 +2759,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
2575
2759
|
top: anchor.y === "top" ? box.top : box.bottom - size,
|
|
2576
2760
|
};
|
|
2577
2761
|
}
|
|
2762
|
+
// A stated panel position is a claim about this box, so it moves with it.
|
|
2763
|
+
if (this.#panelPos !== null) {
|
|
2764
|
+
this.#panelPos = { left: box.left, top: box.top };
|
|
2765
|
+
}
|
|
2578
2766
|
}
|
|
2579
2767
|
|
|
2580
2768
|
/** Finish a resize: keep the box, remember it, and re-read the pinned edges. */
|
|
@@ -3289,6 +3477,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
3289
3477
|
controls.append(collapse);
|
|
3290
3478
|
header.append(title, headerActions, controls);
|
|
3291
3479
|
|
|
3480
|
+
// A panel is a window and a header is its title bar. Only while open: a
|
|
3481
|
+
// collapsed widget has no header on screen, and the launcher is the handle
|
|
3482
|
+
// then.
|
|
3483
|
+
enablePanelDrag(header, {
|
|
3484
|
+
enabled: () => !this.collapsed && this.#launcherDraggable(),
|
|
3485
|
+
rect: () => this.getBoundingClientRect(),
|
|
3486
|
+
apply: (box) => this.#movePanel(box),
|
|
3487
|
+
commit: (box, from) => this.#commitPanel(box, from),
|
|
3488
|
+
});
|
|
3489
|
+
|
|
3292
3490
|
this.#messages.className = "messages";
|
|
3293
3491
|
this.#messages.setAttribute("part", "messages");
|
|
3294
3492
|
this.#messages.setAttribute("role", "log");
|