@perspective-dev/viewer 5.1.0 → 5.3.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.
Files changed (245) hide show
  1. package/dist/cdn/perspective-viewer.js +9 -9
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/docs/perspective-docs.json +1 -0
  24. package/dist/esm/column-format.d.ts +19 -28
  25. package/dist/esm/perspective-viewer.d.ts +3 -0
  26. package/dist/esm/perspective-viewer.inline.js +9 -9
  27. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  28. package/dist/esm/perspective-viewer.js +9 -9
  29. package/dist/esm/perspective-viewer.js.map +4 -4
  30. package/dist/esm/plugin.d.ts +10 -0
  31. package/dist/esm/providers.d.ts +79 -0
  32. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  33. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  35. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  36. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  38. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  39. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  40. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  41. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  42. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  43. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  44. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  45. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  46. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +42 -2
  47. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  48. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  49. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  50. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  51. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  52. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  53. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  54. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  55. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  56. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  57. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  59. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  60. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  61. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  62. package/dist/esm/ts-rs/ViewerConfig.d.ts +42 -2
  63. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  64. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
  66. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +20 -9
  68. package/dist/wasm/perspective-viewer.d.ts +157 -41
  69. package/dist/wasm/perspective-viewer.js +452 -151
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +22 -17
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +8 -0
  75. package/src/css/column-settings-panel.css +9 -5
  76. package/src/css/column-style.css +209 -31
  77. package/src/css/config-selector.css +38 -3
  78. package/src/css/dom/select.css +2 -1
  79. package/src/css/form/code-editor.css +6 -7
  80. package/src/css/form/mirrored-textarea.css +81 -0
  81. package/src/css/viewer.css +328 -0
  82. package/src/rust/agent/client/mod.rs +19 -0
  83. package/src/rust/agent/client/protocol.rs +320 -0
  84. package/src/rust/agent/client/transport.rs +287 -0
  85. package/src/rust/agent/client/turn.rs +174 -0
  86. package/src/rust/agent/config.rs +159 -0
  87. package/src/rust/agent/docs.rs +293 -0
  88. package/src/rust/agent/markdown/block.rs +509 -0
  89. package/src/rust/agent/markdown/entity.rs +52 -0
  90. package/src/rust/agent/markdown/events.rs +71 -0
  91. package/src/rust/agent/markdown/inline.rs +497 -0
  92. package/src/rust/agent/markdown/mod.rs +466 -0
  93. package/src/rust/agent/mod.rs +23 -0
  94. package/src/rust/agent/preamble.md +16 -0
  95. package/src/rust/agent/runtime.rs +201 -0
  96. package/src/rust/agent/slot.rs +236 -0
  97. package/src/rust/agent/tools.rs +1141 -0
  98. package/src/rust/components/chat_panel.rs +289 -0
  99. package/src/rust/components/column_selector/active_column.rs +2 -2
  100. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  101. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  102. package/src/rust/components/column_selector/config_selector.rs +84 -7
  103. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  104. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  105. package/src/rust/components/column_selector.rs +55 -4
  106. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  107. package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
  108. package/src/rust/components/column_settings_sidebar.rs +88 -69
  109. package/src/rust/components/containers/dragdrop_list.rs +37 -0
  110. package/src/rust/components/editable_header.rs +85 -144
  111. package/src/rust/components/expression_editor.rs +9 -16
  112. package/src/rust/components/form/code_editor.rs +29 -28
  113. package/src/rust/components/form/debug.rs +9 -5
  114. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  115. package/src/rust/components/form/mod.rs +4 -1
  116. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  117. package/src/rust/components/form/named_value_picker.rs +86 -0
  118. package/src/rust/components/form/palette_selector.rs +440 -0
  119. package/src/rust/components/main_panel/msg.rs +2 -2
  120. package/src/rust/components/main_panel/reconcile.rs +4 -4
  121. package/src/rust/components/mod.rs +2 -0
  122. package/src/rust/components/plugin_tab.rs +4 -28
  123. package/src/rust/components/settings_panel.rs +105 -22
  124. package/src/rust/components/viewer/msg.rs +46 -7
  125. package/src/rust/components/viewer/panels.rs +17 -20
  126. package/src/rust/components/viewer/render.rs +10 -3
  127. package/src/rust/components/viewer/settings.rs +163 -23
  128. package/src/rust/components/viewer/snapshots.rs +46 -21
  129. package/src/rust/components/viewer/wiring.rs +14 -25
  130. package/src/rust/components/viewer.rs +12 -8
  131. package/src/rust/components/window_editor.rs +75 -103
  132. package/src/rust/config/column_config_schema.rs +100 -41
  133. package/src/rust/config/css_scale.rs +882 -0
  134. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  135. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  136. package/src/rust/config/mod.rs +2 -0
  137. package/src/rust/config/number_string_format.rs +33 -0
  138. package/src/rust/config/options.rs +28 -0
  139. package/src/rust/config/plugin_static_config.rs +120 -5
  140. package/src/rust/config/viewer_config.rs +216 -3
  141. package/src/rust/config/workspace_config.rs +138 -10
  142. package/src/rust/custom_elements/viewer.rs +369 -119
  143. package/src/rust/custom_events.rs +43 -1
  144. package/src/rust/js/regular_layout.rs +3 -3
  145. package/src/rust/lib.rs +15 -0
  146. package/src/rust/presentation/column_locator.rs +19 -5
  147. package/src/rust/presentation.rs +200 -36
  148. package/src/rust/queries/column_locator.rs +74 -44
  149. package/src/rust/queries/get_viewer_config.rs +2 -1
  150. package/src/rust/queries/mod.rs +2 -0
  151. package/src/rust/queries/palette_set.rs +82 -0
  152. package/src/rust/queries/plugin_column_styles.rs +6 -2
  153. package/src/rust/renderer/dispatch.rs +16 -4
  154. package/src/rust/renderer/plugin_config.rs +309 -47
  155. package/src/rust/renderer.rs +15 -42
  156. package/src/rust/session/column_defaults_update.rs +32 -7
  157. package/src/rust/session/metadata.rs +23 -21
  158. package/src/rust/session/props.rs +1 -0
  159. package/src/rust/session/replace_expression_update.rs +1 -0
  160. package/src/rust/session.rs +9 -6
  161. package/src/rust/tasks/create_panel.rs +46 -49
  162. package/src/rust/tasks/edit_expression.rs +19 -23
  163. package/src/rust/tasks/edit_window.rs +9 -11
  164. package/src/rust/tasks/eject.rs +4 -1
  165. package/src/rust/tasks/pipeline.rs +2 -2
  166. package/src/rust/tasks/presize_panels.rs +44 -5
  167. package/src/rust/tasks/reset_all.rs +4 -4
  168. package/src/rust/tasks/restore_and_render.rs +17 -44
  169. package/src/rust/tasks/restore_panel.rs +37 -22
  170. package/src/rust/tasks/set_edit_mode.rs +3 -1
  171. package/src/rust/tasks/sync_update_panels.rs +18 -24
  172. package/src/rust/tasks/update_theme.rs +23 -18
  173. package/src/rust/utils/css_vars.rs +26 -0
  174. package/src/rust/utils/mod.rs +2 -0
  175. package/src/rust/utils/spawn.rs +1 -1
  176. package/src/rust/workspace.rs +454 -199
  177. package/src/svg/lock.svg +7 -0
  178. package/src/themes/botanical.css +10 -0
  179. package/src/themes/defaults.css +12 -0
  180. package/src/themes/dracula.css +17 -0
  181. package/src/themes/gruvbox-dark.css +20 -0
  182. package/src/themes/gruvbox.css +10 -0
  183. package/src/themes/icons.css +1 -0
  184. package/src/themes/intl/de.css +16 -0
  185. package/src/themes/intl/es.css +16 -0
  186. package/src/themes/intl/fr.css +16 -0
  187. package/src/themes/intl/ja.css +16 -0
  188. package/src/themes/intl/pt.css +16 -0
  189. package/src/themes/intl/zh.css +16 -0
  190. package/src/themes/intl.css +16 -0
  191. package/src/themes/monokai.css +22 -0
  192. package/src/themes/phosphor.css +5 -0
  193. package/src/themes/pro-dark.css +10 -0
  194. package/src/themes/solarized-dark.css +7 -0
  195. package/src/themes/solarized.css +7 -0
  196. package/src/themes/vaporwave.css +20 -0
  197. package/src/ts/column-format.ts +67 -46
  198. package/src/ts/perspective-viewer.ts +3 -0
  199. package/src/ts/plugin.ts +14 -0
  200. package/src/ts/providers.ts +96 -0
  201. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  202. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  203. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  204. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  205. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  206. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  207. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  208. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  209. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  210. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  211. package/src/ts/ts-rs/FormatMode.ts +3 -0
  212. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  213. package/src/ts/ts-rs/Notation.ts +11 -0
  214. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  215. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -2
  216. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  217. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  218. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  219. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  220. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  221. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  222. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  223. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  224. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  225. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  226. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  227. package/src/ts/ts-rs/Unit.ts +3 -0
  228. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  229. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  230. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  231. package/src/ts/ts-rs/ViewerConfig.ts +48 -2
  232. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  233. package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
  234. package/src/ts/ts-rs/WindowSpec.ts +3 -2
  235. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  236. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +18 -9
  237. package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
  238. package/src/rust/components/form/color_range_selector.rs +0 -137
  239. package/src/ts/ts-rs/WindowAggregate.ts +0 -3
  240. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
  241. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
  242. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
  243. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
  244. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
  245. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
@@ -0,0 +1,289 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ //! The chat sidebar tab (feature `llm-agent`) — a pure VIEW over
14
+ //! [`AgentSlot`]: all conversation state lives on the slot so this component
15
+ //! can unmount (tab switch, settings close) without losing the transcript or
16
+ //! interrupting a running turn.
17
+
18
+ use yew::prelude::*;
19
+
20
+ use crate::agent::{AgentSlot, ChatEntry, render_markdown};
21
+ use crate::components::form::mirrored_textarea::MirroredTextarea;
22
+
23
+ #[derive(Properties, PartialEq)]
24
+ struct ChatReasoningProps {
25
+ reasoning: String,
26
+ open: bool,
27
+ }
28
+
29
+ /// A reasoning-model's "thinking" text: plain-text collapsible block,
30
+ /// expanded while it is streaming (the only progress signal during a long
31
+ /// think) and collapsed once real content exists.
32
+ ///
33
+ /// The body is capped and scrolls (`viewer.css`), and follows its own
34
+ /// tail as deltas append — but only while the reader is AT that tail.
35
+ /// Scrolling up to read something is a deliberate act, so it wins until
36
+ /// the reader returns to the bottom, exactly like the transcript.
37
+ #[function_component]
38
+ fn ChatReasoning(props: &ChatReasoningProps) -> Html {
39
+ let body_ref = use_node_ref();
40
+ let scroll_pinned = use_mut_ref(|| true);
41
+ let seen_len = use_mut_ref(|| None::<usize>);
42
+ let onscroll = {
43
+ let body_ref = body_ref.clone();
44
+ let scroll_pinned = scroll_pinned.clone();
45
+ Callback::from(move |_: Event| {
46
+ if let Some(elem) = body_ref.cast::<web_sys::Element>() {
47
+ let gap = elem.scroll_height() - elem.scroll_top() - elem.client_height();
48
+ *scroll_pinned.borrow_mut() = gap < 24;
49
+ }
50
+ })
51
+ };
52
+
53
+ {
54
+ let body_ref = body_ref.clone();
55
+ let scroll_pinned = scroll_pinned.clone();
56
+ let seen_len = seen_len.clone();
57
+ let len = props.reasoning.len();
58
+ use_effect(move || {
59
+ let grew = seen_len.replace(Some(len)).is_some_and(|seen| len > seen);
60
+ if grew
61
+ && *scroll_pinned.borrow()
62
+ && let Some(elem) = body_ref.cast::<web_sys::Element>()
63
+ {
64
+ elem.set_scroll_top(elem.scroll_height());
65
+ }
66
+ });
67
+ }
68
+
69
+ html! {
70
+ <details class="chat-reasoning" open={props.open}>
71
+ <summary />
72
+ <div class="chat-reasoning-body scrollable" ref={body_ref} {onscroll}>
73
+ { &props.reasoning }
74
+ </div>
75
+ </details>
76
+ }
77
+ }
78
+
79
+ /// The prompt input, over the shared [`MirroredTextarea`] — so it grows
80
+ /// with its text instead of scrolling. This consumer wraps
81
+ /// (`white-space: pre-wrap` is the shared default), which confines growth
82
+ /// to the vertical axis; the expression editor's `pre` instead lets it
83
+ /// grow sideways too.
84
+ ///
85
+ /// Split from [`ChatPanel`] because the mirror re-renders per keystroke:
86
+ /// the transcript re-parses markdown for every message it renders, which
87
+ /// must not happen per character.
88
+ #[derive(Properties, PartialEq)]
89
+ struct ChatInputProps {
90
+ agent: AgentSlot,
91
+ busy: bool,
92
+ }
93
+
94
+ #[function_component]
95
+ fn ChatInput(props: &ChatInputProps) -> Html {
96
+ let input_ref = use_node_ref();
97
+
98
+ // Mirrors the textarea's value. The textarea is deliberately NOT
99
+ // value-bound — binding it fights the caret — so this is a shadow of
100
+ // the DOM value, written on `input` and cleared with it on submit.
101
+ let text = use_state_eq(String::new);
102
+ let submit = {
103
+ let agent = props.agent.clone();
104
+ let input_ref = input_ref.clone();
105
+ let text = text.clone();
106
+ Callback::from(move |()| {
107
+ if agent.is_busy() {
108
+ return;
109
+ }
110
+
111
+ if let Some(elem) = input_ref.cast::<web_sys::HtmlTextAreaElement>() {
112
+ let prompt = elem.value().trim().to_owned();
113
+ if prompt.is_empty() {
114
+ return;
115
+ }
116
+
117
+ elem.set_value("");
118
+ text.set(String::new());
119
+ let agent = agent.clone();
120
+ wasm_bindgen_futures::spawn_local(async move {
121
+ // Failures land in the transcript; nothing to propagate.
122
+ let _ = agent.run_prompt(prompt).await;
123
+ });
124
+ }
125
+ })
126
+ };
127
+
128
+ let onkeydown = {
129
+ let submit = submit.clone();
130
+ Callback::from(move |event: KeyboardEvent| {
131
+ if event.key() == "Enter" && !event.shift_key() {
132
+ event.prevent_default();
133
+ submit.emit(());
134
+ }
135
+ })
136
+ };
137
+
138
+ // `input` fires after the value settles and covers paste, cut and
139
+ // Shift+Enter newlines, none of which `keydown` sees correctly.
140
+ let oninput = {
141
+ let input_ref = input_ref.clone();
142
+ let text = text.clone();
143
+ Callback::from(move |_: InputEvent| {
144
+ if let Some(elem) = input_ref.cast::<web_sys::HtmlTextAreaElement>() {
145
+ text.set(elem.value());
146
+ }
147
+ })
148
+ };
149
+
150
+ let onsend = submit.reform(|_: MouseEvent| ());
151
+ let onstop = {
152
+ let agent = props.agent.clone();
153
+ Callback::from(move |_: MouseEvent| agent.stop())
154
+ };
155
+
156
+ html! {
157
+ <div id="chat_input_row">
158
+ <div id="chat_input_scroll" class="scrollable">
159
+ <MirroredTextarea
160
+ id="chat_input"
161
+ class="chat-input-box"
162
+ mirror={html! { (*text).clone() }}
163
+ is_empty={text.is_empty()}
164
+ textarea_ref={input_ref}
165
+ placeholder="Ask about your data\u{2026}"
166
+ disabled={props.busy}
167
+ {onkeydown}
168
+ {oninput}
169
+ />
170
+ </div>
171
+ if props.busy {
172
+ <button id="chat_stop_button" onclick={onstop} />
173
+ } else {
174
+ <button id="chat_send_button" onclick={onsend} />
175
+ }
176
+ </div>
177
+ }
178
+ }
179
+
180
+ #[derive(Properties, PartialEq)]
181
+ pub struct ChatPanelProps {
182
+ pub agent: AgentSlot,
183
+ }
184
+
185
+ #[function_component]
186
+ pub fn ChatPanel(props: &ChatPanelProps) -> Html {
187
+ let update = use_force_update();
188
+ {
189
+ let agent = props.agent.clone();
190
+ use_effect_with((), move |_| {
191
+ let sub = agent
192
+ .on_update
193
+ .add_notify_listener(&Callback::from(move |_| update.force_update()));
194
+
195
+ move || drop(sub)
196
+ });
197
+ }
198
+
199
+ // Autoscroll only while the user is pinned at the bottom — reading
200
+ // scrolled-back transcript must survive incoming streaming deltas.
201
+ // `onscroll` fires for programmatic scrolls too, so scrolling to the
202
+ // bottom re-pins consistently.
203
+ let log_ref = use_node_ref();
204
+ let scroll_pinned = use_mut_ref(|| true);
205
+ let onscroll = {
206
+ let log_ref = log_ref.clone();
207
+ let scroll_pinned = scroll_pinned.clone();
208
+ Callback::from(move |_: Event| {
209
+ if let Some(elem) = log_ref.cast::<web_sys::Element>() {
210
+ let gap = elem.scroll_height() - elem.scroll_top() - elem.client_height();
211
+ *scroll_pinned.borrow_mut() = gap < 30;
212
+ }
213
+ })
214
+ };
215
+
216
+ {
217
+ let log_ref = log_ref.clone();
218
+ let scroll_pinned = scroll_pinned.clone();
219
+ use_effect(move || {
220
+ if let Some(elem) = log_ref.cast::<web_sys::Element>()
221
+ && *scroll_pinned.borrow()
222
+ {
223
+ elem.set_scroll_top(elem.scroll_height());
224
+ }
225
+ });
226
+ }
227
+
228
+ let busy = props.agent.is_busy();
229
+ let entries = props
230
+ .agent
231
+ .transcript()
232
+ .into_iter()
233
+ .map(|entry| match entry {
234
+ ChatEntry::User(text) => html! {
235
+ <div class="chat-message chat-user">{ text.trim() }</div>
236
+ },
237
+ ChatEntry::Assistant { text, reasoning } => html! {
238
+ <div class="chat-message chat-assistant">
239
+ if let Some(reasoning) = reasoning { <ChatReasoning {reasoning} open=false /> }
240
+ { render_markdown(&text) }
241
+ </div>
242
+ },
243
+ ChatEntry::Tool { name, args, error } => {
244
+ let class = if error.is_some() {
245
+ "chat-tool-chip chat-tool-chip-error"
246
+ } else {
247
+ "chat-tool-chip"
248
+ };
249
+
250
+ let title = match &error {
251
+ Some(err) => format!("{err}\n\n{args}"),
252
+ None => args.clone(),
253
+ };
254
+
255
+ html! { <div {class} {title}>{ name }</div> }
256
+ },
257
+ ChatEntry::Error(text) => html! { <div class="chat-message chat-error">{ text }</div> },
258
+ })
259
+ .collect::<Html>();
260
+
261
+ // The in-flight turn's streaming tail: reasoning renders auto-OPEN
262
+ // while it is the only signal, collapsing once answer text starts;
263
+ // no deltas yet (or between tool rounds) shows the typing dots.
264
+ let pending = props.agent.pending();
265
+ let tail = match &pending {
266
+ Some((text, reasoning)) if !text.is_empty() || !reasoning.is_empty() => html! {
267
+ <div class="chat-message chat-assistant chat-streaming">
268
+ if !reasoning.is_empty() {
269
+ <ChatReasoning reasoning={reasoning.clone()} open={text.is_empty()} />
270
+ }
271
+ if !text.is_empty() {
272
+ { render_markdown(text) }
273
+ }
274
+ </div>
275
+ },
276
+ _ if busy => html! {
277
+ <div class="chat-message chat-assistant chat-pending"><span /><span /><span /></div>
278
+ },
279
+ _ => html! {},
280
+ };
281
+
282
+ html! {
283
+ <div id="chat_panel">
284
+ <div id="chat_log" class="scrollable" ref={log_ref} {onscroll}>{ entries }{ tail }</div>
285
+ <ChatInput agent={props.agent.clone()} {busy} />
286
+ <div id="chat_badge">{ props.agent.label().unwrap_or_default() }</div>
287
+ </div>
288
+ }
289
+ }
@@ -23,7 +23,7 @@ use super::expr_edit_button::*;
23
23
  use crate::components::column_dropdown::ColumnDropDownElement;
24
24
  use crate::components::column_selector::{EmptyColumn, InvalidColumn};
25
25
  use crate::config::ColumnSelectMode;
26
- use crate::presentation::{ColumnLocator, Presentation};
26
+ use crate::presentation::{ColumnSettingsTarget, Presentation};
27
27
  use crate::queries::*;
28
28
  use crate::renderer::*;
29
29
  use crate::session::*;
@@ -51,7 +51,7 @@ pub struct ActiveColumnProps {
51
51
  pub onselect: Callback<()>,
52
52
 
53
53
  /// Fires when this component's expression/config button is clicked.
54
- pub on_open_expr_panel: Callback<ColumnLocator>,
54
+ pub on_open_expr_panel: Callback<ColumnSettingsTarget>,
55
55
 
56
56
  /// Is this column in a grouped context (does the aggregate selector
57
57
  /// need to be visible)?
@@ -12,12 +12,12 @@
12
12
 
13
13
  use yew::prelude::*;
14
14
 
15
- use super::ColumnLocator;
15
+ use super::{ColumnLocator, ColumnSettingsTarget};
16
16
 
17
17
  #[derive(Clone, PartialEq, Properties)]
18
18
  pub struct AddExpressionButtonProps {
19
19
  // Fires when this button is clicked.
20
- pub on_open_expr_panel: Callback<ColumnLocator>,
20
+ pub on_open_expr_panel: Callback<ColumnSettingsTarget>,
21
21
 
22
22
  /// The column this butotn will open configuration for.
23
23
  pub selected_column: Option<ColumnLocator>,
@@ -47,7 +47,7 @@ pub fn AddExpressionButton(p: &AddExpressionButtonProps) -> Html {
47
47
 
48
48
  let onmousedown = p
49
49
  .on_open_expr_panel
50
- .reform(|_| ColumnLocator::NewExpression);
50
+ .reform(|_| ColumnSettingsTarget::NewExpression);
51
51
 
52
52
  let class = if *is_mouseover || matches!(p.selected_column, Some(ColumnLocator::NewExpression))
53
53
  {
@@ -10,9 +10,9 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use std::collections::HashSet;
14
13
  use std::rc::Rc;
15
14
 
15
+ use itertools::Itertools;
16
16
  use perspective_client::config::*;
17
17
  use yew::prelude::*;
18
18
 
@@ -163,8 +163,7 @@ impl AggregateSelector {
163
163
  Aggregate::MultiAggregate(x, _) => Some(x),
164
164
  _ => None,
165
165
  })
166
- .collect::<HashSet<_>>()
167
- .into_iter()
166
+ .unique()
168
167
  .map(|x| {
169
168
  SelectItem::OptGroup(
170
169
  x.clone().into(),
@@ -24,6 +24,7 @@ use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPo
24
24
  use crate::components::containers::dragdrop_list::*;
25
25
  use crate::components::containers::select::{Select, SelectItem};
26
26
  use crate::components::filter_dropdown::{FilterDropDownElement, FilterDropDownPortal};
27
+ use crate::config::PluginStaticConfig;
27
28
  use crate::presentation::Presentation;
28
29
  use crate::renderer::*;
29
30
  use crate::session::drag_drop_update::*;
@@ -48,6 +49,14 @@ pub struct ConfigSelectorProps {
48
49
  /// Session metadata snapshot — threaded from `SessionProps`.
49
50
  pub metadata: SessionMetadataRc,
50
51
 
52
+ /// The ACTIVE plugin's declared contract, threaded as a VALUE prop.
53
+ /// Read from `renderer.metadata()` during render instead, the pivot
54
+ /// labels went stale: `renderer` is deliberately excluded from prop
55
+ /// equality, so switching Y Line back to Datagrid — same view config,
56
+ /// same named-slot count — re-rendered nothing and left the previous
57
+ /// plugin's role labels on screen until the panel was reopened.
58
+ pub plugin_static_config: Rc<PluginStaticConfig>,
59
+
51
60
  /// Selected theme name, threaded for PortalModal consumers.
52
61
  pub selected_theme: Option<String>,
53
62
 
@@ -63,6 +72,7 @@ impl PartialEq for ConfigSelectorProps {
63
72
  && self.drag_column == other.drag_column
64
73
  && self.metadata == other.metadata
65
74
  && self.selected_theme == other.selected_theme
75
+ && self.plugin_static_config == other.plugin_static_config
66
76
  }
67
77
  }
68
78
 
@@ -76,6 +86,7 @@ pub enum ConfigSelectorMsg {
76
86
  TransposePivots,
77
87
  New(DragTarget, InPlaceColumn),
78
88
  UpdateGroupRollupMode(GroupRollupMode),
89
+ UpdateSplitRollupMode(SplitRollupMode),
79
90
  }
80
91
 
81
92
  #[derive(Clone)]
@@ -156,6 +167,22 @@ impl Component for ConfigSelector {
156
167
 
157
168
  false
158
169
  },
170
+ ConfigSelectorMsg::UpdateSplitRollupMode(mode) => {
171
+ let config = ViewConfigUpdate {
172
+ split_rollup_mode: Some(mode),
173
+ ..ViewConfigUpdate::default()
174
+ };
175
+
176
+ {
177
+ let session = ctx.props().session.clone();
178
+ let renderer = ctx.props().renderer.clone();
179
+ if let Ok(task) = apply_and_render(&session, &renderer, config) {
180
+ spawn_owned("config-selector", task);
181
+ }
182
+ }
183
+
184
+ false
185
+ },
159
186
  ConfigSelectorMsg::Close(index, DragTarget::GroupBy) => {
160
187
  if ctx.props().view_config.group_rollup_mode == GroupRollupMode::Total {
161
188
  let requirements = ctx.props().renderer.metadata();
@@ -252,6 +279,14 @@ impl Component for ConfigSelector {
252
279
  ctx.props().metadata.get_features().unwrap(),
253
280
  );
254
281
 
282
+ super::close_column_settings_if_displaced(
283
+ &ctx.props().presentation,
284
+ &ctx.props().renderer,
285
+ &ctx.props().metadata,
286
+ &ctx.props().view_config,
287
+ &update,
288
+ );
289
+
255
290
  {
256
291
  let session = ctx.props().session.clone();
257
292
  let renderer = ctx.props().renderer.clone();
@@ -531,6 +566,18 @@ impl Component for ConfigSelector {
531
566
  ..
532
567
  } = ctx.props();
533
568
  let config = &ctx.props().view_config;
569
+
570
+ // What the ACTIVE plugin says these pivots draw — the same
571
+ // declaration the agent reads through `list_plugins`, so the
572
+ // label a user sees and the role a model is told cannot drift.
573
+ let plugin_roles = {
574
+ let plugin = &ctx.props().plugin_static_config;
575
+ (
576
+ plugin.group_by_role.clone().map(AttrValue::from),
577
+ plugin.split_by_role.clone().map(AttrValue::from),
578
+ )
579
+ };
580
+
534
581
  let transpose = ctx.link().callback(|_| ConfigSelectorMsg::TransposePivots);
535
582
  let column_dropdown = self.column_dropdown.clone();
536
583
  let mut class = classes!();
@@ -562,6 +609,17 @@ impl Component for ConfigSelector {
562
609
 
563
610
  let group_rollups = requirements.get_group_rollups(&rollup_features);
564
611
 
612
+ let on_split_rollup_mode = ctx
613
+ .link()
614
+ .callback(ConfigSelectorMsg::UpdateSplitRollupMode);
615
+
616
+ let split_rollup_features = metadata
617
+ .get_features()
618
+ .map(|x| x.get_split_rollup_modes())
619
+ .unwrap();
620
+
621
+ let split_rollups = requirements.get_split_rollups(&split_rollup_features);
622
+
565
623
  html! {
566
624
  <>
567
625
  <div slot="top_panel" id="top_panel" {class} ondragend={dragend}>
@@ -593,6 +651,7 @@ impl Component for ConfigSelector {
593
651
  if features.group_by {
594
652
  <GroupBySelector
595
653
  name="group_by"
654
+ role_label={plugin_roles.0.clone()}
596
655
  disabled={config.group_rollup_mode == GroupRollupMode::Total}
597
656
  parent={ctx.link().clone()}
598
657
  column_dropdown={column_dropdown.clone()}
@@ -615,18 +674,36 @@ impl Component for ConfigSelector {
615
674
  </GroupBySelector>
616
675
  }
617
676
  if features.split_by {
618
- if !config.split_by.is_empty() {
677
+ if !config.split_by.is_empty() || split_rollups.len() > 1 {
619
678
  <div class="pivot_controls">
620
- <span
621
- id="transpose_button"
622
- class="rrow centered"
623
- title="Transpose Pivots"
624
- onmousedown={transpose}
625
- />
679
+ if split_rollups.len() > 1 {
680
+ <Select<SplitRollupMode>
681
+ id="split_rollup_mode_selector"
682
+ wrapper_class="split_rollup_wrapper"
683
+ is_autosize=true
684
+ values={Rc::new(
685
+ split_rollups
686
+ .iter()
687
+ .map(|x| SelectItem::Option(*x))
688
+ .collect(),
689
+ )}
690
+ selected={config.split_rollup_mode}
691
+ on_select={on_split_rollup_mode}
692
+ />
693
+ }
694
+ if !config.split_by.is_empty() {
695
+ <span
696
+ id="transpose_button"
697
+ class="rrow centered"
698
+ title="Transpose Pivots"
699
+ onmousedown={transpose}
700
+ />
701
+ }
626
702
  </div>
627
703
  }
628
704
  <SplitBySelector
629
705
  name="split_by"
706
+ role_label={plugin_roles.1.clone()}
630
707
  parent={ctx.link().clone()}
631
708
  column_dropdown={column_dropdown.clone()}
632
709
  exclude={config.split_by.iter().cloned().collect::<HashSet<_>>()}
@@ -12,7 +12,7 @@
12
12
 
13
13
  use yew::prelude::*;
14
14
 
15
- use super::ColumnLocator;
15
+ use super::ColumnSettingsTarget;
16
16
 
17
17
  #[derive(PartialEq, Clone, Properties)]
18
18
  pub struct ExprEditButtonProps {
@@ -27,7 +27,7 @@ pub struct ExprEditButtonProps {
27
27
  pub is_window: bool,
28
28
 
29
29
  /// Fires when the config/expresison button is clicked.
30
- pub on_open_expr_panel: Callback<ColumnLocator>,
30
+ pub on_open_expr_panel: Callback<ColumnSettingsTarget>,
31
31
 
32
32
  /// Is the expression/config panel open?
33
33
  pub is_editing: bool,
@@ -42,14 +42,8 @@ pub struct ExprEditButtonProps {
42
42
  #[function_component]
43
43
  pub fn ExprEditButton(p: &ExprEditButtonProps) -> Html {
44
44
  let onmousedown = yew::use_callback(p.clone(), |_, p| {
45
- let name = if p.is_window {
46
- ColumnLocator::Window(p.name.clone())
47
- } else if p.is_expression {
48
- ColumnLocator::Expression(p.name.clone())
49
- } else {
50
- ColumnLocator::Table(p.name.clone())
51
- };
52
- p.on_open_expr_panel.emit(name)
45
+ p.on_open_expr_panel
46
+ .emit(ColumnSettingsTarget::Column(p.name.clone()))
53
47
  });
54
48
 
55
49
  let class = if p.is_disabled {
@@ -18,7 +18,7 @@ use yew::prelude::*;
18
18
  use super::expr_edit_button::*;
19
19
  use crate::components::type_icon::TypeIcon;
20
20
  use crate::config::ColumnSelectMode;
21
- use crate::presentation::{ColumnLocator, Presentation};
21
+ use crate::presentation::{ColumnSettingsTarget, Presentation};
22
22
  use crate::renderer::*;
23
23
  use crate::session::*;
24
24
  use crate::tasks::apply_and_render;
@@ -61,7 +61,7 @@ pub struct InactiveColumnProps {
61
61
  pub onselect: Callback<()>,
62
62
 
63
63
  /// Fires when this column's expression/config button is clicked.
64
- pub on_open_expr_panel: Callback<ColumnLocator>,
64
+ pub on_open_expr_panel: Callback<ColumnSettingsTarget>,
65
65
 
66
66
  // State
67
67
  pub presentation: Presentation,