@artooi/ag-ui-web-component 0.21.0 → 0.22.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 +81 -1
- package/README.md +127 -12
- package/dist/ag-ui-web-component.bundle.js +404 -99
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +29 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +11 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +550 -106
- package/dist/index.js.map +2 -2
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +8 -1
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +35 -0
- package/src/core/ag_ui_chat.ts +197 -32
- package/src/index.ts +2 -0
- package/src/ui/styles.ts +383 -78
- package/src/ui/ui_strings.ts +9 -1
- package/src/ui/voice_input.ts +7 -1
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
var ELEMENT_TAG = "ag-ui-chat";
|
|
3
3
|
var SUBMIT_EVENT = "ag-ui-submit";
|
|
4
4
|
var TOGGLE_EVENT = "ag-ui-toggle";
|
|
5
|
+
var UNREAD_EVENT = "ag-ui-unread";
|
|
5
6
|
var STATE_EVENT = "ag-ui-state";
|
|
6
7
|
var ATTACHMENT_EVENT = "ag-ui-attachments";
|
|
7
8
|
var MESSAGE_ROLE = {
|
|
@@ -34,6 +35,11 @@ var TOOL_DISPLAY = {
|
|
|
34
35
|
};
|
|
35
36
|
var COMPACTION_ACTIVITY_TYPE = "compaction";
|
|
36
37
|
var LOAD_CAPABILITY_TOOL = "load_capability";
|
|
38
|
+
var ICON_SEND = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19.5V5m-6.5 6.5L12 5l6.5 6.5"/></svg>`;
|
|
39
|
+
var ICON_STOP = `<svg class="glyph glyph--solid" viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="2.5"/></svg>`;
|
|
40
|
+
var ICON_ATTACH = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M17 8.5V15a5 5 0 0 1-10 0V7a3 3 0 0 1 6 0v7.5a1 1 0 0 1-2 0V8.5"/></svg>`;
|
|
41
|
+
var ICON_VOICE = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0V7a3 3 0 0 1 3-3z"/><path d="M5 11v1a7 7 0 0 0 14 0v-1"/><path d="M12 19v3"/></svg>`;
|
|
42
|
+
var ICON_LAUNCHER = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v8a2.5 2.5 0 0 1-2.5 2.5H9l-5 4z"/></svg>`;
|
|
37
43
|
|
|
38
44
|
// src/skills/fill_template.ts
|
|
39
45
|
var PLACEHOLDER_RE = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
@@ -509,6 +515,7 @@ var DEFAULT_UI_STRINGS = {
|
|
|
509
515
|
newChat: "New chat",
|
|
510
516
|
collapse: "Collapse",
|
|
511
517
|
expand: "Expand",
|
|
518
|
+
expandUnread: "Expand \u2014 {count} unread",
|
|
512
519
|
toggleTheme: "Toggle theme",
|
|
513
520
|
copyCode: "Copy",
|
|
514
521
|
copied: "Copied",
|
|
@@ -4532,6 +4539,36 @@ var STYLES = `
|
|
|
4532
4539
|
--_icon-size: var(--ag-ui-icon-size, 22px);
|
|
4533
4540
|
--_icon-radius: var(--ag-ui-icon-radius, 4px);
|
|
4534
4541
|
|
|
4542
|
+
/* Composer \u2014 one bordered surface holding the field and its tool row. */
|
|
4543
|
+
--_composer-radius: var(--ag-ui-composer-radius, 14px);
|
|
4544
|
+
--_composer-max-height: var(--ag-ui-composer-max-height, 40vh);
|
|
4545
|
+
--_tool-btn-size: var(--ag-ui-tool-btn-size, 30px);
|
|
4546
|
+
--_send-size: var(--ag-ui-send-size, 30px);
|
|
4547
|
+
--_glyph-size: var(--ag-ui-glyph-size, 18px);
|
|
4548
|
+
--_glyph-stroke: var(--ag-ui-glyph-stroke, 1.75);
|
|
4549
|
+
|
|
4550
|
+
/* Unread badge on the launcher. */
|
|
4551
|
+
--_badge-bg: var(--ag-ui-badge-bg, var(--_danger));
|
|
4552
|
+
--_badge-fg: var(--ag-ui-badge-fg, #ffffff);
|
|
4553
|
+
--_badge-size: var(--ag-ui-badge-size, 18px);
|
|
4554
|
+
--_badge-font-size: var(--ag-ui-badge-font-size, 11px);
|
|
4555
|
+
|
|
4556
|
+
/* The floating launcher a collapsed widget shrinks to. */
|
|
4557
|
+
--_launcher-size: var(--ag-ui-launcher-size, 56px);
|
|
4558
|
+
--_launcher-bg: var(--ag-ui-launcher-bg, var(--_header-bg));
|
|
4559
|
+
--_launcher-fg: var(--ag-ui-launcher-fg, var(--_header-fg));
|
|
4560
|
+
--_launcher-radius: var(--ag-ui-launcher-radius, 50%);
|
|
4561
|
+
--_launcher-icon-size: var(--ag-ui-launcher-icon-size, 26px);
|
|
4562
|
+
--_launcher-inset: var(--ag-ui-launcher-inset, auto 0 0 auto);
|
|
4563
|
+
|
|
4564
|
+
/* Motion. One duration and two curves drive every collapse, expand and
|
|
4565
|
+
slide-over, so the whole widget accelerates and settles as one thing.
|
|
4566
|
+
The default curve is a long-tailed ease-out (a fast start that decelerates
|
|
4567
|
+
into place); the pop curve overshoots slightly, for something arriving. */
|
|
4568
|
+
--_motion: var(--ag-ui-motion, 0.28s);
|
|
4569
|
+
--_ease: var(--ag-ui-ease, cubic-bezier(0.32, 0.72, 0, 1));
|
|
4570
|
+
--_ease-pop: var(--ag-ui-ease-pop, cubic-bezier(0.34, 1.36, 0.64, 1));
|
|
4571
|
+
|
|
4535
4572
|
/* Spacing \u2014 the density preset overrides these. */
|
|
4536
4573
|
--_space: var(--ag-ui-space, 10px);
|
|
4537
4574
|
--_pad: var(--ag-ui-pad, 16px);
|
|
@@ -4696,56 +4733,161 @@ var STYLES = `
|
|
|
4696
4733
|
--_height: var(--ag-ui-height, 100vh);
|
|
4697
4734
|
--_max-height: var(--ag-ui-max-height, 100vh);
|
|
4698
4735
|
--_radius: var(--ag-ui-radius, 0);
|
|
4699
|
-
transition: width
|
|
4736
|
+
transition: width var(--_motion) var(--_ease);
|
|
4700
4737
|
}
|
|
4701
4738
|
|
|
4702
4739
|
:host([placement="sidebar"][data-side="left"]) {
|
|
4703
4740
|
--_inset: var(--ag-ui-inset, 0 auto 0 0);
|
|
4704
4741
|
}
|
|
4705
4742
|
|
|
4743
|
+
/* The docked panel is pinned to the edge it docks against rather than filling
|
|
4744
|
+
the host as a flex child. Collapsing shrinks the host to the rail width, and
|
|
4745
|
+
a flex child would be squashed to 52px on the way out instead of sliding out
|
|
4746
|
+
at full width. */
|
|
4706
4747
|
:host([placement="sidebar"]) .chat {
|
|
4707
|
-
|
|
4748
|
+
position: absolute;
|
|
4749
|
+
inset: 0 0 0 auto;
|
|
4750
|
+
width: var(--_width);
|
|
4751
|
+
transform-origin: right center;
|
|
4708
4752
|
}
|
|
4709
4753
|
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4754
|
+
:host([placement="sidebar"][data-side="left"]) .chat {
|
|
4755
|
+
inset: 0 auto 0 0;
|
|
4756
|
+
transform-origin: left center;
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4759
|
+
/* Collapsed sidebar: shrink the host to the rail width and slide the panel out
|
|
4760
|
+
through the edge it docks against. Higher specificity than the generic
|
|
4761
|
+
collapse rules, so it wins regardless of source order. */
|
|
4713
4762
|
:host([placement="sidebar"][collapsed]) {
|
|
4714
4763
|
width: var(--_rail-width);
|
|
4715
4764
|
height: 100vh;
|
|
4716
4765
|
max-height: 100vh;
|
|
4717
4766
|
bottom: 0;
|
|
4767
|
+
pointer-events: auto;
|
|
4718
4768
|
}
|
|
4719
4769
|
|
|
4720
4770
|
:host([placement="sidebar"][collapsed]) .chat {
|
|
4721
|
-
|
|
4771
|
+
transform: translateX(100%);
|
|
4722
4772
|
}
|
|
4723
4773
|
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4774
|
+
:host([placement="sidebar"][data-side="left"][collapsed]) .chat {
|
|
4775
|
+
transform: translateX(-100%);
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4779
|
+
:host([placement="sidebar"]),
|
|
4780
|
+
:host([placement="sidebar"]) .chat {
|
|
4781
|
+
transition: none;
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
/* \u2500\u2500 Launcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4786
|
+
What a collapsed widget shrinks to: a round floating button by default, the
|
|
4787
|
+
sidebar's slim edge rail under that placement. A sibling of the panel, so it
|
|
4788
|
+
survives the panel being hidden, and the only part of a collapsed widget
|
|
4789
|
+
that takes pointer events. */
|
|
4790
|
+
.launcher {
|
|
4791
|
+
display: flex;
|
|
4792
|
+
position: absolute;
|
|
4793
|
+
inset: var(--_launcher-inset);
|
|
4794
|
+
align-items: center;
|
|
4795
|
+
justify-content: center;
|
|
4796
|
+
width: var(--_launcher-size);
|
|
4797
|
+
height: var(--_launcher-size);
|
|
4798
|
+
padding: 0;
|
|
4728
4799
|
border: none;
|
|
4800
|
+
border-radius: var(--_launcher-radius);
|
|
4801
|
+
background: var(--_launcher-bg);
|
|
4802
|
+
color: var(--_launcher-fg);
|
|
4803
|
+
box-shadow: var(--_shadow);
|
|
4729
4804
|
font: inherit;
|
|
4805
|
+
cursor: pointer;
|
|
4806
|
+
pointer-events: auto;
|
|
4807
|
+
opacity: 0;
|
|
4808
|
+
transform: scale(0.4);
|
|
4809
|
+
visibility: hidden;
|
|
4810
|
+
transition:
|
|
4811
|
+
opacity var(--_motion) var(--_ease),
|
|
4812
|
+
transform var(--_motion) var(--_ease-pop),
|
|
4813
|
+
visibility var(--_motion) var(--_ease);
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4816
|
+
/* The launcher grows out of the corner the panel shrank into.
|
|
4817
|
+
It is laid out at rest rather than display:none, which is what lets it
|
|
4818
|
+
animate in *and* out: an element that was not rendered has no before-change
|
|
4819
|
+
style to transition from, and one whose display flips to none cannot
|
|
4820
|
+
transition at all. visibility keeps it unpaintable, untabbable and
|
|
4821
|
+
unclickable in between -- the expanded panel's own controls sit under it and
|
|
4822
|
+
must stay reachable. */
|
|
4823
|
+
:host([collapsed]) .launcher {
|
|
4824
|
+
opacity: 1;
|
|
4825
|
+
transform: none;
|
|
4826
|
+
visibility: visible;
|
|
4730
4827
|
}
|
|
4731
4828
|
|
|
4732
|
-
:host([
|
|
4733
|
-
|
|
4829
|
+
:host([collapsed]) .launcher:hover {
|
|
4830
|
+
transform: scale(1.06);
|
|
4831
|
+
}
|
|
4832
|
+
|
|
4833
|
+
:host([collapsed]) .launcher:active {
|
|
4834
|
+
transform: scale(0.94);
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
.launcher .icon-holder {
|
|
4838
|
+
width: var(--_launcher-icon-size);
|
|
4839
|
+
height: var(--_launcher-icon-size);
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
/* The unread badge rides the launcher's top-right corner. The ring in the
|
|
4843
|
+
widget's own background is what separates it from the launcher underneath,
|
|
4844
|
+
whatever colour the host themes either one. */
|
|
4845
|
+
.launcher-badge {
|
|
4734
4846
|
position: absolute;
|
|
4847
|
+
top: -2px;
|
|
4848
|
+
right: -2px;
|
|
4849
|
+
display: inline-flex;
|
|
4850
|
+
align-items: center;
|
|
4851
|
+
justify-content: center;
|
|
4852
|
+
box-sizing: border-box;
|
|
4853
|
+
min-width: var(--_badge-size);
|
|
4854
|
+
height: var(--_badge-size);
|
|
4855
|
+
padding: 0 5px;
|
|
4856
|
+
border-radius: 999px;
|
|
4857
|
+
background: var(--_badge-bg);
|
|
4858
|
+
color: var(--_badge-fg);
|
|
4859
|
+
font-size: var(--_badge-font-size);
|
|
4860
|
+
font-weight: 600;
|
|
4861
|
+
line-height: 1;
|
|
4862
|
+
box-shadow: 0 0 0 2px var(--_bg);
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
.launcher-badge[hidden] {
|
|
4866
|
+
display: none;
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
/* The sidebar collapses to an edge rail instead: full height, square, flush
|
|
4870
|
+
against the dock. It slides in with the panel rather than popping. */
|
|
4871
|
+
:host([placement="sidebar"][collapsed]) .launcher {
|
|
4735
4872
|
inset: 0;
|
|
4873
|
+
width: auto;
|
|
4874
|
+
height: auto;
|
|
4736
4875
|
align-items: flex-start;
|
|
4737
|
-
justify-content: center;
|
|
4738
4876
|
padding-top: 16px;
|
|
4739
4877
|
border: 1px solid var(--_border);
|
|
4878
|
+
border-radius: 0;
|
|
4740
4879
|
background: var(--_header-bg);
|
|
4741
4880
|
color: var(--_header-fg);
|
|
4742
|
-
|
|
4881
|
+
box-shadow: none;
|
|
4882
|
+
transform: none;
|
|
4743
4883
|
}
|
|
4744
4884
|
|
|
4885
|
+
/* Every transition in this file is timed by --_motion, so collapsing it to a
|
|
4886
|
+
frame is the whole reduced-motion story: states still change (and display
|
|
4887
|
+
still flips at the end of its discrete transition), nothing travels. */
|
|
4745
4888
|
@media (prefers-reduced-motion: reduce) {
|
|
4746
|
-
:host
|
|
4747
|
-
|
|
4748
|
-
transition: none;
|
|
4889
|
+
:host {
|
|
4890
|
+
--_motion: var(--ag-ui-motion, 0.001s);
|
|
4749
4891
|
}
|
|
4750
4892
|
}
|
|
4751
4893
|
|
|
@@ -4836,27 +4978,78 @@ var STYLES = `
|
|
|
4836
4978
|
background: rgba(255, 255, 255, 0.18);
|
|
4837
4979
|
}
|
|
4838
4980
|
|
|
4839
|
-
/*
|
|
4840
|
-
|
|
4981
|
+
/* \u2500\u2500 Collapse \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4982
|
+
Collapsing shrinks the widget to the round floating launcher: the panel
|
|
4983
|
+
scales down toward the launcher's corner and fades out, the launcher pops in
|
|
4984
|
+
from that same point. Both halves are transform and opacity only, so the
|
|
4985
|
+
morph runs on the compositor and never reflows the host page.
|
|
4986
|
+
|
|
4987
|
+
The host box keeps its expanded size. Animating it would animate layout, and
|
|
4988
|
+
a dragged --ag-ui-width would then fight the launcher's own size. Nothing
|
|
4989
|
+
paints there once the panel is gone, so the box only has to stop swallowing
|
|
4990
|
+
clicks: pointer events go to none, and the launcher takes them back.
|
|
4991
|
+
|
|
4992
|
+
Two placements collapse to something else and restore what they need:
|
|
4993
|
+
"sidebar" slides to its rail (below), while "embedded" and "page" keep the
|
|
4994
|
+
original header bar \u2014 embedded is laid out by the host page, where a
|
|
4995
|
+
floating circle would escape the layout, and page is a full-screen route
|
|
4996
|
+
with no corner to float in. */
|
|
4841
4997
|
:host([collapsed]) {
|
|
4998
|
+
pointer-events: none;
|
|
4999
|
+
}
|
|
5000
|
+
|
|
5001
|
+
:host([collapsed]) .chat {
|
|
5002
|
+
opacity: 0;
|
|
5003
|
+
transform: scale(0.94);
|
|
5004
|
+
visibility: hidden;
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
/* visibility is what keeps the panel out of the tab order and the a11y tree at
|
|
5008
|
+
rest without display:none killing the transition. It interpolates so that any
|
|
5009
|
+
progress below 1 still counts as visible: the panel stays on screen for the
|
|
5010
|
+
whole collapse and flips hidden exactly at the end, and on expand it is
|
|
5011
|
+
visible from the first frame. */
|
|
5012
|
+
.chat {
|
|
5013
|
+
transform-origin: bottom right;
|
|
5014
|
+
transition:
|
|
5015
|
+
opacity var(--_motion) var(--_ease),
|
|
5016
|
+
transform var(--_motion) var(--_ease),
|
|
5017
|
+
visibility var(--_motion) var(--_ease);
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
:host([placement="bottom-left"]) .chat {
|
|
5021
|
+
transform-origin: bottom left;
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
/* The two in-flow placements keep the original collapse: hide the body, let
|
|
5025
|
+
the host shrink to the header bar. */
|
|
5026
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) {
|
|
4842
5027
|
height: auto;
|
|
4843
5028
|
max-height: none;
|
|
5029
|
+
pointer-events: auto;
|
|
4844
5030
|
}
|
|
4845
5031
|
|
|
4846
|
-
:host([collapsed]) .
|
|
4847
|
-
:
|
|
4848
|
-
:
|
|
4849
|
-
:
|
|
4850
|
-
:host([collapsed]) .skill-hint {
|
|
4851
|
-
display: none;
|
|
5032
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .chat {
|
|
5033
|
+
opacity: 1;
|
|
5034
|
+
transform: none;
|
|
5035
|
+
visibility: visible;
|
|
4852
5036
|
}
|
|
4853
5037
|
|
|
4854
|
-
/*
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
:host([collapsed][placement="
|
|
4859
|
-
|
|
5038
|
+
/* These two keep the header bar, so the launcher must stay out of the way \u2014
|
|
5039
|
+
an embedded host is position: static, which would otherwise leave an
|
|
5040
|
+
absolutely-positioned circle to escape the layout entirely and land against
|
|
5041
|
+
whatever the page happens to position. */
|
|
5042
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .launcher {
|
|
5043
|
+
visibility: hidden;
|
|
5044
|
+
opacity: 0;
|
|
5045
|
+
}
|
|
5046
|
+
|
|
5047
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .messages,
|
|
5048
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .input-row,
|
|
5049
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-chips,
|
|
5050
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-palette,
|
|
5051
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-hint {
|
|
5052
|
+
display: none;
|
|
4860
5053
|
}
|
|
4861
5054
|
|
|
4862
5055
|
.messages {
|
|
@@ -5529,102 +5722,187 @@ var STYLES = `
|
|
|
5529
5722
|
opacity: 0.35;
|
|
5530
5723
|
}
|
|
5531
5724
|
|
|
5725
|
+
/* \u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5726
|
+
One surface owns the border, the background and the focus ring; the field and
|
|
5727
|
+
its tool row sit inside it. The row used to be four siblings stretched to the
|
|
5728
|
+
textarea's height, which gave a paperclip the same visual weight as the field
|
|
5729
|
+
and turned Send into a full-height slab. */
|
|
5532
5730
|
.input-row {
|
|
5533
5731
|
display: flex;
|
|
5534
|
-
gap: 8px;
|
|
5535
5732
|
padding: 12px;
|
|
5536
5733
|
border-top: 1px solid var(--_border);
|
|
5537
5734
|
}
|
|
5538
5735
|
|
|
5539
|
-
.
|
|
5736
|
+
.composer {
|
|
5540
5737
|
flex: 1;
|
|
5541
|
-
|
|
5738
|
+
min-width: 0;
|
|
5739
|
+
display: flex;
|
|
5740
|
+
flex-direction: column;
|
|
5741
|
+
gap: 4px;
|
|
5742
|
+
padding: 6px 6px 6px 10px;
|
|
5542
5743
|
background: var(--_input-bg);
|
|
5543
5744
|
border: 1px solid var(--_border);
|
|
5544
|
-
border-radius:
|
|
5545
|
-
|
|
5745
|
+
border-radius: var(--_composer-radius);
|
|
5746
|
+
transition: border-color var(--_motion) var(--_ease);
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5749
|
+
.composer:focus-within {
|
|
5750
|
+
border-color: var(--_accent);
|
|
5751
|
+
}
|
|
5752
|
+
|
|
5753
|
+
/* The field grows with its content (sized by #autoGrow) up to the ceiling,
|
|
5754
|
+
then scrolls. border-box keeps that measurement stable: with content-box the
|
|
5755
|
+
padding would be added to every scrollHeight read and the field would creep
|
|
5756
|
+
taller on each keystroke. */
|
|
5757
|
+
.input {
|
|
5758
|
+
box-sizing: border-box;
|
|
5759
|
+
resize: none;
|
|
5760
|
+
max-height: var(--_composer-max-height);
|
|
5761
|
+
overflow-y: auto;
|
|
5762
|
+
padding: 6px 4px 2px;
|
|
5763
|
+
background: transparent;
|
|
5764
|
+
border: none;
|
|
5546
5765
|
font: inherit;
|
|
5547
5766
|
color: inherit;
|
|
5548
5767
|
outline: none;
|
|
5549
5768
|
}
|
|
5550
5769
|
|
|
5551
|
-
.
|
|
5552
|
-
|
|
5770
|
+
.composer-tools {
|
|
5771
|
+
display: flex;
|
|
5772
|
+
align-items: center;
|
|
5773
|
+
gap: 2px;
|
|
5553
5774
|
}
|
|
5554
5775
|
|
|
5555
|
-
.
|
|
5776
|
+
/* Icon buttons: quiet at rest, so the field is what the eye lands on. */
|
|
5777
|
+
.attach-btn,
|
|
5778
|
+
.voice-btn {
|
|
5779
|
+
display: inline-flex;
|
|
5780
|
+
align-items: center;
|
|
5781
|
+
justify-content: center;
|
|
5782
|
+
width: var(--_tool-btn-size);
|
|
5783
|
+
height: var(--_tool-btn-size);
|
|
5784
|
+
padding: 0;
|
|
5556
5785
|
border: none;
|
|
5557
5786
|
border-radius: 8px;
|
|
5558
|
-
|
|
5787
|
+
background: transparent;
|
|
5788
|
+
color: var(--_muted);
|
|
5789
|
+
font: inherit;
|
|
5790
|
+
line-height: 1;
|
|
5791
|
+
cursor: pointer;
|
|
5792
|
+
transition:
|
|
5793
|
+
background-color var(--_motion) var(--_ease),
|
|
5794
|
+
color var(--_motion) var(--_ease);
|
|
5795
|
+
}
|
|
5796
|
+
|
|
5797
|
+
.attach-btn:hover,
|
|
5798
|
+
.voice-btn:hover {
|
|
5799
|
+
background: var(--_hover);
|
|
5800
|
+
color: var(--_fg);
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5803
|
+
.attach-btn:disabled,
|
|
5804
|
+
.voice-btn:disabled {
|
|
5805
|
+
cursor: default;
|
|
5806
|
+
opacity: 0.6;
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
/* Send closes the row on the right: a circle, the only filled control in the
|
|
5810
|
+
composer, so "the thing that acts" reads at a glance. */
|
|
5811
|
+
.send {
|
|
5812
|
+
margin-left: auto;
|
|
5813
|
+
display: inline-flex;
|
|
5814
|
+
align-items: center;
|
|
5815
|
+
justify-content: center;
|
|
5816
|
+
flex: none;
|
|
5817
|
+
width: var(--_send-size);
|
|
5818
|
+
height: var(--_send-size);
|
|
5819
|
+
padding: 0;
|
|
5820
|
+
border: none;
|
|
5821
|
+
border-radius: 50%;
|
|
5559
5822
|
background: var(--_accent);
|
|
5560
5823
|
color: #ffffff;
|
|
5561
5824
|
font: inherit;
|
|
5562
|
-
|
|
5825
|
+
line-height: 1;
|
|
5563
5826
|
cursor: pointer;
|
|
5827
|
+
transition:
|
|
5828
|
+
background-color var(--_motion) var(--_ease),
|
|
5829
|
+
transform var(--_motion) var(--_ease-pop);
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
.send:hover {
|
|
5833
|
+
transform: scale(1.08);
|
|
5834
|
+
}
|
|
5835
|
+
|
|
5836
|
+
.send:active {
|
|
5837
|
+
transform: scale(0.92);
|
|
5564
5838
|
}
|
|
5565
5839
|
|
|
5566
5840
|
.send:disabled {
|
|
5567
5841
|
opacity: 0.5;
|
|
5568
5842
|
cursor: default;
|
|
5843
|
+
transform: none;
|
|
5569
5844
|
}
|
|
5570
5845
|
|
|
5571
|
-
/* The composer button doubles as the Stop control while a run is in flight
|
|
5846
|
+
/* The composer button doubles as the Stop control while a run is in flight \u2014
|
|
5847
|
+
same circle, different glyph, so nothing moves when a run starts. */
|
|
5572
5848
|
.send[data-state="running"] {
|
|
5573
5849
|
background: var(--_muted);
|
|
5574
5850
|
}
|
|
5575
5851
|
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
border: 1px solid var(--_border);
|
|
5580
|
-
border-radius: 8px;
|
|
5581
|
-
padding: 0 10px;
|
|
5582
|
-
background: var(--_input-bg);
|
|
5583
|
-
color: inherit;
|
|
5584
|
-
font: inherit;
|
|
5585
|
-
cursor: pointer;
|
|
5586
|
-
}
|
|
5587
|
-
|
|
5588
|
-
.attach-btn:hover {
|
|
5589
|
-
border-color: var(--_accent);
|
|
5852
|
+
.send[data-state="idle"] .send-stop,
|
|
5853
|
+
.send[data-state="running"] .send-send {
|
|
5854
|
+
display: none;
|
|
5590
5855
|
}
|
|
5591
5856
|
|
|
5592
|
-
|
|
5593
|
-
|
|
5857
|
+
/* Glyphs \u2014 one class, painted from the button's own colour so every state
|
|
5858
|
+
(hover, recording, running) carries the icon with it. */
|
|
5859
|
+
.glyph {
|
|
5860
|
+
width: var(--_glyph-size);
|
|
5861
|
+
height: var(--_glyph-size);
|
|
5862
|
+
fill: none;
|
|
5863
|
+
stroke: currentColor;
|
|
5864
|
+
stroke-width: var(--_glyph-stroke);
|
|
5865
|
+
stroke-linecap: round;
|
|
5866
|
+
stroke-linejoin: round;
|
|
5594
5867
|
}
|
|
5595
5868
|
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5869
|
+
.glyph--solid {
|
|
5870
|
+
fill: currentColor;
|
|
5871
|
+
stroke: none;
|
|
5599
5872
|
}
|
|
5600
5873
|
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
color: inherit;
|
|
5607
|
-
font: inherit;
|
|
5608
|
-
cursor: pointer;
|
|
5874
|
+
/* In an icon holder (header brand, launcher) the glyph takes the holder's size
|
|
5875
|
+
rather than the composer's. */
|
|
5876
|
+
.icon-holder .glyph {
|
|
5877
|
+
width: 100%;
|
|
5878
|
+
height: 100%;
|
|
5609
5879
|
}
|
|
5610
5880
|
|
|
5611
|
-
|
|
5612
|
-
|
|
5881
|
+
/* \u2500\u2500 File attachments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
5882
|
+
/* The picker button sits in the composer's tool row; hidden until
|
|
5883
|
+
data-attachments-url. */
|
|
5884
|
+
.attach-input {
|
|
5885
|
+
display: none;
|
|
5613
5886
|
}
|
|
5614
5887
|
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5888
|
+
/* The mic button's mount point; filled only once #wireVoice mounts the
|
|
5889
|
+
control. */
|
|
5890
|
+
.voice-slot {
|
|
5891
|
+
display: contents;
|
|
5618
5892
|
}
|
|
5619
5893
|
|
|
5620
5894
|
/* Recording: a red tint + a gentle pulse so it's clearly "live". */
|
|
5621
5895
|
.voice-btn[data-state="recording"] {
|
|
5622
|
-
border-color: var(--_danger);
|
|
5623
5896
|
background: var(--_danger);
|
|
5624
5897
|
color: #ffffff;
|
|
5625
5898
|
animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
|
|
5626
5899
|
}
|
|
5627
5900
|
|
|
5901
|
+
.voice-btn[data-state="recording"]:hover {
|
|
5902
|
+
background: var(--_danger);
|
|
5903
|
+
color: #ffffff;
|
|
5904
|
+
}
|
|
5905
|
+
|
|
5628
5906
|
@keyframes ag-ui-voice-pulse {
|
|
5629
5907
|
0%, 100% { opacity: 1; }
|
|
5630
5908
|
50% { opacity: 0.6; }
|
|
@@ -6015,16 +6293,35 @@ var STYLES = `
|
|
|
6015
6293
|
color: var(--_danger);
|
|
6016
6294
|
}
|
|
6017
6295
|
|
|
6018
|
-
/* Chat-history drawer \u2014 a slide-over within the chat panel.
|
|
6296
|
+
/* Chat-history drawer \u2014 a slide-over within the chat panel.
|
|
6297
|
+
The hidden attribute stays the single source of truth for open/closed (no
|
|
6298
|
+
JS-driven animation state): display is transitioned discretely, so on close
|
|
6299
|
+
the drawer stays displayed for the whole exit and is removed only at the end,
|
|
6300
|
+
which is what lets the backdrop fade and the panel slide out. */
|
|
6019
6301
|
.drawer {
|
|
6020
6302
|
position: absolute;
|
|
6021
6303
|
inset: 0;
|
|
6022
6304
|
z-index: 5;
|
|
6023
6305
|
display: flex;
|
|
6306
|
+
transition: visibility var(--_motion) var(--_ease);
|
|
6024
6307
|
}
|
|
6025
6308
|
|
|
6309
|
+
/* Closed. The overlay keeps its box (display, not none) so the backdrop and
|
|
6310
|
+
panel inside it stay rendered and can transition both ways; visibility is
|
|
6311
|
+
what takes the whole subtree out of the tab order, the a11y tree and hit
|
|
6312
|
+
testing at rest, and it holds off until the slide has finished. */
|
|
6026
6313
|
.drawer[hidden] {
|
|
6027
|
-
display:
|
|
6314
|
+
display: flex;
|
|
6315
|
+
visibility: hidden;
|
|
6316
|
+
pointer-events: none;
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6319
|
+
.drawer[hidden] .drawer-backdrop {
|
|
6320
|
+
opacity: 0;
|
|
6321
|
+
}
|
|
6322
|
+
|
|
6323
|
+
.drawer[hidden] .drawer-panel {
|
|
6324
|
+
transform: translateX(-100%);
|
|
6028
6325
|
}
|
|
6029
6326
|
|
|
6030
6327
|
.checkpoints {
|
|
@@ -6042,10 +6339,21 @@ var STYLES = `
|
|
|
6042
6339
|
box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
|
|
6043
6340
|
max-height: 60%;
|
|
6044
6341
|
overflow-y: auto;
|
|
6342
|
+
transform-origin: top center;
|
|
6343
|
+
transition:
|
|
6344
|
+
opacity var(--_motion) var(--_ease),
|
|
6345
|
+
transform var(--_motion) var(--_ease),
|
|
6346
|
+
visibility var(--_motion) var(--_ease);
|
|
6045
6347
|
}
|
|
6046
6348
|
|
|
6349
|
+
/* Same idiom as the drawer: laid out at rest, hidden by visibility, so the
|
|
6350
|
+
popover can animate open and closed. */
|
|
6047
6351
|
.checkpoints[hidden] {
|
|
6048
|
-
display:
|
|
6352
|
+
display: flex;
|
|
6353
|
+
visibility: hidden;
|
|
6354
|
+
pointer-events: none;
|
|
6355
|
+
opacity: 0;
|
|
6356
|
+
transform: scale(0.96) translateY(-6px);
|
|
6049
6357
|
}
|
|
6050
6358
|
|
|
6051
6359
|
.checkpoints-title {
|
|
@@ -6107,6 +6415,8 @@ var STYLES = `
|
|
|
6107
6415
|
position: absolute;
|
|
6108
6416
|
inset: 0;
|
|
6109
6417
|
background: rgba(20, 20, 50, 0.32);
|
|
6418
|
+
opacity: 1;
|
|
6419
|
+
transition: opacity var(--_motion) var(--_ease);
|
|
6110
6420
|
}
|
|
6111
6421
|
|
|
6112
6422
|
.drawer-panel {
|
|
@@ -6119,6 +6429,8 @@ var STYLES = `
|
|
|
6119
6429
|
border-right: 1px solid var(--_border);
|
|
6120
6430
|
box-shadow: var(--_shadow);
|
|
6121
6431
|
overflow: hidden;
|
|
6432
|
+
transform: none;
|
|
6433
|
+
transition: transform var(--_motion) var(--_ease);
|
|
6122
6434
|
}
|
|
6123
6435
|
|
|
6124
6436
|
.drawer-header {
|
|
@@ -6748,7 +7060,10 @@ var VoiceInput = class {
|
|
|
6748
7060
|
this.element.type = "button";
|
|
6749
7061
|
this.element.className = "voice-btn";
|
|
6750
7062
|
this.element.setAttribute("part", "voice-button");
|
|
6751
|
-
|
|
7063
|
+
const glyph = document.createElement("slot");
|
|
7064
|
+
glyph.name = "icon-voice";
|
|
7065
|
+
glyph.innerHTML = ICON_VOICE;
|
|
7066
|
+
this.element.append(glyph);
|
|
6752
7067
|
this.#setState("idle");
|
|
6753
7068
|
this.element.addEventListener("click", () => {
|
|
6754
7069
|
void this.toggle();
|
|
@@ -7865,8 +8180,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
7865
8180
|
#attachSlot;
|
|
7866
8181
|
/** Optional built-in header theme toggle; shown only with `data-theme-toggle`. */
|
|
7867
8182
|
#themeToggle;
|
|
7868
|
-
/**
|
|
7869
|
-
#
|
|
8183
|
+
/** What the collapsed widget shrinks to: the floating launcher, or the sidebar rail. */
|
|
8184
|
+
#launcher;
|
|
8185
|
+
/** The launcher's unread badge; hidden at zero, and when the host opts out. */
|
|
8186
|
+
#badge;
|
|
8187
|
+
// Answers that finished while the widget was collapsed. Expanding clears it.
|
|
8188
|
+
#unread = 0;
|
|
7870
8189
|
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
7871
8190
|
#emptyWrap;
|
|
7872
8191
|
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
@@ -7939,7 +8258,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
7939
8258
|
this.#attachSlot = document.createElement("div");
|
|
7940
8259
|
this.#voiceSlot = document.createElement("span");
|
|
7941
8260
|
this.#themeToggle = document.createElement("button");
|
|
7942
|
-
this.#
|
|
8261
|
+
this.#launcher = document.createElement("button");
|
|
8262
|
+
this.#badge = document.createElement("span");
|
|
7943
8263
|
this.#emptyWrap = document.createElement("div");
|
|
7944
8264
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
7945
8265
|
this.#drawer = new ThreadDrawer({
|
|
@@ -8002,6 +8322,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8002
8322
|
return;
|
|
8003
8323
|
}
|
|
8004
8324
|
this.#input.value = "";
|
|
8325
|
+
this.#autoGrow();
|
|
8005
8326
|
const endpoint = verb === "resume" ? index.resumeUrl(runId) : index.forkUrl(runId);
|
|
8006
8327
|
const agent = this.agentFactory({
|
|
8007
8328
|
endpoint,
|
|
@@ -8332,7 +8653,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
8332
8653
|
if (this.#readScopedItem(COLLAPSED_KEY) === "1") {
|
|
8333
8654
|
this.setAttribute("collapsed", "");
|
|
8334
8655
|
}
|
|
8335
|
-
this.#
|
|
8656
|
+
this.#syncLauncher();
|
|
8336
8657
|
this.#initSkills();
|
|
8337
8658
|
if (this.#storageNs !== "" && this.conversationStore instanceof SessionStorageStore) {
|
|
8338
8659
|
this.conversationStore = new SessionStorageStore(this.#storageNs);
|
|
@@ -8690,12 +9011,14 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
8690
9011
|
this.#skillHint.textContent = this.#strings.skillNeeds.replace("{title}", skill.title).replace("{fields}", missing.join(", "));
|
|
8691
9012
|
this.#skillHint.hidden = false;
|
|
8692
9013
|
this.#input.value = text2;
|
|
9014
|
+
this.#autoGrow();
|
|
8693
9015
|
this.#input.focus();
|
|
8694
9016
|
this.#selectFirstPlaceholder(text2);
|
|
8695
9017
|
return;
|
|
8696
9018
|
}
|
|
8697
9019
|
this.#skillHint.hidden = true;
|
|
8698
9020
|
this.#input.value = text2;
|
|
9021
|
+
this.#autoGrow();
|
|
8699
9022
|
if (skill.sendImmediately === false) {
|
|
8700
9023
|
this.#input.focus();
|
|
8701
9024
|
return;
|
|
@@ -8733,7 +9056,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
8733
9056
|
this.removeAttribute("collapsed");
|
|
8734
9057
|
}
|
|
8735
9058
|
sessionStorage.setItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
8736
|
-
this.#
|
|
9059
|
+
this.#setUnread(0);
|
|
8737
9060
|
this.dispatchEvent(
|
|
8738
9061
|
new CustomEvent(TOGGLE_EVENT, {
|
|
8739
9062
|
detail: { collapsed },
|
|
@@ -8742,6 +9065,15 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
8742
9065
|
})
|
|
8743
9066
|
);
|
|
8744
9067
|
}
|
|
9068
|
+
/**
|
|
9069
|
+
* Answers that finished while the widget was collapsed, and that the user has
|
|
9070
|
+
* therefore not seen. Expanding (or {@link newChat}) clears it. The launcher's
|
|
9071
|
+
* badge renders this; {@link UNREAD_EVENT} announces every change, so a host
|
|
9072
|
+
* chrome can render its own instead.
|
|
9073
|
+
*/
|
|
9074
|
+
get unread() {
|
|
9075
|
+
return this.#unread;
|
|
9076
|
+
}
|
|
8745
9077
|
/** Flip the collapsed state. Bound to the built-in header toggle. */
|
|
8746
9078
|
toggleCollapsed() {
|
|
8747
9079
|
this.setCollapsed(!this.collapsed);
|
|
@@ -8935,6 +9267,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
8935
9267
|
this.#resetState();
|
|
8936
9268
|
this.#threadId = this.conversationStore.threadId();
|
|
8937
9269
|
this.#setRunning(false);
|
|
9270
|
+
this.#setUnread(0);
|
|
8938
9271
|
}
|
|
8939
9272
|
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
8940
9273
|
#resetState() {
|
|
@@ -9233,17 +9566,27 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9233
9566
|
const inputRow = document.createElement("div");
|
|
9234
9567
|
inputRow.className = "input-row";
|
|
9235
9568
|
inputRow.setAttribute("part", "composer");
|
|
9569
|
+
const composer = document.createElement("div");
|
|
9570
|
+
composer.className = "composer";
|
|
9571
|
+
composer.setAttribute("part", "composer-surface");
|
|
9572
|
+
const tools = document.createElement("div");
|
|
9573
|
+
tools.className = "composer-tools";
|
|
9574
|
+
tools.setAttribute("part", "composer-tools");
|
|
9236
9575
|
this.#input.className = "input";
|
|
9237
9576
|
this.#input.setAttribute("part", "input");
|
|
9238
9577
|
this.#input.setAttribute("aria-label", this.#strings.message);
|
|
9239
|
-
this.#input.rows =
|
|
9578
|
+
this.#input.rows = 1;
|
|
9240
9579
|
this.#input.placeholder = this.#strings.inputPlaceholder;
|
|
9241
9580
|
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
9242
9581
|
this.#input.addEventListener("input", () => this.#onInput());
|
|
9243
9582
|
this.#send.className = "send";
|
|
9244
9583
|
this.#send.type = "button";
|
|
9245
9584
|
this.#send.setAttribute("part", "send");
|
|
9246
|
-
this.#send.
|
|
9585
|
+
this.#send.append(
|
|
9586
|
+
this.#glyphSlot("icon-send", "send-send", ICON_SEND),
|
|
9587
|
+
this.#glyphSlot("icon-stop", "send-stop", ICON_STOP)
|
|
9588
|
+
);
|
|
9589
|
+
this.#send.title = this.#strings.send;
|
|
9247
9590
|
this.#send.setAttribute("aria-label", this.#strings.send);
|
|
9248
9591
|
this.#send.dataset["state"] = "idle";
|
|
9249
9592
|
this.#send.addEventListener("click", () => {
|
|
@@ -9259,7 +9602,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9259
9602
|
this.#attachButton.className = "attach-btn";
|
|
9260
9603
|
this.#attachButton.type = "button";
|
|
9261
9604
|
this.#attachButton.setAttribute("part", "attach-button");
|
|
9262
|
-
this.#attachButton.
|
|
9605
|
+
this.#attachButton.append(this.#glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
|
|
9263
9606
|
this.#attachButton.title = this.#strings.attachFiles;
|
|
9264
9607
|
this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
|
|
9265
9608
|
this.#attachButton.hidden = true;
|
|
@@ -9273,7 +9616,9 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9273
9616
|
this.#voiceSlot.className = "voice-slot";
|
|
9274
9617
|
const footer = document.createElement("slot");
|
|
9275
9618
|
footer.name = "footer";
|
|
9276
|
-
|
|
9619
|
+
tools.append(this.#attachButton, this.#voiceSlot, this.#send);
|
|
9620
|
+
composer.append(this.#input, tools);
|
|
9621
|
+
inputRow.append(composer, this.#fileInput);
|
|
9277
9622
|
this.#chat.append(
|
|
9278
9623
|
header,
|
|
9279
9624
|
this.#messages,
|
|
@@ -9286,12 +9631,19 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9286
9631
|
this.#drawer.element,
|
|
9287
9632
|
this.#checkpoints.element
|
|
9288
9633
|
);
|
|
9289
|
-
this.#
|
|
9290
|
-
this.#
|
|
9291
|
-
this.#
|
|
9292
|
-
this.#
|
|
9293
|
-
this.#
|
|
9294
|
-
this.#
|
|
9634
|
+
this.#launcher.className = "launcher";
|
|
9635
|
+
this.#launcher.type = "button";
|
|
9636
|
+
this.#launcher.setAttribute("part", "launcher");
|
|
9637
|
+
this.#launcher.setAttribute("aria-label", this.#strings.expand);
|
|
9638
|
+
this.#badge.className = "launcher-badge";
|
|
9639
|
+
this.#badge.setAttribute("part", "launcher-badge");
|
|
9640
|
+
this.#badge.setAttribute("aria-hidden", "true");
|
|
9641
|
+
this.#badge.hidden = true;
|
|
9642
|
+
this.#launcher.append(
|
|
9643
|
+
this.#iconElement("launcher", "launcher-icon", ICON_LAUNCHER, this.#launcherIconUrl()),
|
|
9644
|
+
this.#badge
|
|
9645
|
+
);
|
|
9646
|
+
this.#launcher.addEventListener("click", () => this.setCollapsed(false));
|
|
9295
9647
|
this.#chat.append(
|
|
9296
9648
|
createResizeHandle({
|
|
9297
9649
|
axis: () => this.#resizeAxis(),
|
|
@@ -9305,7 +9657,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9305
9657
|
label: this.#strings.resizePanel
|
|
9306
9658
|
})
|
|
9307
9659
|
);
|
|
9308
|
-
this.#root.append(style, this.#chat, this.#
|
|
9660
|
+
this.#root.append(style, this.#chat, this.#launcher);
|
|
9309
9661
|
}
|
|
9310
9662
|
/**
|
|
9311
9663
|
* Build a header control button: a named slot a host can project markup into,
|
|
@@ -9332,16 +9684,37 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9332
9684
|
return button2;
|
|
9333
9685
|
}
|
|
9334
9686
|
/**
|
|
9335
|
-
*
|
|
9336
|
-
*
|
|
9687
|
+
* A `<slot>` a host can project its own mark into, falling back to one of the
|
|
9688
|
+
* built-in glyphs. The markup is an author-written constant, never user or
|
|
9689
|
+
* server data, so it is assigned directly rather than sanitised.
|
|
9690
|
+
*/
|
|
9691
|
+
#glyphSlot(slotName, className, markup) {
|
|
9692
|
+
const slot = document.createElement("slot");
|
|
9693
|
+
slot.name = slotName;
|
|
9694
|
+
slot.className = className;
|
|
9695
|
+
slot.innerHTML = markup;
|
|
9696
|
+
return slot;
|
|
9697
|
+
}
|
|
9698
|
+
/**
|
|
9699
|
+
* The launcher's own image URL. `data-launcher-icon-url` lets the collapsed
|
|
9700
|
+
* button carry a different mark from the header's — a product logo reads at
|
|
9701
|
+
* 22px in a header bar but rarely at 26px in a circle — and falls back to the
|
|
9702
|
+
* header icon so a single `data-icon-url` still feeds both.
|
|
9703
|
+
*/
|
|
9704
|
+
#launcherIconUrl() {
|
|
9705
|
+
return this.getAttribute("data-launcher-icon-url") ?? this.getAttribute("data-icon-url");
|
|
9706
|
+
}
|
|
9707
|
+
/**
|
|
9708
|
+
* An icon holder wrapping a `<slot>` so a host can project custom markup;
|
|
9709
|
+
* with an `<img>` as the slot's fallback when an icon URL is configured, or
|
|
9710
|
+
* the given glyph markup when it is not.
|
|
9337
9711
|
*/
|
|
9338
|
-
#iconElement(slotName, part, fallbackGlyph) {
|
|
9712
|
+
#iconElement(slotName, part, fallbackGlyph, iconUrl = this.getAttribute("data-icon-url")) {
|
|
9339
9713
|
const holder = document.createElement("span");
|
|
9340
9714
|
holder.className = "icon-holder";
|
|
9341
9715
|
holder.setAttribute("part", part);
|
|
9342
9716
|
const slot = document.createElement("slot");
|
|
9343
9717
|
slot.name = slotName;
|
|
9344
|
-
const iconUrl = this.getAttribute("data-icon-url");
|
|
9345
9718
|
if (iconUrl !== null) {
|
|
9346
9719
|
const img = document.createElement("img");
|
|
9347
9720
|
img.className = "icon-img";
|
|
@@ -9349,14 +9722,63 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9349
9722
|
img.alt = "";
|
|
9350
9723
|
slot.append(img);
|
|
9351
9724
|
} else if (fallbackGlyph !== null) {
|
|
9352
|
-
slot.
|
|
9725
|
+
slot.innerHTML = fallbackGlyph;
|
|
9353
9726
|
}
|
|
9354
9727
|
holder.append(slot);
|
|
9355
9728
|
return holder;
|
|
9356
9729
|
}
|
|
9357
|
-
/**
|
|
9358
|
-
|
|
9359
|
-
|
|
9730
|
+
/**
|
|
9731
|
+
* Reflect the collapsed state and the unread count on the launcher.
|
|
9732
|
+
*
|
|
9733
|
+
* The count is also the launcher's accessible name: a badge that only exists
|
|
9734
|
+
* as a coloured dot says nothing to a screen reader, and "Expand" alone would
|
|
9735
|
+
* be a lie once answers are waiting behind it.
|
|
9736
|
+
*/
|
|
9737
|
+
#syncLauncher() {
|
|
9738
|
+
this.#launcher.setAttribute("aria-expanded", String(!this.collapsed));
|
|
9739
|
+
const unread = this.#unread;
|
|
9740
|
+
this.#badge.textContent = unread > 9 ? "9+" : String(unread);
|
|
9741
|
+
this.#badge.hidden = unread === 0 || !this.#badgeEnabled();
|
|
9742
|
+
const label = this.#badge.hidden ? this.#strings.expand : this.#strings.expandUnread.replace("{count}", String(unread));
|
|
9743
|
+
this.#launcher.setAttribute("aria-label", label);
|
|
9744
|
+
this.#launcher.title = label;
|
|
9745
|
+
}
|
|
9746
|
+
/**
|
|
9747
|
+
* The unread badge, unlike every other affordance here, is on by default:
|
|
9748
|
+
* a collapsed widget is the one state where an answer can arrive with nothing
|
|
9749
|
+
* on screen to say so. `data-unread-badge="false"` turns it off for a host
|
|
9750
|
+
* that drives its own chrome from the `ag-ui-unread` event.
|
|
9751
|
+
*/
|
|
9752
|
+
#badgeEnabled() {
|
|
9753
|
+
return this.getAttribute("data-unread-badge") !== "false";
|
|
9754
|
+
}
|
|
9755
|
+
/**
|
|
9756
|
+
* Set the unread count, repaint the badge, and tell the host.
|
|
9757
|
+
*
|
|
9758
|
+
* The count is kept whether or not the badge renders it, so `unread` stays
|
|
9759
|
+
* truthful for a host chrome and switching the badge on mid-session doesn't
|
|
9760
|
+
* start from a number that was never counted.
|
|
9761
|
+
*/
|
|
9762
|
+
#setUnread(count) {
|
|
9763
|
+
this.#unread = count;
|
|
9764
|
+
this.#syncLauncher();
|
|
9765
|
+
this.dispatchEvent(
|
|
9766
|
+
new CustomEvent(UNREAD_EVENT, {
|
|
9767
|
+
detail: { unread: count },
|
|
9768
|
+
bubbles: true,
|
|
9769
|
+
composed: true
|
|
9770
|
+
})
|
|
9771
|
+
);
|
|
9772
|
+
}
|
|
9773
|
+
/**
|
|
9774
|
+
* Count an answer the user cannot have seen: one that finished while the
|
|
9775
|
+
* widget was collapsed. Expanding is what marks them read.
|
|
9776
|
+
*/
|
|
9777
|
+
#noteUnread() {
|
|
9778
|
+
if (!this.collapsed) {
|
|
9779
|
+
return;
|
|
9780
|
+
}
|
|
9781
|
+
this.#setUnread(this.#unread + 1);
|
|
9360
9782
|
}
|
|
9361
9783
|
/** Hide the empty-state region once the message list holds anything else. */
|
|
9362
9784
|
#updateEmptyState() {
|
|
@@ -9366,6 +9788,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9366
9788
|
#onInput() {
|
|
9367
9789
|
this.#skillsMenu.onInput(this.#input.value);
|
|
9368
9790
|
this.#skillHint.hidden = true;
|
|
9791
|
+
this.#autoGrow();
|
|
9369
9792
|
}
|
|
9370
9793
|
#onKeydown(event) {
|
|
9371
9794
|
if (this.#skillsMenu.onKeydown(event)) {
|
|
@@ -9392,14 +9815,32 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9392
9815
|
this.#confirmAbort?.abort();
|
|
9393
9816
|
this.#client?.cancel();
|
|
9394
9817
|
}
|
|
9395
|
-
/**
|
|
9818
|
+
/**
|
|
9819
|
+
* Swap the composer button between Send (idle) and Stop (running).
|
|
9820
|
+
*
|
|
9821
|
+
* The glyph is swapped by CSS from `data-state` — both are mounted — so this
|
|
9822
|
+
* only has to move the accessible name, which is the button's whole label now
|
|
9823
|
+
* that it carries no text.
|
|
9824
|
+
*/
|
|
9396
9825
|
#setRunning(running) {
|
|
9397
9826
|
this.#running = running;
|
|
9398
9827
|
const label = running ? this.#strings.stop : this.#strings.send;
|
|
9399
|
-
this.#send.
|
|
9828
|
+
this.#send.title = label;
|
|
9400
9829
|
this.#send.setAttribute("aria-label", label);
|
|
9401
9830
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
9402
9831
|
}
|
|
9832
|
+
/**
|
|
9833
|
+
* Size the field to its content: one row when empty, growing with what is
|
|
9834
|
+
* typed until the CSS ceiling takes over and it scrolls.
|
|
9835
|
+
*
|
|
9836
|
+
* Resetting to `auto` first is what makes it shrink again — `scrollHeight`
|
|
9837
|
+
* never reports less than the current height, so measuring without the reset
|
|
9838
|
+
* would ratchet the composer taller and never back down.
|
|
9839
|
+
*/
|
|
9840
|
+
#autoGrow() {
|
|
9841
|
+
this.#input.style.height = "auto";
|
|
9842
|
+
this.#input.style.height = `${this.#input.scrollHeight}px`;
|
|
9843
|
+
}
|
|
9403
9844
|
async #submit() {
|
|
9404
9845
|
if (this.#running) {
|
|
9405
9846
|
return;
|
|
@@ -9410,6 +9851,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9410
9851
|
return;
|
|
9411
9852
|
}
|
|
9412
9853
|
this.#input.value = "";
|
|
9854
|
+
this.#autoGrow();
|
|
9413
9855
|
if (this.#attachTray?.hasPending() === true) {
|
|
9414
9856
|
this.#appendNotice(
|
|
9415
9857
|
"\u{1F4CE}",
|
|
@@ -9678,6 +10120,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
9678
10120
|
}
|
|
9679
10121
|
attachCopyButtons(bubble, this.#strings);
|
|
9680
10122
|
this.#streamingBubble = null;
|
|
10123
|
+
this.#noteUnread();
|
|
9681
10124
|
},
|
|
9682
10125
|
onToolCall: (call) => {
|
|
9683
10126
|
this.#hidePending();
|
|
@@ -9904,7 +10347,7 @@ function setControlValue(el, value) {
|
|
|
9904
10347
|
}
|
|
9905
10348
|
|
|
9906
10349
|
// src/version.ts
|
|
9907
|
-
var VERSION = "0.
|
|
10350
|
+
var VERSION = "0.22.0";
|
|
9908
10351
|
export {
|
|
9909
10352
|
ATTACHMENT_EVENT,
|
|
9910
10353
|
AgUiChat,
|
|
@@ -9928,6 +10371,7 @@ export {
|
|
|
9928
10371
|
TOOL_CALL_STATUS,
|
|
9929
10372
|
TOOL_DISPLAY,
|
|
9930
10373
|
ToolCallCard,
|
|
10374
|
+
UNREAD_EVENT,
|
|
9931
10375
|
VERSION,
|
|
9932
10376
|
X_CONFIRM_KEY,
|
|
9933
10377
|
X_DESTRUCTIVE_KEY,
|