mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-24 01:38:29 +02:00
test: minio to devenv, improved testing s3 and relationships
This commit is contained in:
@@ -281,6 +281,33 @@ pub mod testing {
|
|||||||
use crate::utils::config::{AppConfig, PdfIngestMode};
|
use crate::utils::config::{AppConfig, PdfIngestMode};
|
||||||
use uuid;
|
use uuid;
|
||||||
|
|
||||||
|
const DEFAULT_TEST_S3_BUCKET: &str = "minne-tests";
|
||||||
|
const DEFAULT_TEST_S3_ENDPOINT: &str = "http://127.0.0.1:19000";
|
||||||
|
|
||||||
|
fn configured_test_s3_bucket() -> String {
|
||||||
|
std::env::var("MINNE_TEST_S3_BUCKET")
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.or_else(|| {
|
||||||
|
std::env::var("S3_BUCKET")
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| DEFAULT_TEST_S3_BUCKET.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_test_s3_endpoint() -> String {
|
||||||
|
std::env::var("MINNE_TEST_S3_ENDPOINT")
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.or_else(|| {
|
||||||
|
std::env::var("S3_ENDPOINT")
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| DEFAULT_TEST_S3_ENDPOINT.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a test configuration with memory storage.
|
/// Create a test configuration with memory storage.
|
||||||
///
|
///
|
||||||
/// This provides a ready-to-use configuration for testing scenarios
|
/// This provides a ready-to-use configuration for testing scenarios
|
||||||
@@ -326,7 +353,8 @@ pub mod testing {
|
|||||||
|
|
||||||
/// Create a test configuration with S3 storage (MinIO).
|
/// Create a test configuration with S3 storage (MinIO).
|
||||||
///
|
///
|
||||||
/// This requires a running MinIO instance on localhost:9000.
|
/// Uses `MINNE_TEST_S3_ENDPOINT` / `S3_ENDPOINT` and
|
||||||
|
/// `MINNE_TEST_S3_BUCKET` / `S3_BUCKET` when provided.
|
||||||
pub fn test_config_s3() -> AppConfig {
|
pub fn test_config_s3() -> AppConfig {
|
||||||
AppConfig {
|
AppConfig {
|
||||||
openai_api_key: "test".into(),
|
openai_api_key: "test".into(),
|
||||||
@@ -339,8 +367,8 @@ pub mod testing {
|
|||||||
http_port: 0,
|
http_port: 0,
|
||||||
openai_base_url: "..".into(),
|
openai_base_url: "..".into(),
|
||||||
storage: StorageKind::S3,
|
storage: StorageKind::S3,
|
||||||
s3_bucket: Some("minne-tests".into()),
|
s3_bucket: Some(configured_test_s3_bucket()),
|
||||||
s3_endpoint: Some("http://localhost:9000".into()),
|
s3_endpoint: Some(configured_test_s3_endpoint()),
|
||||||
s3_region: Some("us-east-1".into()),
|
s3_region: Some("us-east-1".into()),
|
||||||
pdf_ingest_mode: PdfIngestMode::LlmFirst,
|
pdf_ingest_mode: PdfIngestMode::LlmFirst,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -391,8 +419,7 @@ pub mod testing {
|
|||||||
|
|
||||||
/// Create a new TestStorageManager with S3 backend (MinIO).
|
/// Create a new TestStorageManager with S3 backend (MinIO).
|
||||||
///
|
///
|
||||||
/// This requires a running MinIO instance on localhost:9000 with
|
/// This requires a reachable MinIO endpoint and an existing test bucket.
|
||||||
/// default credentials (minioadmin/minioadmin) and a 'minne-tests' bucket.
|
|
||||||
pub async fn new_s3() -> object_store::Result<Self> {
|
pub async fn new_s3() -> object_store::Result<Self> {
|
||||||
// Ensure credentials are set for MinIO
|
// Ensure credentials are set for MinIO
|
||||||
// We set these env vars for the process, which AmazonS3Builder will pick up
|
// We set these env vars for the process, which AmazonS3Builder will pick up
|
||||||
@@ -403,6 +430,11 @@ pub mod testing {
|
|||||||
let cfg = test_config_s3();
|
let cfg = test_config_s3();
|
||||||
let storage = StorageManager::new(&cfg).await?;
|
let storage = StorageManager::new(&cfg).await?;
|
||||||
|
|
||||||
|
// Probe the bucket so tests can cleanly skip when the endpoint is unreachable
|
||||||
|
// or the test bucket is not provisioned.
|
||||||
|
let probe_prefix = format!("__minne_s3_probe__/{}", uuid::Uuid::new_v4());
|
||||||
|
storage.list(Some(&probe_prefix)).await?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
storage,
|
storage,
|
||||||
_temp_dir: None,
|
_temp_dir: None,
|
||||||
@@ -923,8 +955,8 @@ mod tests {
|
|||||||
assert_eq!(*test_storage.storage().backend_kind(), StorageKind::Memory);
|
assert_eq!(*test_storage.storage().backend_kind(), StorageKind::Memory);
|
||||||
}
|
}
|
||||||
|
|
||||||
// S3 Tests - Require MinIO on localhost:9000 with bucket 'minne-tests'
|
// S3 Tests - Require a reachable MinIO endpoint and test bucket.
|
||||||
// These tests will fail if MinIO is not running or bucket doesn't exist.
|
// `TestStorageManager::new_s3()` probes connectivity and these tests auto-skip when unavailable.
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_storage_manager_s3_basic_operations() {
|
async fn test_storage_manager_s3_basic_operations() {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ impl KnowledgeRelationship {
|
|||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
db_client
|
db_client
|
||||||
.client
|
.client
|
||||||
.query("DELETE knowledge_entity -> relates_to WHERE metadata.source_id = $source_id")
|
.query("DELETE FROM relates_to WHERE metadata.source_id = $source_id")
|
||||||
.bind(("source_id", source_id.to_owned()))
|
.bind(("source_id", source_id.to_owned()))
|
||||||
.await?
|
.await?
|
||||||
.check()?;
|
.check()?;
|
||||||
@@ -127,6 +127,34 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::storage::types::knowledge_entity::{KnowledgeEntity, KnowledgeEntityType};
|
use crate::storage::types::knowledge_entity::{KnowledgeEntity, KnowledgeEntityType};
|
||||||
|
|
||||||
|
async fn setup_test_db() -> SurrealDbClient {
|
||||||
|
let namespace = "test_ns";
|
||||||
|
let database = &Uuid::new_v4().to_string();
|
||||||
|
let db = SurrealDbClient::memory(namespace, database)
|
||||||
|
.await
|
||||||
|
.expect("Failed to start in-memory surrealdb");
|
||||||
|
|
||||||
|
db.apply_migrations()
|
||||||
|
.await
|
||||||
|
.expect("Failed to apply migrations");
|
||||||
|
|
||||||
|
db
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_relationship_by_id(
|
||||||
|
relationship_id: &str,
|
||||||
|
db_client: &SurrealDbClient,
|
||||||
|
) -> Option<KnowledgeRelationship> {
|
||||||
|
let mut result = db_client
|
||||||
|
.client
|
||||||
|
.query("SELECT * FROM type::thing('relates_to', $id)")
|
||||||
|
.bind(("id", relationship_id.to_owned()))
|
||||||
|
.await
|
||||||
|
.expect("relationship query by id failed");
|
||||||
|
|
||||||
|
result.take(0).expect("failed to take relationship by id")
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to create a test knowledge entity for the relationship tests
|
// Helper function to create a test knowledge entity for the relationship tests
|
||||||
async fn create_test_entity(name: &str, db_client: &SurrealDbClient) -> String {
|
async fn create_test_entity(name: &str, db_client: &SurrealDbClient) -> String {
|
||||||
let source_id = "source123".to_string();
|
let source_id = "source123".to_string();
|
||||||
@@ -178,15 +206,7 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_store_and_verify_by_source_id() {
|
async fn test_store_and_verify_by_source_id() {
|
||||||
// Setup in-memory database for testing
|
// Setup in-memory database for testing
|
||||||
let namespace = "test_ns";
|
let db = setup_test_db().await;
|
||||||
let database = &Uuid::new_v4().to_string();
|
|
||||||
let db = SurrealDbClient::memory(namespace, database)
|
|
||||||
.await
|
|
||||||
.expect("Failed to start in-memory surrealdb");
|
|
||||||
|
|
||||||
db.apply_migrations()
|
|
||||||
.await
|
|
||||||
.expect("Failed to apply migrations");
|
|
||||||
|
|
||||||
// Create two entities to relate
|
// Create two entities to relate
|
||||||
let entity1_id = create_test_entity("Entity 1", &db).await;
|
let entity1_id = create_test_entity("Entity 1", &db).await;
|
||||||
@@ -211,33 +231,33 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to store relationship");
|
.expect("Failed to store relationship");
|
||||||
|
|
||||||
|
let persisted = get_relationship_by_id(&relationship.id, &db)
|
||||||
|
.await
|
||||||
|
.expect("Relationship should be retrievable by id");
|
||||||
|
assert_eq!(persisted.in_, entity1_id);
|
||||||
|
assert_eq!(persisted.out, entity2_id);
|
||||||
|
assert_eq!(persisted.metadata.user_id, user_id);
|
||||||
|
assert_eq!(persisted.metadata.source_id, source_id);
|
||||||
|
|
||||||
// Query to verify the relationship exists by checking for relationships with our source_id
|
// Query to verify the relationship exists by checking for relationships with our source_id
|
||||||
// This approach is more reliable than trying to look up by ID
|
// This approach is more reliable than trying to look up by ID
|
||||||
let check_query = format!(
|
let mut check_result = db
|
||||||
"SELECT * FROM relates_to WHERE metadata.source_id = '{}'",
|
.query("SELECT * FROM relates_to WHERE metadata.source_id = $source_id")
|
||||||
source_id
|
.bind(("source_id", source_id.clone()))
|
||||||
);
|
.await
|
||||||
let mut check_result = db.query(check_query).await.expect("Check query failed");
|
.expect("Check query failed");
|
||||||
let check_results: Vec<KnowledgeRelationship> = check_result.take(0).unwrap_or_default();
|
let check_results: Vec<KnowledgeRelationship> = check_result.take(0).unwrap_or_default();
|
||||||
|
|
||||||
// Just verify that a relationship was created
|
assert_eq!(
|
||||||
assert!(
|
check_results.len(),
|
||||||
!check_results.is_empty(),
|
1,
|
||||||
"Relationship should exist in the database"
|
"Expected one relationship for source_id"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_store_relationship_resists_query_injection() {
|
async fn test_store_relationship_resists_query_injection() {
|
||||||
let namespace = "test_ns";
|
let db = setup_test_db().await;
|
||||||
let database = &Uuid::new_v4().to_string();
|
|
||||||
let db = SurrealDbClient::memory(namespace, database)
|
|
||||||
.await
|
|
||||||
.expect("Failed to start in-memory surrealdb");
|
|
||||||
|
|
||||||
db.apply_migrations()
|
|
||||||
.await
|
|
||||||
.expect("Failed to apply migrations");
|
|
||||||
|
|
||||||
let entity1_id = create_test_entity("Entity 1", &db).await;
|
let entity1_id = create_test_entity("Entity 1", &db).await;
|
||||||
let entity2_id = create_test_entity("Entity 2", &db).await;
|
let entity2_id = create_test_entity("Entity 2", &db).await;
|
||||||
@@ -273,11 +293,7 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_store_and_delete_relationship() {
|
async fn test_store_and_delete_relationship() {
|
||||||
// Setup in-memory database for testing
|
// Setup in-memory database for testing
|
||||||
let namespace = "test_ns";
|
let db = setup_test_db().await;
|
||||||
let database = &Uuid::new_v4().to_string();
|
|
||||||
let db = SurrealDbClient::memory(namespace, database)
|
|
||||||
.await
|
|
||||||
.expect("Failed to start in-memory surrealdb");
|
|
||||||
|
|
||||||
// Create two entities to relate
|
// Create two entities to relate
|
||||||
let entity1_id = create_test_entity("Entity 1", &db).await;
|
let entity1_id = create_test_entity("Entity 1", &db).await;
|
||||||
@@ -338,11 +354,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_relationship_by_id_unauthorized() {
|
async fn test_delete_relationship_by_id_unauthorized() {
|
||||||
let namespace = "test_ns";
|
let db = setup_test_db().await;
|
||||||
let database = &Uuid::new_v4().to_string();
|
|
||||||
let db = SurrealDbClient::memory(namespace, database)
|
|
||||||
.await
|
|
||||||
.expect("Failed to start in-memory surrealdb");
|
|
||||||
|
|
||||||
let entity1_id = create_test_entity("Entity 1", &db).await;
|
let entity1_id = create_test_entity("Entity 1", &db).await;
|
||||||
let entity2_id = create_test_entity("Entity 2", &db).await;
|
let entity2_id = create_test_entity("Entity 2", &db).await;
|
||||||
@@ -406,11 +418,7 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_store_relationship_exists() {
|
async fn test_store_relationship_exists() {
|
||||||
// Setup in-memory database for testing
|
// Setup in-memory database for testing
|
||||||
let namespace = "test_ns";
|
let db = setup_test_db().await;
|
||||||
let database = &Uuid::new_v4().to_string();
|
|
||||||
let db = SurrealDbClient::memory(namespace, database)
|
|
||||||
.await
|
|
||||||
.expect("Failed to start in-memory surrealdb");
|
|
||||||
|
|
||||||
// Create entities to relate
|
// Create entities to relate
|
||||||
let entity1_id = create_test_entity("Entity 1", &db).await;
|
let entity1_id = create_test_entity("Entity 1", &db).await;
|
||||||
@@ -462,49 +470,87 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to store different relationship");
|
.expect("Failed to store different relationship");
|
||||||
|
|
||||||
|
// Sanity-check setup: exactly two relationships use source_id and one uses different_source_id.
|
||||||
|
let mut before_delete = db
|
||||||
|
.query("SELECT * FROM relates_to WHERE metadata.source_id = $source_id")
|
||||||
|
.bind(("source_id", source_id.clone()))
|
||||||
|
.await
|
||||||
|
.expect("before delete query failed");
|
||||||
|
let before_delete_rows: Vec<KnowledgeRelationship> =
|
||||||
|
before_delete.take(0).unwrap_or_default();
|
||||||
|
assert_eq!(before_delete_rows.len(), 2);
|
||||||
|
|
||||||
|
let mut before_delete_different = db
|
||||||
|
.query("SELECT * FROM relates_to WHERE metadata.source_id = $source_id")
|
||||||
|
.bind(("source_id", different_source_id.clone()))
|
||||||
|
.await
|
||||||
|
.expect("before delete different query failed");
|
||||||
|
let before_delete_different_rows: Vec<KnowledgeRelationship> =
|
||||||
|
before_delete_different.take(0).unwrap_or_default();
|
||||||
|
assert_eq!(before_delete_different_rows.len(), 1);
|
||||||
|
|
||||||
// Delete relationships by source_id
|
// Delete relationships by source_id
|
||||||
KnowledgeRelationship::delete_relationships_by_source_id(&source_id, &db)
|
KnowledgeRelationship::delete_relationships_by_source_id(&source_id, &db)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to delete relationships by source_id");
|
.expect("Failed to delete relationships by source_id");
|
||||||
|
|
||||||
// Query to verify the relationships with source_id were deleted
|
// Query to verify the specific relationships with source_id were deleted.
|
||||||
let query1 = format!("SELECT * FROM relates_to WHERE id = '{}'", relationship1.id);
|
let result1 = get_relationship_by_id(&relationship1.id, &db).await;
|
||||||
let query2 = format!("SELECT * FROM relates_to WHERE id = '{}'", relationship2.id);
|
let result2 = get_relationship_by_id(&relationship2.id, &db).await;
|
||||||
let different_query = format!(
|
let different_result = get_relationship_by_id(&different_relationship.id, &db).await;
|
||||||
"SELECT * FROM relates_to WHERE id = '{}'",
|
|
||||||
different_relationship.id
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut result1 = db.query(query1).await.expect("Query 1 failed");
|
|
||||||
let results1: Vec<KnowledgeRelationship> = result1.take(0).unwrap_or_default();
|
|
||||||
|
|
||||||
let mut result2 = db.query(query2).await.expect("Query 2 failed");
|
|
||||||
let results2: Vec<KnowledgeRelationship> = result2.take(0).unwrap_or_default();
|
|
||||||
|
|
||||||
let mut different_result = db
|
|
||||||
.query(different_query)
|
|
||||||
.await
|
|
||||||
.expect("Different query failed");
|
|
||||||
let _different_results: Vec<KnowledgeRelationship> =
|
|
||||||
different_result.take(0).unwrap_or_default();
|
|
||||||
|
|
||||||
// Verify relationships with the source_id are deleted
|
// Verify relationships with the source_id are deleted
|
||||||
assert!(results1.is_empty(), "Relationship 1 should be deleted");
|
assert!(result1.is_none(), "Relationship 1 should be deleted");
|
||||||
assert!(results2.is_empty(), "Relationship 2 should be deleted");
|
assert!(result2.is_none(), "Relationship 2 should be deleted");
|
||||||
|
let remaining =
|
||||||
|
different_result.expect("Relationship with different source_id should remain");
|
||||||
|
assert_eq!(remaining.metadata.source_id, different_source_id);
|
||||||
|
}
|
||||||
|
|
||||||
// For the relationship with different source ID, we need to check differently
|
#[tokio::test]
|
||||||
// Let's just verify we have a relationship where the source_id matches different_source_id
|
async fn test_delete_relationships_by_source_id_resists_query_injection() {
|
||||||
let check_query = format!(
|
let db = setup_test_db().await;
|
||||||
"SELECT * FROM relates_to WHERE metadata.source_id = '{}'",
|
|
||||||
different_source_id
|
let entity1_id = create_test_entity("Entity 1", &db).await;
|
||||||
|
let entity2_id = create_test_entity("Entity 2", &db).await;
|
||||||
|
let entity3_id = create_test_entity("Entity 3", &db).await;
|
||||||
|
|
||||||
|
let safe_relationship = KnowledgeRelationship::new(
|
||||||
|
entity1_id.clone(),
|
||||||
|
entity2_id.clone(),
|
||||||
|
"user123".to_string(),
|
||||||
|
"safe_source".to_string(),
|
||||||
|
"references".to_string(),
|
||||||
);
|
);
|
||||||
let mut check_result = db.query(check_query).await.expect("Check query failed");
|
|
||||||
let check_results: Vec<KnowledgeRelationship> = check_result.take(0).unwrap_or_default();
|
|
||||||
|
|
||||||
// Verify the relationship with a different source_id still exists
|
let other_relationship = KnowledgeRelationship::new(
|
||||||
|
entity2_id,
|
||||||
|
entity3_id,
|
||||||
|
"user123".to_string(),
|
||||||
|
"other_source".to_string(),
|
||||||
|
"contains".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
safe_relationship
|
||||||
|
.store_relationship(&db)
|
||||||
|
.await
|
||||||
|
.expect("store safe relationship");
|
||||||
|
other_relationship
|
||||||
|
.store_relationship(&db)
|
||||||
|
.await
|
||||||
|
.expect("store other relationship");
|
||||||
|
|
||||||
|
KnowledgeRelationship::delete_relationships_by_source_id("safe_source' OR 1=1 --", &db)
|
||||||
|
.await
|
||||||
|
.expect("delete call should succeed");
|
||||||
|
|
||||||
|
let remaining_safe = get_relationship_by_id(&safe_relationship.id, &db).await;
|
||||||
|
let remaining_other = get_relationship_by_id(&other_relationship.id, &db).await;
|
||||||
|
|
||||||
|
assert!(remaining_safe.is_some(), "Safe relationship should remain");
|
||||||
assert!(
|
assert!(
|
||||||
!check_results.is_empty(),
|
remaining_other.is_some(),
|
||||||
"Relationship with different source_id should still exist"
|
"Other relationship should remain"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
devenv.nix
14
devenv.nix
@@ -30,6 +30,20 @@
|
|||||||
|
|
||||||
env = {
|
env = {
|
||||||
ORT_DYLIB_PATH = "${pkgs.onnxruntime}/lib/libonnxruntime.so";
|
ORT_DYLIB_PATH = "${pkgs.onnxruntime}/lib/libonnxruntime.so";
|
||||||
|
S3_ENDPOINT = "http://127.0.0.1:19000";
|
||||||
|
S3_BUCKET = "minne-tests";
|
||||||
|
MINNE_TEST_S3_ENDPOINT = "http://127.0.0.1:19000";
|
||||||
|
MINNE_TEST_S3_BUCKET = "minne-tests";
|
||||||
|
};
|
||||||
|
|
||||||
|
services.minio = {
|
||||||
|
enable = true;
|
||||||
|
listenAddress = "127.0.0.1:19000";
|
||||||
|
consoleAddress = "127.0.0.1:19001";
|
||||||
|
buckets = ["minne-tests"];
|
||||||
|
accessKey = "minioadmin";
|
||||||
|
secretKey = "minioadmin";
|
||||||
|
region = "us-east-1";
|
||||||
};
|
};
|
||||||
|
|
||||||
processes = {
|
processes = {
|
||||||
|
|||||||
@@ -285,6 +285,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.drawer-open {
|
||||||
|
> .drawer-side {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
> .drawer-toggle {
|
||||||
|
display: none;
|
||||||
|
& ~ .drawer-side {
|
||||||
|
pointer-events: auto;
|
||||||
|
visibility: visible;
|
||||||
|
position: sticky;
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
overscroll-behavior: auto;
|
||||||
|
opacity: 100%;
|
||||||
|
& > .drawer-overlay {
|
||||||
|
cursor: default;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
& > *:not(.drawer-overlay) {
|
||||||
|
translate: 0%;
|
||||||
|
[dir="rtl"] & {
|
||||||
|
translate: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:checked ~ .drawer-side {
|
||||||
|
pointer-events: auto;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.drawer-toggle {
|
.drawer-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: calc(0.25rem * 0);
|
height: calc(0.25rem * 0);
|
||||||
@@ -1043,6 +1074,22 @@
|
|||||||
grid-row-start: 1;
|
grid-row-start: 1;
|
||||||
min-width: calc(0.25rem * 0);
|
min-width: calc(0.25rem * 0);
|
||||||
}
|
}
|
||||||
|
.chat-image {
|
||||||
|
grid-row: span 2 / span 2;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
.chat-footer {
|
||||||
|
grid-row-start: 3;
|
||||||
|
display: flex;
|
||||||
|
gap: calc(0.25rem * 1);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
}
|
||||||
|
.chat-header {
|
||||||
|
grid-row-start: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: calc(0.25rem * 1);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
@@ -1749,6 +1796,9 @@
|
|||||||
.w-10 {
|
.w-10 {
|
||||||
width: calc(var(--spacing) * 10);
|
width: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
|
.w-11 {
|
||||||
|
width: calc(var(--spacing) * 11);
|
||||||
|
}
|
||||||
.w-11\/12 {
|
.w-11\/12 {
|
||||||
width: calc(11/12 * 100%);
|
width: calc(11/12 * 100%);
|
||||||
}
|
}
|
||||||
@@ -1812,6 +1862,9 @@
|
|||||||
.flex-none {
|
.flex-none {
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
.flex-shrink {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
.flex-shrink-0 {
|
.flex-shrink-0 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -1824,6 +1877,13 @@
|
|||||||
.grow {
|
.grow {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
.border-collapse {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.-translate-y-1 {
|
||||||
|
--tw-translate-y: calc(var(--spacing) * -1);
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
}
|
||||||
.-translate-y-1\/2 {
|
.-translate-y-1\/2 {
|
||||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
@@ -1896,6 +1956,9 @@
|
|||||||
.justify-start {
|
.justify-start {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
.gap-0 {
|
||||||
|
gap: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
.gap-0\.5 {
|
.gap-0\.5 {
|
||||||
gap: calc(var(--spacing) * 0.5);
|
gap: calc(var(--spacing) * 0.5);
|
||||||
}
|
}
|
||||||
@@ -2052,6 +2115,9 @@
|
|||||||
.bg-transparent {
|
.bg-transparent {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
.bg-warning {
|
||||||
|
background-color: var(--color-warning);
|
||||||
|
}
|
||||||
.bg-warning\/10 {
|
.bg-warning\/10 {
|
||||||
background-color: var(--color-warning);
|
background-color: var(--color-warning);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
@@ -2070,6 +2136,9 @@
|
|||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
|
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
.mask-repeat {
|
||||||
|
mask-repeat: repeat;
|
||||||
|
}
|
||||||
.fill-current {
|
.fill-current {
|
||||||
fill: currentcolor;
|
fill: currentcolor;
|
||||||
}
|
}
|
||||||
@@ -2100,6 +2169,9 @@
|
|||||||
.p-8 {
|
.p-8 {
|
||||||
padding: calc(var(--spacing) * 8);
|
padding: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
|
.px-1 {
|
||||||
|
padding-inline: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.px-1\.5 {
|
.px-1\.5 {
|
||||||
padding-inline: calc(var(--spacing) * 1.5);
|
padding-inline: calc(var(--spacing) * 1.5);
|
||||||
}
|
}
|
||||||
@@ -2254,6 +2326,9 @@
|
|||||||
--tw-tracking: var(--tracking-widest);
|
--tw-tracking: var(--tracking-widest);
|
||||||
letter-spacing: var(--tracking-widest);
|
letter-spacing: var(--tracking-widest);
|
||||||
}
|
}
|
||||||
|
.text-wrap {
|
||||||
|
text-wrap: wrap;
|
||||||
|
}
|
||||||
.break-words {
|
.break-words {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
@@ -2320,6 +2395,17 @@
|
|||||||
.italic {
|
.italic {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
.swap-active {
|
||||||
|
.swap-off {
|
||||||
|
opacity: 0%;
|
||||||
|
}
|
||||||
|
.swap-on {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
.opacity-0 {
|
.opacity-0 {
|
||||||
opacity: 0%;
|
opacity: 0%;
|
||||||
}
|
}
|
||||||
@@ -2410,6 +2496,10 @@
|
|||||||
--tw-duration: 300ms;
|
--tw-duration: 300ms;
|
||||||
transition-duration: 300ms;
|
transition-duration: 300ms;
|
||||||
}
|
}
|
||||||
|
.ease-in-out {
|
||||||
|
--tw-ease: var(--ease-in-out);
|
||||||
|
transition-timing-function: var(--ease-in-out);
|
||||||
|
}
|
||||||
.ease-out {
|
.ease-out {
|
||||||
--tw-ease: var(--ease-out);
|
--tw-ease: var(--ease-out);
|
||||||
transition-timing-function: var(--ease-out);
|
transition-timing-function: var(--ease-out);
|
||||||
|
|||||||
Reference in New Issue
Block a user