mirror of https://github.com/buster-so/buster.git
commit
eb24d0fe0d
|
@ -12,6 +12,7 @@ members = [
|
||||||
"libs/dataset_security",
|
"libs/dataset_security",
|
||||||
"libs/email",
|
"libs/email",
|
||||||
"libs/stored_values",
|
"libs/stored_values",
|
||||||
|
"libs/raindrop",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ sqlx = { workspace = true }
|
||||||
stored_values = { path = "../stored_values" }
|
stored_values = { path = "../stored_values" }
|
||||||
tokio-retry = { workspace = true }
|
tokio-retry = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
raindrop = { path = "../raindrop" }
|
||||||
sql_analyzer = { path = "../sql_analyzer" }
|
sql_analyzer = { path = "../sql_analyzer" }
|
||||||
|
|
||||||
# Development dependencies
|
# Development dependencies
|
||||||
|
|
|
@ -11,9 +11,13 @@ use std::time::{Duration, Instant};
|
||||||
use std::{collections::HashMap, env, sync::Arc};
|
use std::{collections::HashMap, env, sync::Arc};
|
||||||
use tokio::sync::{broadcast, mpsc, RwLock};
|
use tokio::sync::{broadcast, mpsc, RwLock};
|
||||||
use tokio_retry::{strategy::ExponentialBackoff, Retry};
|
use tokio_retry::{strategy::ExponentialBackoff, Retry};
|
||||||
use tracing::{error, warn};
|
use tracing::{debug, error, info, instrument, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// Raindrop imports
|
||||||
|
use raindrop::types::{AiData as RaindropAiData, Event as RaindropEvent};
|
||||||
|
use raindrop::RaindropClient;
|
||||||
|
|
||||||
// Type definition for tool registry to simplify complex type
|
// Type definition for tool registry to simplify complex type
|
||||||
// No longer needed, defined below
|
// No longer needed, defined below
|
||||||
use crate::models::AgentThread;
|
use crate::models::AgentThread;
|
||||||
|
@ -561,6 +565,9 @@ impl Agent {
|
||||||
trace_builder: Option<TraceBuilder>,
|
trace_builder: Option<TraceBuilder>,
|
||||||
parent_span: Option<braintrust::Span>,
|
parent_span: Option<braintrust::Span>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
// Attempt to initialize Raindrop client (non-blocking)
|
||||||
|
let raindrop_client = RaindropClient::new().ok();
|
||||||
|
|
||||||
// Set the initial thread
|
// Set the initial thread
|
||||||
{
|
{
|
||||||
let mut current = agent.current_thread.write().await;
|
let mut current = agent.current_thread.write().await;
|
||||||
|
@ -721,6 +728,35 @@ impl Agent {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Track Request with Raindrop ---
|
||||||
|
if let Some(client) = raindrop_client.clone() {
|
||||||
|
let request_clone = request.clone(); // Clone request for tracking
|
||||||
|
let user_id = agent.user_id.clone();
|
||||||
|
let session_id = agent.session_id.to_string();
|
||||||
|
let current_history = agent.get_conversation_history().await.unwrap_or_default();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let event = RaindropEvent {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
event: "llm_request".to_string(),
|
||||||
|
properties: Some(HashMap::from([(
|
||||||
|
"conversation_history".to_string(),
|
||||||
|
serde_json::to_value(¤t_history).unwrap_or(Value::Null),
|
||||||
|
)])),
|
||||||
|
attachments: None,
|
||||||
|
ai_data: Some(RaindropAiData {
|
||||||
|
model: request_clone.model.clone(),
|
||||||
|
input: serde_json::to_string(&request_clone.messages).unwrap_or_default(),
|
||||||
|
output: "".to_string(), // Output is not known yet
|
||||||
|
convo_id: Some(session_id.clone()),
|
||||||
|
}),
|
||||||
|
event_id: None, // Raindrop assigns this
|
||||||
|
timestamp: Some(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
if let Err(e) = client.track_events(vec![event]).await {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// --- End Track Request ---
|
||||||
|
|
||||||
// --- Retry Logic for Initial Stream Request ---
|
// --- Retry Logic for Initial Stream Request ---
|
||||||
let retry_strategy = ExponentialBackoff::from_millis(100).take(3); // Retry 3 times, ~100ms, ~200ms, ~400ms
|
let retry_strategy = ExponentialBackoff::from_millis(100).take(3); // Retry 3 times, ~100ms, ~200ms, ~400ms
|
||||||
|
|
||||||
|
@ -980,6 +1016,37 @@ impl Agent {
|
||||||
// Update thread with assistant message
|
// Update thread with assistant message
|
||||||
agent.update_current_thread(final_message.clone()).await?;
|
agent.update_current_thread(final_message.clone()).await?;
|
||||||
|
|
||||||
|
// --- Track Response with Raindrop ---
|
||||||
|
if let Some(client) = raindrop_client {
|
||||||
|
let request_clone = request.clone(); // Clone again for response tracking
|
||||||
|
let final_message_clone = final_message.clone();
|
||||||
|
let user_id = agent.user_id.clone();
|
||||||
|
let session_id = agent.session_id.to_string();
|
||||||
|
// Get history *after* adding the final message
|
||||||
|
let current_history = agent.get_conversation_history().await.unwrap_or_default();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let event = RaindropEvent {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
event: "llm_response".to_string(),
|
||||||
|
properties: Some(HashMap::from([(
|
||||||
|
"conversation_history".to_string(),
|
||||||
|
serde_json::to_value(¤t_history).unwrap_or(Value::Null),
|
||||||
|
)])),
|
||||||
|
attachments: None,
|
||||||
|
ai_data: Some(RaindropAiData {
|
||||||
|
model: request_clone.model.clone(),
|
||||||
|
input: serde_json::to_string(&request_clone.messages).unwrap_or_default(),
|
||||||
|
output: serde_json::to_string(&final_message_clone).unwrap_or_default(),
|
||||||
|
convo_id: Some(session_id.clone()),
|
||||||
|
}),
|
||||||
|
event_id: None, // Raindrop assigns this
|
||||||
|
timestamp: Some(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
if let Err(e) = client.track_events(vec![event]).await {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// --- End Track Response ---
|
||||||
|
|
||||||
// Get the updated thread state AFTER adding the final assistant message
|
// Get the updated thread state AFTER adding the final assistant message
|
||||||
// This will be used for the potential recursive call later.
|
// This will be used for the potential recursive call later.
|
||||||
let mut updated_thread_for_recursion = agent
|
let mut updated_thread_for_recursion = agent
|
||||||
|
@ -1825,4 +1892,3 @@ mod tests {
|
||||||
assert_eq!(agent.get_state_bool("bool_key").await, None);
|
assert_eq!(agent.get_state_bool("bool_key").await, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "raindrop"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Workspace dependencies
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
|
||||||
|
|
||||||
|
# Non-workspace dependencies (if any, prefer workspace)
|
||||||
|
dotenvy = "0.15"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio-test = { workspace = true }
|
||||||
|
mockito = { workspace = true }
|
|
@ -0,0 +1,27 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
use reqwest::header::InvalidHeaderValue;
|
||||||
|
|
||||||
|
/// Custom error types for the Raindrop SDK.
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RaindropError {
|
||||||
|
#[error("Missing Raindrop API Write Key. Set the RAINDROP_WRITE_KEY environment variable.")]
|
||||||
|
MissingApiKey,
|
||||||
|
|
||||||
|
#[error("Invalid header value provided: {0}")]
|
||||||
|
InvalidHeaderValue(#[from] InvalidHeaderValue),
|
||||||
|
|
||||||
|
#[error("Failed to build HTTP client: {0}")]
|
||||||
|
HttpClientBuildError(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("HTTP request failed: {0}")]
|
||||||
|
RequestError(reqwest::Error),
|
||||||
|
|
||||||
|
#[error("Raindrop API error: {status} - {body}")]
|
||||||
|
ApiError {
|
||||||
|
status: reqwest::StatusCode,
|
||||||
|
body: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Failed to serialize request body: {0}")]
|
||||||
|
SerializationError(#[from] serde_json::Error),
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
#![doc = "A Rust SDK for interacting with the Raindrop.ai API."]
|
||||||
|
|
||||||
|
pub mod errors;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use reqwest::{Client, header};
|
||||||
|
use std::env;
|
||||||
|
use tracing::{debug, error, instrument};
|
||||||
|
|
||||||
|
use errors::RaindropError;
|
||||||
|
use types::{Event, Signal};
|
||||||
|
|
||||||
|
const DEFAULT_BASE_URL: &str = "https://api.raindrop.ai/v1";
|
||||||
|
|
||||||
|
/// Client for interacting with the Raindrop API.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RaindropClient {
|
||||||
|
client: Client,
|
||||||
|
base_url: String,
|
||||||
|
write_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RaindropClient {
|
||||||
|
/// Creates a new RaindropClient.
|
||||||
|
/// Reads the write key from the `RAINDROP_WRITE_KEY` environment variable.
|
||||||
|
/// Uses the default Raindrop API base URL.
|
||||||
|
pub fn new() -> Result<Self, RaindropError> {
|
||||||
|
let write_key = env::var("RAINDROP_WRITE_KEY")
|
||||||
|
.map_err(|_| RaindropError::MissingApiKey)?;
|
||||||
|
let base_url = DEFAULT_BASE_URL.to_string();
|
||||||
|
Self::build_client(write_key, base_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new RaindropClient with a specific write key and base URL.
|
||||||
|
/// Useful for testing or custom deployments.
|
||||||
|
pub fn with_key_and_url(write_key: String, base_url: &str) -> Result<Self, RaindropError> {
|
||||||
|
Self::build_client(write_key, base_url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the underlying reqwest client.
|
||||||
|
fn build_client(write_key: String, base_url: String) -> Result<Self, RaindropError> {
|
||||||
|
let mut headers = header::HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
header::AUTHORIZATION,
|
||||||
|
header::HeaderValue::from_str(&format!("Bearer {}", write_key))
|
||||||
|
.map_err(RaindropError::InvalidHeaderValue)?,
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
header::HeaderValue::from_static("application/json"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let client = Client::builder()
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()
|
||||||
|
.map_err(RaindropError::HttpClientBuildError)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
base_url,
|
||||||
|
write_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracks a batch of events.
|
||||||
|
#[instrument(skip(self, events), fields(count = events.len()))]
|
||||||
|
pub async fn track_events(&self, events: Vec<Event>) -> Result<(), RaindropError> {
|
||||||
|
if events.is_empty() {
|
||||||
|
debug!("No events to track, skipping API call.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let url = format!("{}/events/track", self.base_url);
|
||||||
|
self.post_data(&url, &events).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracks a batch of signals.
|
||||||
|
#[instrument(skip(self, signals), fields(count = signals.len()))]
|
||||||
|
pub async fn track_signals(&self, signals: Vec<Signal>) -> Result<(), RaindropError> {
|
||||||
|
if signals.is_empty() {
|
||||||
|
debug!("No signals to track, skipping API call.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let url = format!("{}/signals/track", self.base_url);
|
||||||
|
self.post_data(&url, &signals).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to POST JSON data to a specified URL.
|
||||||
|
async fn post_data<T: serde::Serialize>(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
data: &T,
|
||||||
|
) -> Result<(), RaindropError> {
|
||||||
|
debug!(url = url, "Sending POST request to Raindrop");
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(url)
|
||||||
|
.json(data)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(RaindropError::RequestError)?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
if status.is_success() {
|
||||||
|
debug!(url = url, status = %status, "Raindrop API call successful");
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let body = response.text().await.unwrap_or_else(|_| "Failed to read error body".to_string());
|
||||||
|
error!(url = url, status = %status, body = body, "Raindrop API call failed");
|
||||||
|
Err(RaindropError::ApiError { status, body })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// Represents a single event to be tracked.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Event {
|
||||||
|
pub user_id: String,
|
||||||
|
pub event: String,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub properties: Option<HashMap<String, Value>>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub attachments: Option<Vec<Attachment>>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ai_data: Option<AiData>,
|
||||||
|
|
||||||
|
// Optional fields provided by Raindrop API
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub event_id: Option<String>, // Returned by Raindrop, optional on send
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub timestamp: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents an attachment associated with an event.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Attachment {
|
||||||
|
#[serde(rename = "type")] // Use `type` keyword in JSON
|
||||||
|
pub attachment_type: String, // e.g., "image", "text", "json"
|
||||||
|
pub value: String, // URL or content
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub role: Option<String>, // e.g., "input", "output"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents AI-specific data for an event.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct AiData {
|
||||||
|
pub model: String,
|
||||||
|
pub input: String,
|
||||||
|
pub output: String,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub convo_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a single signal to be tracked.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Signal {
|
||||||
|
pub event_id: String, // The ID of the event this signal relates to
|
||||||
|
pub signal_name: String, // e.g., "thumbs_down", "corrected_answer"
|
||||||
|
pub signal_type: String, // e.g., "feedback", "correction"
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub properties: Option<HashMap<String, Value>>,
|
||||||
|
|
||||||
|
// Optional fields
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub timestamp: Option<DateTime<Utc>>,
|
||||||
|
}
|
|
@ -195,9 +195,6 @@ export const BusterChartJSComponent = React.memo(
|
||||||
if (selectedChartType === 'combo') return [ChartHoverBarPlugin, ChartTotalizerPlugin];
|
if (selectedChartType === 'combo') return [ChartHoverBarPlugin, ChartTotalizerPlugin];
|
||||||
return [];
|
return [];
|
||||||
}, [selectedChartType]);
|
}, [selectedChartType]);
|
||||||
console.log('datasetOptions', datasetOptions);
|
|
||||||
console.log('data', data);
|
|
||||||
console.log('options', options);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartMountedWrapper>
|
<ChartMountedWrapper>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { formatBarAndLineDataLabel } from './formatBarAndLineDataLabel';
|
import { formatBarAndLineDataLabel } from './formatBarAndLineDataLabel';
|
||||||
import { ColumnLabelFormat } from '@/api/asset_interfaces/metric';
|
import { ColumnLabelFormat } from '@/api/asset_interfaces/metric';
|
||||||
import { Context } from 'chartjs-plugin-datalabels';
|
import type { Context } from 'chartjs-plugin-datalabels';
|
||||||
|
|
||||||
describe('formatBarAndLineDataLabel', () => {
|
describe('formatBarAndLineDataLabel', () => {
|
||||||
it('formats a single value without percentage', () => {
|
it('formats a single value without percentage', () => {
|
||||||
|
@ -33,4 +33,89 @@ describe('formatBarAndLineDataLabel', () => {
|
||||||
|
|
||||||
expect(result).toBe('1,234.56');
|
expect(result).toBe('1,234.56');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mock chart context
|
||||||
|
const createMockContext = (datasets: any[]): Partial<Context> => ({
|
||||||
|
chart: {
|
||||||
|
data: {
|
||||||
|
datasets
|
||||||
|
},
|
||||||
|
$totalizer: {
|
||||||
|
stackTotals: [100],
|
||||||
|
seriesTotals: [50]
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
dataIndex: 0,
|
||||||
|
datasetIndex: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useStackTotal logic', () => {
|
||||||
|
const baseDataset = { hidden: false, isTrendline: false };
|
||||||
|
|
||||||
|
test('should use stack total when there are multiple visible datasets', () => {
|
||||||
|
const mockContext = createMockContext([baseDataset, { ...baseDataset }]) as Context;
|
||||||
|
|
||||||
|
const result = formatBarAndLineDataLabel(25, mockContext, 'data-label', {
|
||||||
|
style: 'number',
|
||||||
|
columnType: 'number'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 25 out of stack total (100) = 25%
|
||||||
|
expect(result).toBe('25%');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use stack total when percentageMode is stacked', () => {
|
||||||
|
const mockContext = createMockContext([baseDataset]) as Context;
|
||||||
|
|
||||||
|
const result = formatBarAndLineDataLabel(25, mockContext, 'stacked', {
|
||||||
|
style: 'number',
|
||||||
|
columnType: 'number'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 25 out of stack total (100) = 25%
|
||||||
|
expect(result).toBe('25%');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use series total for single dataset and non-stacked percentage mode', () => {
|
||||||
|
const mockContext = createMockContext([baseDataset]) as Context;
|
||||||
|
|
||||||
|
const result = formatBarAndLineDataLabel(25, mockContext, 'data-label', {
|
||||||
|
style: 'number',
|
||||||
|
columnType: 'number'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 25 out of series total (50) = 50%
|
||||||
|
expect(result).toBe('50%');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should ignore hidden datasets when counting multiple datasets', () => {
|
||||||
|
const mockContext = createMockContext([
|
||||||
|
baseDataset,
|
||||||
|
{ ...baseDataset, hidden: true }
|
||||||
|
]) as Context;
|
||||||
|
|
||||||
|
const result = formatBarAndLineDataLabel(25, mockContext, 'data-label', {
|
||||||
|
style: 'number',
|
||||||
|
columnType: 'number'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 25 out of series total (50) = 50% (since second dataset is hidden)
|
||||||
|
expect(result).toBe('50%');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should ignore trendline datasets when counting multiple datasets', () => {
|
||||||
|
const mockContext = createMockContext([
|
||||||
|
baseDataset,
|
||||||
|
{ ...baseDataset, isTrendline: true }
|
||||||
|
]) as Context;
|
||||||
|
|
||||||
|
const result = formatBarAndLineDataLabel(25, mockContext, 'data-label', {
|
||||||
|
style: 'number',
|
||||||
|
columnType: 'number'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 25 out of series total (50) = 50% (since second dataset is a trendline)
|
||||||
|
expect(result).toBe('50%');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const formatBarAndLineDataLabel = (
|
||||||
);
|
);
|
||||||
const hasMultipleDatasets = shownDatasets.length > 1;
|
const hasMultipleDatasets = shownDatasets.length > 1;
|
||||||
|
|
||||||
const useStackTotal = !hasMultipleDatasets || percentageMode === 'stacked';
|
const useStackTotal = hasMultipleDatasets || percentageMode === 'stacked';
|
||||||
|
|
||||||
const total: number = useStackTotal
|
const total: number = useStackTotal
|
||||||
? context.chart.$totalizer.stackTotals[context.dataIndex]
|
? context.chart.$totalizer.stackTotals[context.dataIndex]
|
||||||
|
|
|
@ -332,13 +332,12 @@ const getFormattedValueAndSetBarDataLabels = (
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const rawValue = context.dataset.data[context.dataIndex] as number;
|
const rawValue = context.dataset.data[context.dataIndex] as number;
|
||||||
const percentageModesMatch = context.chart.$barDataLabelsPercentageMode === percentageMode;
|
const formattedValue = formatBarAndLineDataLabel(
|
||||||
const currentValue = percentageModesMatch
|
rawValue,
|
||||||
? context.chart.$barDataLabels?.[context.datasetIndex]?.[context.dataIndex] || ''
|
context,
|
||||||
: '';
|
percentageMode,
|
||||||
|
columnLabelFormat
|
||||||
const formattedValue =
|
);
|
||||||
currentValue || formatBarAndLineDataLabel(rawValue, context, percentageMode, columnLabelFormat);
|
|
||||||
// Store only the formatted value, rotation is handled globally
|
// Store only the formatted value, rotation is handled globally
|
||||||
setBarDataLabelsManager(context, formattedValue, percentageMode);
|
setBarDataLabelsManager(context, formattedValue, percentageMode);
|
||||||
|
|
||||||
|
|
|
@ -52,14 +52,6 @@ export const useAutoChangeLayout = ({
|
||||||
const hasReasoning = !!lastReasoningMessageId;
|
const hasReasoning = !!lastReasoningMessageId;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(
|
|
||||||
'REASONING: useEffect',
|
|
||||||
isCompletedStream,
|
|
||||||
hasReasoning,
|
|
||||||
chatId,
|
|
||||||
lastMessageId,
|
|
||||||
isFinishedReasoning
|
|
||||||
);
|
|
||||||
//this will trigger when the chat is streaming and is has not completed yet (new chat)
|
//this will trigger when the chat is streaming and is has not completed yet (new chat)
|
||||||
if (
|
if (
|
||||||
!isCompletedStream &&
|
!isCompletedStream &&
|
||||||
|
@ -70,14 +62,11 @@ export const useAutoChangeLayout = ({
|
||||||
) {
|
) {
|
||||||
previousLastMessageId.current = lastMessageId;
|
previousLastMessageId.current = lastMessageId;
|
||||||
|
|
||||||
console.log('REASONING: FLIP TO REASONING!', lastMessageId);
|
|
||||||
|
|
||||||
onSetSelectedFile({ id: lastMessageId, type: 'reasoning', versionNumber: undefined });
|
onSetSelectedFile({ id: lastMessageId, type: 'reasoning', versionNumber: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
//this will when the chat is completed and it WAS streaming
|
//this will when the chat is completed and it WAS streaming
|
||||||
else if (isCompletedStream && previousIsCompletedStream === false) {
|
else if (isCompletedStream && previousIsCompletedStream === false) {
|
||||||
console.log('REASONING: SELECT STREAMING FILE');
|
|
||||||
const chatMessage = getChatMessageMemoized(lastMessageId);
|
const chatMessage = getChatMessageMemoized(lastMessageId);
|
||||||
const lastFileId = findLast(chatMessage?.response_message_ids, (id) => {
|
const lastFileId = findLast(chatMessage?.response_message_ids, (id) => {
|
||||||
const responseMessage = chatMessage?.response_messages[id];
|
const responseMessage = chatMessage?.response_messages[id];
|
||||||
|
@ -97,7 +86,6 @@ export const useAutoChangeLayout = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
console.log('auto change layout', link);
|
|
||||||
onChangePage(link);
|
onChangePage(link);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -105,7 +93,6 @@ export const useAutoChangeLayout = ({
|
||||||
}
|
}
|
||||||
//this will trigger on a page refresh and the chat is completed
|
//this will trigger on a page refresh and the chat is completed
|
||||||
else if (isCompletedStream && chatId) {
|
else if (isCompletedStream && chatId) {
|
||||||
console.log('REASONING: SELECT INITIAL CHAT FILE - PAGE LOAD');
|
|
||||||
const isChatOnlyMode = !metricId && !dashboardId && !messageId;
|
const isChatOnlyMode = !metricId && !dashboardId && !messageId;
|
||||||
if (isChatOnlyMode) {
|
if (isChatOnlyMode) {
|
||||||
return;
|
return;
|
||||||
|
@ -122,7 +109,6 @@ export const useAutoChangeLayout = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
console.log('auto change layout2', href);
|
|
||||||
onChangePage(href);
|
onChangePage(href);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ export const useChatLayoutContext = ({ appSplitterRef }: UseLayoutConfigProps) =
|
||||||
const chatParams = useGetChatParams();
|
const chatParams = useGetChatParams();
|
||||||
|
|
||||||
const animateOpenSplitter = useMemoizedFn((side: 'left' | 'right' | 'both') => {
|
const animateOpenSplitter = useMemoizedFn((side: 'left' | 'right' | 'both') => {
|
||||||
console.log('animateOpenSplitter', !!appSplitterRef.current, { side });
|
|
||||||
if (appSplitterRef.current) {
|
if (appSplitterRef.current) {
|
||||||
const { animateWidth, sizes } = appSplitterRef.current;
|
const { animateWidth, sizes } = appSplitterRef.current;
|
||||||
const leftSize = sizes[0] ?? 0;
|
const leftSize = sizes[0] ?? 0;
|
||||||
|
|
|
@ -88,11 +88,6 @@ export const useLayoutConfig = ({
|
||||||
fileId?: string | undefined;
|
fileId?: string | undefined;
|
||||||
secondaryView?: FileViewSecondary;
|
secondaryView?: FileViewSecondary;
|
||||||
}) => {
|
}) => {
|
||||||
console.log('onSetFileView', {
|
|
||||||
fileView,
|
|
||||||
fileId: fileIdProp,
|
|
||||||
secondaryView
|
|
||||||
});
|
|
||||||
const fileId = fileIdProp ?? selectedFileId;
|
const fileId = fileIdProp ?? selectedFileId;
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
onCollapseFileClick();
|
onCollapseFileClick();
|
||||||
|
@ -146,11 +141,7 @@ export const useLayoutConfig = ({
|
||||||
|
|
||||||
const closeSecondaryView = useMemoizedFn(async () => {
|
const closeSecondaryView = useMemoizedFn(async () => {
|
||||||
if (!selectedFileId || !selectedFileViewConfig || !selectedFileView) return;
|
if (!selectedFileId || !selectedFileViewConfig || !selectedFileView) return;
|
||||||
console.log('closeSecondaryView', {
|
|
||||||
selectedFileId,
|
|
||||||
selectedFileViewConfig,
|
|
||||||
selectedFileView
|
|
||||||
});
|
|
||||||
setFileViews((prev) => {
|
setFileViews((prev) => {
|
||||||
return create(prev, (draft) => {
|
return create(prev, (draft) => {
|
||||||
if (!draft[selectedFileId]?.fileViewConfig?.[selectedFileView]) return;
|
if (!draft[selectedFileId]?.fileViewConfig?.[selectedFileView]) return;
|
||||||
|
@ -162,9 +153,7 @@ export const useLayoutConfig = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const onCollapseFileClick = useMemoizedFn(async () => {
|
const onCollapseFileClick = useMemoizedFn(async () => {
|
||||||
console.log('onCollapseFileClick');
|
|
||||||
const isSecondaryViewOpen = !!selectedFileViewSecondary;
|
const isSecondaryViewOpen = !!selectedFileViewSecondary;
|
||||||
console.log('isSecondaryViewOpen', chatId, isSecondaryViewOpen);
|
|
||||||
|
|
||||||
if (isSecondaryViewOpen) {
|
if (isSecondaryViewOpen) {
|
||||||
closeSecondaryView();
|
closeSecondaryView();
|
||||||
|
@ -190,15 +179,6 @@ export const useLayoutConfig = ({
|
||||||
|
|
||||||
//we need to use for when the user clicks the back or forward in the browser
|
//we need to use for when the user clicks the back or forward in the browser
|
||||||
useUpdateEffect(() => {
|
useUpdateEffect(() => {
|
||||||
console.log('useUpdateEffect', {
|
|
||||||
metricId,
|
|
||||||
secondaryView,
|
|
||||||
chatId,
|
|
||||||
dashboardId,
|
|
||||||
messageId,
|
|
||||||
currentRoute
|
|
||||||
});
|
|
||||||
|
|
||||||
const newInitialFileViews = initializeFileViews({
|
const newInitialFileViews = initializeFileViews({
|
||||||
secondaryView,
|
secondaryView,
|
||||||
metricId,
|
metricId,
|
||||||
|
@ -218,17 +198,8 @@ export const useLayoutConfig = ({
|
||||||
currentRoute
|
currentRoute
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('isFileViewsChanged', isFileViewsChanged);
|
|
||||||
|
|
||||||
if (!isFileViewsChanged) return;
|
if (!isFileViewsChanged) return;
|
||||||
|
|
||||||
console.log('setting file view', {
|
|
||||||
newInitialFileViews,
|
|
||||||
fileId,
|
|
||||||
fileView,
|
|
||||||
secondaryView
|
|
||||||
});
|
|
||||||
|
|
||||||
onSetFileView({
|
onSetFileView({
|
||||||
fileId,
|
fileId,
|
||||||
fileView,
|
fileView,
|
||||||
|
|
|
@ -31,9 +31,7 @@ export const useSelectedFile = ({
|
||||||
* @param file
|
* @param file
|
||||||
*/
|
*/
|
||||||
const onSetSelectedFile = useMemoizedFn(async (file: SelectedFile | null) => {
|
const onSetSelectedFile = useMemoizedFn(async (file: SelectedFile | null) => {
|
||||||
console.log('onSetSelectedFile', file);
|
|
||||||
const handleFileCollapse = shouldCloseSplitter(file, selectedFile, appSplitterRef);
|
const handleFileCollapse = shouldCloseSplitter(file, selectedFile, appSplitterRef);
|
||||||
console.log('handleFileCollapse', handleFileCollapse);
|
|
||||||
|
|
||||||
if (file && chatParams.chatId) {
|
if (file && chatParams.chatId) {
|
||||||
const link = assetParamsToRoute({
|
const link = assetParamsToRoute({
|
||||||
|
@ -43,8 +41,6 @@ export const useSelectedFile = ({
|
||||||
versionNumber: file?.versionNumber
|
versionNumber: file?.versionNumber
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('link', link);
|
|
||||||
|
|
||||||
if (link) onChangePage(link);
|
if (link) onChangePage(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,24 +47,24 @@ export const compareObjectsByKeys = <K extends string>(
|
||||||
if (typeof val1 === 'object' && typeof val2 === 'object') {
|
if (typeof val1 === 'object' && typeof val2 === 'object') {
|
||||||
const itWasEqual = isEqual(val1, val2) || isEqual(JSON.stringify(val1), JSON.stringify(val2));
|
const itWasEqual = isEqual(val1, val2) || isEqual(JSON.stringify(val1), JSON.stringify(val2));
|
||||||
|
|
||||||
if (!itWasEqual) {
|
// if (!itWasEqual) {
|
||||||
console.log('--------------NESTED KEYS NOT EQUAL------------------');
|
// console.log('--------------NESTED KEYS NOT EQUAL------------------');
|
||||||
console.log('KEY', key);
|
// console.log('KEY', key);
|
||||||
console.log('ORIGINAL', val1);
|
// console.log('ORIGINAL', val1);
|
||||||
console.log('NEW', val2);
|
// console.log('NEW', val2);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return itWasEqual;
|
return itWasEqual;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itWasEqual = isEqual(val1, val2);
|
const itWasEqual = isEqual(val1, val2);
|
||||||
|
|
||||||
if (!itWasEqual) {
|
// if (!itWasEqual) {
|
||||||
console.log('--------------KEYS NOT EQUAL------------------');
|
// console.log('--------------KEYS NOT EQUAL------------------');
|
||||||
console.log('KEY', key);
|
// console.log('KEY', key);
|
||||||
console.log('ORIGINAL', val1);
|
// console.log('ORIGINAL', val1);
|
||||||
console.log('NEW', val2);
|
// console.log('NEW', val2);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return itWasEqual;
|
return itWasEqual;
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue