@perspective-dev/viewer 5.0.0 → 5.2.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/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/docs/perspective-docs.json +1 -0
- package/dist/esm/column-format.d.ts +19 -28
- package/dist/esm/perspective-viewer.d.ts +2 -0
- package/dist/esm/perspective-viewer.inline.js +8 -8
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/providers.d.ts +79 -0
- package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
- package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
- package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
- package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
- package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
- package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
- package/dist/esm/ts-rs/Notation.d.ts +17 -0
- package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
- package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
- package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
- package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
- package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
- package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
- package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/Unit.d.ts +1 -0
- package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
- package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
- package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
- package/dist/wasm/perspective-viewer.d.ts +222 -26
- package/dist/wasm/perspective-viewer.js +378 -125
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
- package/package.json +2 -1
- package/src/css/_viewer_bundle.css +1 -0
- package/src/css/column-selector.css +9 -0
- package/src/css/column-settings-panel.css +67 -2
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +60 -4
- package/src/css/containers/context-menu.css +12 -6
- package/src/css/containers/tabs.css +2 -2
- package/src/css/form/code-editor.css +2 -3
- package/src/css/form/mirrored-textarea.css +81 -0
- package/src/css/viewer.css +336 -0
- package/src/rust/agent/client/mod.rs +19 -0
- package/src/rust/agent/client/protocol.rs +320 -0
- package/src/rust/agent/client/transport.rs +287 -0
- package/src/rust/agent/client/turn.rs +174 -0
- package/src/rust/agent/config.rs +159 -0
- package/src/rust/agent/docs.rs +293 -0
- package/src/rust/agent/markdown/block.rs +509 -0
- package/src/rust/agent/markdown/entity.rs +52 -0
- package/src/rust/agent/markdown/events.rs +71 -0
- package/src/rust/agent/markdown/inline.rs +497 -0
- package/src/rust/agent/markdown/mod.rs +466 -0
- package/src/rust/agent/mod.rs +23 -0
- package/src/rust/agent/preamble.md +16 -0
- package/src/rust/agent/runtime.rs +201 -0
- package/src/rust/agent/slot.rs +236 -0
- package/src/rust/agent/tools.rs +1110 -0
- package/src/rust/components/chat_panel.rs +289 -0
- package/src/rust/components/column_selector/active_column.rs +21 -27
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +83 -8
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/inactive_column.rs +8 -1
- package/src/rust/components/column_selector.rs +27 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +110 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +6 -0
- package/src/rust/components/editable_header.rs +2 -0
- package/src/rust/components/form/code_editor.rs +14 -23
- package/src/rust/components/form/debug.rs +8 -5
- package/src/rust/components/form/mirrored_textarea.rs +165 -0
- package/src/rust/components/form/mod.rs +1 -0
- package/src/rust/components/main_panel.rs +5 -0
- package/src/rust/components/mod.rs +3 -0
- package/src/rust/components/panel_menu.rs +7 -0
- package/src/rust/components/panel_tab.rs +5 -0
- package/src/rust/components/portal.rs +7 -0
- package/src/rust/components/settings_panel.rs +80 -2
- package/src/rust/components/viewer/msg.rs +2 -0
- package/src/rust/components/viewer/panels.rs +9 -14
- package/src/rust/components/viewer/render.rs +14 -0
- package/src/rust/components/viewer/settings.rs +33 -1
- package/src/rust/components/viewer.rs +2 -0
- package/src/rust/components/window_editor.rs +985 -0
- package/src/rust/config/datetime_column_style/custom.rs +17 -0
- package/src/rust/config/datetime_column_style/simple.rs +10 -0
- package/src/rust/config/number_string_format.rs +33 -0
- package/src/rust/config/options.rs +18 -0
- package/src/rust/config/plugin_static_config.rs +120 -5
- package/src/rust/config/viewer_config.rs +220 -1
- package/src/rust/config/workspace_config.rs +7 -6
- package/src/rust/custom_elements/viewer.rs +209 -18
- package/src/rust/custom_events.rs +29 -0
- package/src/rust/lib.rs +17 -1
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/presentation.rs +8 -0
- package/src/rust/queries/columns_iter_set.rs +7 -1
- package/src/rust/queries/get_viewer_config.rs +5 -1
- package/src/rust/renderer/dispatch.rs +85 -58
- package/src/rust/renderer.rs +34 -1
- package/src/rust/session/column_defaults_update.rs +18 -0
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +64 -9
- package/src/rust/session/replace_expression_update.rs +2 -0
- package/src/rust/session.rs +7 -5
- package/src/rust/tasks/create_panel.rs +18 -8
- package/src/rust/tasks/edit_window.rs +155 -0
- package/src/rust/tasks/mod.rs +2 -0
- package/src/rust/tasks/restore_panel.rs +34 -17
- package/src/rust/tasks/sync_update_panels.rs +7 -23
- package/src/rust/utils/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/debounce.rs +76 -27
- package/src/rust/workspace.rs +112 -20
- package/src/svg/pin-icon.svg +5 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/de.css +23 -0
- package/src/themes/intl/es.css +23 -0
- package/src/themes/intl/fr.css +23 -0
- package/src/themes/intl/ja.css +23 -0
- package/src/themes/intl/pt.css +23 -0
- package/src/themes/intl/zh.css +23 -0
- package/src/themes/intl.css +26 -1
- package/src/ts/column-format.ts +67 -46
- package/src/ts/perspective-viewer.ts +2 -0
- package/src/ts/providers.ts +96 -0
- package/src/ts/ts-rs/CompactDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyCode.ts +3 -0
- package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
- package/src/ts/ts-rs/CurrencySign.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
- package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
- package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
- package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
- package/src/ts/ts-rs/FormatMode.ts +3 -0
- package/src/ts/ts-rs/FormatUnit.ts +3 -0
- package/src/ts/ts-rs/Notation.ts +11 -0
- package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
- package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
- package/src/ts/ts-rs/RestoreOptions.ts +20 -0
- package/src/ts/ts-rs/RoundingMode.ts +3 -0
- package/src/ts/ts-rs/RoundingPriority.ts +3 -0
- package/src/ts/ts-rs/SignDisplay.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
- package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
- package/src/ts/ts-rs/StringColorMode.ts +3 -0
- package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
- package/src/ts/ts-rs/Unit.ts +3 -0
- package/src/ts/ts-rs/UnitDisplay.ts +3 -0
- package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
- package/src/ts/ts-rs/UseGrouping.ts +3 -0
- package/src/ts/ts-rs/ViewerConfig.ts +48 -1
- package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +47 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
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
|
+
use std::rc::Rc;
|
|
14
|
+
|
|
15
|
+
use perspective_js::utils::{ApiError, ApiResult};
|
|
16
|
+
use wasm_bindgen::prelude::*;
|
|
17
|
+
|
|
18
|
+
use super::client::{AgentTransport, ChatMessage, OnDelta, TurnError, run_turn};
|
|
19
|
+
use super::config::{AgentConfig, SystemRole};
|
|
20
|
+
use super::docs::DocsCell;
|
|
21
|
+
use super::tools::ToolCtx;
|
|
22
|
+
use crate::custom_elements::viewer::PerspectiveViewerElement;
|
|
23
|
+
|
|
24
|
+
/// The agent's system preamble — the ONE copy, authored as markdown and
|
|
25
|
+
/// embedded at compile time. This crate knows nothing of its structure:
|
|
26
|
+
/// [`AgentRuntime::preamble`] only chooses between this and a
|
|
27
|
+
/// bundle-supplied override, then appends the host's `systemPrompt`.
|
|
28
|
+
///
|
|
29
|
+
/// Its `search_docs` step is unconditional, which is why `search_docs` is
|
|
30
|
+
/// advertised unconditionally too (an unconfigured corpus searches EMPTY
|
|
31
|
+
/// rather than removing the tool) — one prompt, one tool surface, no
|
|
32
|
+
/// phantom-tool instructions either way.
|
|
33
|
+
const PREAMBLE: &str = include_str!("preamble.md");
|
|
34
|
+
|
|
35
|
+
/// The model-request budget per `prompt()` call — schema lookup + a few
|
|
36
|
+
/// config patches + expression iteration fit comfortably; overridable via
|
|
37
|
+
/// config `maxTurns`.
|
|
38
|
+
const DEFAULT_MAX_TURNS: usize = 16;
|
|
39
|
+
|
|
40
|
+
/// One configured agent: connection + conversation history + the tool
|
|
41
|
+
/// handle.
|
|
42
|
+
/// Owned by the element; created by `agentConfig()` and shared by every
|
|
43
|
+
/// `prompt()` call. The history `Mutex` serializes concurrent prompts.
|
|
44
|
+
pub struct AgentRuntime {
|
|
45
|
+
config: AgentConfig,
|
|
46
|
+
history: async_lock::Mutex<Vec<ChatMessage>>,
|
|
47
|
+
elem: PerspectiveViewerElement,
|
|
48
|
+
docs: Option<Rc<DocsCell>>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl AgentRuntime {
|
|
52
|
+
pub fn new(config: &JsValue, elem: PerspectiveViewerElement) -> ApiResult<Self> {
|
|
53
|
+
let config = AgentConfig::from_js(config)?;
|
|
54
|
+
let docs = config.docs.clone().map(|x| Rc::new(DocsCell::new(x)));
|
|
55
|
+
Ok(Self {
|
|
56
|
+
config,
|
|
57
|
+
history: async_lock::Mutex::new(vec![]),
|
|
58
|
+
elem,
|
|
59
|
+
docs,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Run one conversational turn (which may span many model requests and
|
|
64
|
+
/// tool calls) and return the final assistant text. History commits on
|
|
65
|
+
/// success — and on turn-budget exhaustion, whose transcript is coherent
|
|
66
|
+
/// and whose tool mutations persist in the viewer (see
|
|
67
|
+
/// [`TurnError::Budget`]); a transport-failed or aborted turn leaves the
|
|
68
|
+
/// conversation as it was.
|
|
69
|
+
pub async fn prompt(&self, prompt: String, on_delta: OnDelta<'_>) -> ApiResult<String> {
|
|
70
|
+
let mut history = self.history.lock().await;
|
|
71
|
+
let transport = self.transport()?;
|
|
72
|
+
|
|
73
|
+
// Resolve the docs metadata bundle BEFORE the first request so the
|
|
74
|
+
// turn's tool definitions carry the generated parameter schemas
|
|
75
|
+
// deterministically. A failed load does not abort the turn — the
|
|
76
|
+
// error is cached and surfaces through `search_docs`.
|
|
77
|
+
let bundle = match &self.docs {
|
|
78
|
+
Some(docs) => Some(docs.bundle().await),
|
|
79
|
+
None => None,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let ctx = ToolCtx {
|
|
83
|
+
docs: self.docs.clone(),
|
|
84
|
+
bundle,
|
|
85
|
+
entitlements: self.config.entitlements.clone(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// The preamble is a SYSTEM message by default, but some engines
|
|
89
|
+
// reject one outright when `tools` is present because they need
|
|
90
|
+
// to own the system prompt (WebLLM's Hermes function calling
|
|
91
|
+
// throws `CustomSystemPromptError` on ANY system message, then
|
|
92
|
+
// substitutes its own carrying the tool definitions). Those
|
|
93
|
+
// configure `systemRole: "user"`, which folds the same text into
|
|
94
|
+
// the conversation's opening user turn — sent once, then carried
|
|
95
|
+
// by history like any other message, which is why nothing is
|
|
96
|
+
// stripped on commit below.
|
|
97
|
+
let preamble = self.preamble(&ctx);
|
|
98
|
+
let mut messages = Vec::with_capacity(history.len() + 2);
|
|
99
|
+
let preamble_len = match self.config.system_role {
|
|
100
|
+
SystemRole::System => {
|
|
101
|
+
messages.push(ChatMessage::System {
|
|
102
|
+
content: preamble.clone(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
1
|
|
106
|
+
},
|
|
107
|
+
SystemRole::User => 0,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
messages.extend(history.iter().cloned());
|
|
111
|
+
let content = if preamble_len == 0 && history.is_empty() {
|
|
112
|
+
format!("{preamble}\n\n{prompt}")
|
|
113
|
+
} else {
|
|
114
|
+
prompt
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
messages.push(ChatMessage::User { content });
|
|
118
|
+
let result = run_turn(
|
|
119
|
+
&transport,
|
|
120
|
+
self.config.model_name(),
|
|
121
|
+
self.config.max_turns.unwrap_or(DEFAULT_MAX_TURNS),
|
|
122
|
+
&self.elem,
|
|
123
|
+
&ctx,
|
|
124
|
+
&mut messages,
|
|
125
|
+
self.config.system_role,
|
|
126
|
+
on_delta,
|
|
127
|
+
)
|
|
128
|
+
.await;
|
|
129
|
+
|
|
130
|
+
match result {
|
|
131
|
+
Ok(text) => {
|
|
132
|
+
*history = messages.split_off(preamble_len);
|
|
133
|
+
Ok(text)
|
|
134
|
+
},
|
|
135
|
+
Err(err @ TurnError::Budget { .. }) => {
|
|
136
|
+
*history = messages.split_off(preamble_len);
|
|
137
|
+
Err(err.into())
|
|
138
|
+
},
|
|
139
|
+
Err(err) => Err(err.into()),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Clear the conversation history.
|
|
144
|
+
pub async fn reset(&self) {
|
|
145
|
+
self.history.lock().await.clear();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Connection/model badge text, e.g. `"anthropic · claude-haiku-4-5"`.
|
|
149
|
+
pub fn label(&self) -> String {
|
|
150
|
+
format!(
|
|
151
|
+
"{} · {}",
|
|
152
|
+
self.config.label_name(),
|
|
153
|
+
self.config.model_name()
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Connection fields → transport. `url` XOR `engine` was validated at
|
|
158
|
+
/// configuration; `apiKey` is Bearer sugar and `headers` pass verbatim.
|
|
159
|
+
fn transport(&self) -> ApiResult<AgentTransport> {
|
|
160
|
+
if let Some(engine) = &self.config.engine {
|
|
161
|
+
return Ok(AgentTransport::Engine(engine.clone()));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let url = self
|
|
165
|
+
.config
|
|
166
|
+
.url
|
|
167
|
+
.clone()
|
|
168
|
+
.ok_or_else(|| ApiError::from("`url` or `engine` is required"))?;
|
|
169
|
+
|
|
170
|
+
let mut headers = self
|
|
171
|
+
.config
|
|
172
|
+
.headers
|
|
173
|
+
.clone()
|
|
174
|
+
.map(|x| x.into_iter().collect::<Vec<_>>())
|
|
175
|
+
.unwrap_or_default();
|
|
176
|
+
|
|
177
|
+
if let Some(key) = &self.config.api_key {
|
|
178
|
+
headers.push(("authorization".to_owned(), format!("Bearer {key}")));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
Ok(AgentTransport::Fetch { url, headers })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn preamble(&self, ctx: &ToolCtx) -> String {
|
|
185
|
+
let mut preamble = ctx
|
|
186
|
+
.bundle
|
|
187
|
+
.as_ref()
|
|
188
|
+
.and_then(|x| x.as_ref().as_ref().ok())
|
|
189
|
+
.and_then(|x| x.preamble.as_deref())
|
|
190
|
+
.unwrap_or(PREAMBLE)
|
|
191
|
+
.trim_end()
|
|
192
|
+
.to_owned();
|
|
193
|
+
|
|
194
|
+
if let Some(extra) = &self.config.system_prompt {
|
|
195
|
+
preamble.push_str("\n\n");
|
|
196
|
+
preamble.push_str(extra);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
preamble
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
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
|
+
//! [`AgentSlot`] — the shared agent model hung off `Presentation`. Holds the
|
|
14
|
+
//! configured [`AgentRuntime`] plus the UI-facing conversation state
|
|
15
|
+
//! (transcript, busy flag, abort handle) OUTSIDE the component tree, so the
|
|
16
|
+
//! chat panel is a pure view that can unmount (tab switch, settings close)
|
|
17
|
+
//! without losing the conversation or interrupting a running turn.
|
|
18
|
+
|
|
19
|
+
use std::cell::{Cell, RefCell};
|
|
20
|
+
use std::rc::Rc;
|
|
21
|
+
|
|
22
|
+
use futures::future::{AbortHandle, Abortable};
|
|
23
|
+
use perspective_js::utils::{ApiError, ApiResult};
|
|
24
|
+
|
|
25
|
+
use super::runtime::AgentRuntime;
|
|
26
|
+
use crate::utils::PubSub;
|
|
27
|
+
|
|
28
|
+
/// One rendered transcript entry. `Tool` entries are appended live as the
|
|
29
|
+
/// agent works, giving the chat UI its activity feed.
|
|
30
|
+
#[derive(Clone, PartialEq)]
|
|
31
|
+
pub enum ChatEntry {
|
|
32
|
+
User(String),
|
|
33
|
+
Assistant {
|
|
34
|
+
text: String,
|
|
35
|
+
|
|
36
|
+
/// Reasoning-model "thinking" captured during the turn —
|
|
37
|
+
/// display-only (never part of model history).
|
|
38
|
+
reasoning: Option<String>,
|
|
39
|
+
},
|
|
40
|
+
Tool {
|
|
41
|
+
name: String,
|
|
42
|
+
args: String,
|
|
43
|
+
|
|
44
|
+
/// Set after dispatch when the tool failed; the chip renders in
|
|
45
|
+
/// the theme error color with this text in its tooltip.
|
|
46
|
+
error: Option<String>,
|
|
47
|
+
},
|
|
48
|
+
Error(String),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// The in-flight turn's live tail: `(content, reasoning)` accumulated so
|
|
52
|
+
/// far by the streaming transport. View-state only — a turn that aborts
|
|
53
|
+
/// mid-stream discards it, and history is written exclusively from the
|
|
54
|
+
/// turn's final result.
|
|
55
|
+
type PendingTail = (String, String);
|
|
56
|
+
|
|
57
|
+
#[derive(Clone, Default)]
|
|
58
|
+
pub struct AgentSlot {
|
|
59
|
+
runtime: Rc<RefCell<Option<Rc<AgentRuntime>>>>,
|
|
60
|
+
transcript: Rc<RefCell<Vec<ChatEntry>>>,
|
|
61
|
+
pending: Rc<RefCell<Option<PendingTail>>>,
|
|
62
|
+
busy: Rc<Cell<bool>>,
|
|
63
|
+
abort: Rc<RefCell<Option<AbortHandle>>>,
|
|
64
|
+
|
|
65
|
+
/// Fires on any observable change: configuration, transcript append,
|
|
66
|
+
/// streaming delta, busy-flag flip.
|
|
67
|
+
pub on_update: Rc<PubSub<()>>,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
impl PartialEq for AgentSlot {
|
|
71
|
+
fn eq(&self, rhs: &Self) -> bool {
|
|
72
|
+
Rc::ptr_eq(&self.runtime, &rhs.runtime)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
impl AgentSlot {
|
|
77
|
+
/// Install (or replace) the runtime, discarding any in-flight turn and
|
|
78
|
+
/// the prior conversation.
|
|
79
|
+
pub fn configure(&self, runtime: AgentRuntime) {
|
|
80
|
+
self.abort_in_flight();
|
|
81
|
+
*self.runtime.borrow_mut() = Some(Rc::new(runtime));
|
|
82
|
+
self.transcript.borrow_mut().clear();
|
|
83
|
+
*self.pending.borrow_mut() = None;
|
|
84
|
+
self.on_update.emit(());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn is_configured(&self) -> bool {
|
|
88
|
+
self.runtime.borrow().is_some()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub fn is_busy(&self) -> bool {
|
|
92
|
+
self.busy.get()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Provider/model badge text for the chat UI.
|
|
96
|
+
pub fn label(&self) -> Option<String> {
|
|
97
|
+
self.runtime.borrow().as_ref().map(|x| x.label())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
pub fn transcript(&self) -> Vec<ChatEntry> {
|
|
101
|
+
self.transcript.borrow().clone()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Append a tool-activity entry (called by the tools themselves).
|
|
105
|
+
pub fn record_tool(&self, name: &str, args: &serde_json::Value) {
|
|
106
|
+
self.transcript.borrow_mut().push(ChatEntry::Tool {
|
|
107
|
+
name: name.to_owned(),
|
|
108
|
+
args: args.to_string(),
|
|
109
|
+
error: None,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
self.on_update.emit(());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Mark the most recent tool entry failed (called by the dispatcher
|
|
116
|
+
/// after the tool body settles — dispatch is sequential, so the last
|
|
117
|
+
/// `Tool` entry is the one that just ran).
|
|
118
|
+
pub fn record_tool_error(&self, message: &str) {
|
|
119
|
+
if let Some(ChatEntry::Tool { error, .. }) = self
|
|
120
|
+
.transcript
|
|
121
|
+
.borrow_mut()
|
|
122
|
+
.iter_mut()
|
|
123
|
+
.rev()
|
|
124
|
+
.find(|x| matches!(x, ChatEntry::Tool { .. }))
|
|
125
|
+
{
|
|
126
|
+
*error = Some(message.to_owned());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
self.on_update.emit(());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// The in-flight turn's streamed `(content, reasoning)`, if any.
|
|
133
|
+
pub fn pending(&self) -> Option<(String, String)> {
|
|
134
|
+
self.pending.borrow().clone()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Run one agent turn with transcript bookkeeping — the single execution
|
|
138
|
+
/// path shared by the chat UI and the headless `viewer.agentPrompt()`.
|
|
139
|
+
/// Errors (including cancellation via [`Self::stop`]) land in the
|
|
140
|
+
/// transcript AND propagate to the caller. Rejects if a turn is already
|
|
141
|
+
/// running.
|
|
142
|
+
pub async fn run_prompt(&self, prompt: String) -> ApiResult<String> {
|
|
143
|
+
let runtime = self
|
|
144
|
+
.runtime
|
|
145
|
+
.borrow()
|
|
146
|
+
.clone()
|
|
147
|
+
.ok_or_else(|| ApiError::from("`agentConfig()` has not been called"))?;
|
|
148
|
+
|
|
149
|
+
if self.busy.replace(true) {
|
|
150
|
+
return Err(ApiError::from("A prompt is already running"));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let (handle, registration) = AbortHandle::new_pair();
|
|
154
|
+
*self.abort.borrow_mut() = Some(handle);
|
|
155
|
+
self.transcript
|
|
156
|
+
.borrow_mut()
|
|
157
|
+
.push(ChatEntry::User(prompt.clone()));
|
|
158
|
+
|
|
159
|
+
self.on_update.emit(());
|
|
160
|
+
let on_delta = {
|
|
161
|
+
let pending = self.pending.clone();
|
|
162
|
+
let on_update = self.on_update.clone();
|
|
163
|
+
move |text: &str, reasoning: &str| {
|
|
164
|
+
*pending.borrow_mut() = Some((text.to_owned(), reasoning.to_owned()));
|
|
165
|
+
on_update.emit(());
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
let result = Abortable::new(runtime.prompt(prompt, &on_delta), registration).await;
|
|
170
|
+
*self.abort.borrow_mut() = None;
|
|
171
|
+
self.busy.set(false);
|
|
172
|
+
|
|
173
|
+
// The tail is view-state; the final entry is written from the
|
|
174
|
+
// turn's RESULT, keeping only the tail's reasoning (which has no
|
|
175
|
+
// other channel to the transcript).
|
|
176
|
+
let reasoning = self
|
|
177
|
+
.pending
|
|
178
|
+
.borrow_mut()
|
|
179
|
+
.take()
|
|
180
|
+
.map(|(_, x)| x)
|
|
181
|
+
.filter(|x| !x.is_empty());
|
|
182
|
+
|
|
183
|
+
let result = match result {
|
|
184
|
+
Ok(Ok(text)) => {
|
|
185
|
+
self.transcript.borrow_mut().push(ChatEntry::Assistant {
|
|
186
|
+
text: text.clone(),
|
|
187
|
+
reasoning,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
Ok(text)
|
|
191
|
+
},
|
|
192
|
+
Ok(Err(err)) => {
|
|
193
|
+
self.transcript
|
|
194
|
+
.borrow_mut()
|
|
195
|
+
.push(ChatEntry::Error(format!("{err}")));
|
|
196
|
+
|
|
197
|
+
Err(err)
|
|
198
|
+
},
|
|
199
|
+
Err(futures::future::Aborted) => {
|
|
200
|
+
self.transcript
|
|
201
|
+
.borrow_mut()
|
|
202
|
+
.push(ChatEntry::Error("Stopped".to_owned()));
|
|
203
|
+
|
|
204
|
+
Err(ApiError::from("Stopped"))
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
self.on_update.emit(());
|
|
209
|
+
result
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Cancel the in-flight turn (no-op when idle).
|
|
213
|
+
pub fn stop(&self) {
|
|
214
|
+
self.abort_in_flight();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// Clear the conversation (model history + transcript), keeping the
|
|
218
|
+
/// configuration.
|
|
219
|
+
pub async fn reset(&self) {
|
|
220
|
+
self.abort_in_flight();
|
|
221
|
+
let runtime = self.runtime.borrow().clone();
|
|
222
|
+
if let Some(runtime) = runtime {
|
|
223
|
+
runtime.reset().await;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
self.transcript.borrow_mut().clear();
|
|
227
|
+
*self.pending.borrow_mut() = None;
|
|
228
|
+
self.on_update.emit(());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn abort_in_flight(&self) {
|
|
232
|
+
if let Some(handle) = self.abort.borrow_mut().take() {
|
|
233
|
+
handle.abort();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|