mirror of https://github.com/buster-so/buster.git
merge staging
This commit is contained in:
commit
50e8e431b5
|
@ -233,6 +233,8 @@ pub enum AssetType {
|
|||
MetricFile,
|
||||
#[serde(rename = "dashboard")]
|
||||
DashboardFile,
|
||||
#[serde(rename = "report")]
|
||||
ReportFile,
|
||||
}
|
||||
|
||||
impl AssetType {
|
||||
|
@ -244,6 +246,7 @@ impl AssetType {
|
|||
AssetType::Chat => "chat",
|
||||
AssetType::MetricFile => "metric",
|
||||
AssetType::DashboardFile => "dashboard",
|
||||
AssetType::ReportFile => "report",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -591,6 +594,7 @@ impl ToSql<sql_types::AssetTypeEnum, Pg> for AssetType {
|
|||
AssetType::Chat => out.write_all(b"chat")?,
|
||||
AssetType::DashboardFile => out.write_all(b"dashboard_file")?,
|
||||
AssetType::MetricFile => out.write_all(b"metric_file")?,
|
||||
AssetType::ReportFile => out.write_all(b"report_file")?,
|
||||
}
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
@ -605,6 +609,7 @@ impl FromSql<sql_types::AssetTypeEnum, Pg> for AssetType {
|
|||
b"chat" => Ok(AssetType::Chat),
|
||||
b"dashboard_file" => Ok(AssetType::DashboardFile),
|
||||
b"metric_file" => Ok(AssetType::MetricFile),
|
||||
b"report_file" => Ok(AssetType::ReportFile),
|
||||
_ => Err("Unrecognized enum variant".into()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,6 +105,27 @@ pub struct MetricFile {
|
|||
pub workspace_sharing_enabled_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Insertable, Identifiable, Debug, Clone, Serialize)]
|
||||
#[diesel(table_name = report_files)]
|
||||
pub struct ReportFile {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub content: String,
|
||||
pub organization_id: Uuid,
|
||||
pub created_by: Uuid,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
pub publicly_accessible: bool,
|
||||
pub publicly_enabled_by: Option<Uuid>,
|
||||
pub public_expiry_date: Option<DateTime<Utc>>,
|
||||
pub version_history: VersionHistory,
|
||||
pub public_password: Option<String>,
|
||||
pub workspace_sharing: WorkspaceSharing,
|
||||
pub workspace_sharing_enabled_by: Option<Uuid>,
|
||||
pub workspace_sharing_enabled_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Insertable, Identifiable, Associations, Debug, Clone, Serialize)]
|
||||
#[diesel(belongs_to(Organization))]
|
||||
#[diesel(belongs_to(User, foreign_key = created_by))]
|
||||
|
|
|
@ -463,6 +463,30 @@ diesel::table! {
|
|||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use super::sql_types::WorkspaceSharingEnum;
|
||||
|
||||
report_files (id) {
|
||||
id -> Uuid,
|
||||
name -> Varchar,
|
||||
content -> Text,
|
||||
organization_id -> Uuid,
|
||||
created_by -> Uuid,
|
||||
created_at -> Timestamptz,
|
||||
updated_at -> Timestamptz,
|
||||
deleted_at -> Nullable<Timestamptz>,
|
||||
publicly_accessible -> Bool,
|
||||
publicly_enabled_by -> Nullable<Uuid>,
|
||||
public_expiry_date -> Nullable<Timestamptz>,
|
||||
version_history -> Jsonb,
|
||||
public_password -> Nullable<Text>,
|
||||
workspace_sharing -> WorkspaceSharingEnum,
|
||||
workspace_sharing_enabled_by -> Nullable<Uuid>,
|
||||
workspace_sharing_enabled_at -> Nullable<Timestamptz>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use super::sql_types::UserOrganizationRoleEnum;
|
||||
|
@ -757,6 +781,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||
metric_files_to_dashboard_files,
|
||||
metric_files_to_datasets,
|
||||
organizations,
|
||||
report_files,
|
||||
permission_groups,
|
||||
permission_groups_to_identities,
|
||||
permission_groups_to_users,
|
||||
|
|
|
@ -10,7 +10,7 @@ use database::{
|
|||
enums::AssetType,
|
||||
pool::get_pg_pool,
|
||||
models::UserFavorite,
|
||||
schema::{collections, collections_to_assets, dashboard_files, chats, messages_deprecated, threads_deprecated, user_favorites, metric_files},
|
||||
schema::{collections, collections_to_assets, dashboard_files, chats, messages_deprecated, threads_deprecated, user_favorites, metric_files, report_files},
|
||||
};
|
||||
|
||||
use middleware::AuthenticatedUser;
|
||||
|
@ -108,10 +108,21 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
|
|||
tokio::spawn(async move { get_favorite_chats(chat_ids).await })
|
||||
};
|
||||
|
||||
let (dashboard_fav_res, collection_fav_res, threads_fav_res, metrics_fav_res, chats_fav_res) =
|
||||
match tokio::try_join!(dashboard_favorites, collection_favorites, threads_favorites, metrics_favorites, chats_favorites) {
|
||||
Ok((dashboard_fav_res, collection_fav_res, threads_fav_res, metrics_fav_res, chats_fav_res)) => {
|
||||
(dashboard_fav_res, collection_fav_res, threads_fav_res, metrics_fav_res, chats_fav_res)
|
||||
let reports_favorites = {
|
||||
let report_ids = Arc::new(
|
||||
user_favorites
|
||||
.iter()
|
||||
.filter(|(_, f)| f == &AssetType::ReportFile)
|
||||
.map(|f| f.0)
|
||||
.collect::<Vec<Uuid>>(),
|
||||
);
|
||||
tokio::spawn(async move { get_favorite_reports(report_ids).await })
|
||||
};
|
||||
|
||||
let (dashboard_fav_res, collection_fav_res, threads_fav_res, metrics_fav_res, chats_fav_res, reports_fav_res) =
|
||||
match tokio::try_join!(dashboard_favorites, collection_favorites, threads_favorites, metrics_favorites, chats_favorites, reports_favorites) {
|
||||
Ok((dashboard_fav_res, collection_fav_res, threads_fav_res, metrics_fav_res, chats_fav_res, reports_fav_res)) => {
|
||||
(dashboard_fav_res, collection_fav_res, threads_fav_res, metrics_fav_res, chats_fav_res, reports_fav_res)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting favorite assets: {}", e);
|
||||
|
@ -159,6 +170,14 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
|
|||
}
|
||||
};
|
||||
|
||||
let favorite_reports = match reports_fav_res {
|
||||
Ok(reports) => reports,
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting favorite reports: {}", e);
|
||||
return Err(anyhow!("Error getting favorite reports: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let mut favorites: Vec<FavoriteObject> = Vec::with_capacity(user_favorites.len());
|
||||
|
||||
for favorite in &user_favorites {
|
||||
|
@ -211,6 +230,15 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
|
|||
});
|
||||
}
|
||||
}
|
||||
AssetType::ReportFile => {
|
||||
if let Some(report) = favorite_reports.iter().find(|r| r.id == favorite.0) {
|
||||
favorites.push(FavoriteObject {
|
||||
id: report.id,
|
||||
name: report.name.clone(),
|
||||
type_: AssetType::ReportFile,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -329,15 +357,20 @@ async fn get_assets_from_collections(
|
|||
tokio::spawn(async move { get_chats_from_collections(&collection_ids).await })
|
||||
};
|
||||
|
||||
let reports_handle = {
|
||||
let collection_ids = Arc::clone(&collection_ids);
|
||||
tokio::spawn(async move { get_reports_from_collections(&collection_ids).await })
|
||||
};
|
||||
|
||||
let collection_name_handle = {
|
||||
let collection_ids = Arc::clone(&collection_ids);
|
||||
tokio::spawn(async move { get_collection_names(&collection_ids).await })
|
||||
};
|
||||
|
||||
let (dashboards_res, metrics_res, chats_res, collection_name_res) =
|
||||
match tokio::join!(dashboards_handle, metrics_handle, chats_handle, collection_name_handle) {
|
||||
(Ok(dashboards), Ok(metrics), Ok(chats), Ok(collection_name)) => {
|
||||
(dashboards, metrics, chats, collection_name)
|
||||
let (dashboards_res, metrics_res, chats_res, reports_res, collection_name_res) =
|
||||
match tokio::join!(dashboards_handle, metrics_handle, chats_handle, reports_handle, collection_name_handle) {
|
||||
(Ok(dashboards), Ok(metrics), Ok(chats), Ok(reports), Ok(collection_name)) => {
|
||||
(dashboards, metrics, chats, reports, collection_name)
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
|
@ -361,6 +394,11 @@ async fn get_assets_from_collections(
|
|||
Err(e) => return Err(anyhow!("Error getting chats from collection: {:?}", e)),
|
||||
};
|
||||
|
||||
let reports = match reports_res {
|
||||
Ok(reports) => reports,
|
||||
Err(e) => return Err(anyhow!("Error getting reports from collection: {:?}", e)),
|
||||
};
|
||||
|
||||
let collection_names = match collection_name_res {
|
||||
Ok(collection_names) => collection_names,
|
||||
Err(e) => return Err(anyhow!("Error getting collection name: {:?}", e)),
|
||||
|
@ -407,6 +445,18 @@ async fn get_assets_from_collections(
|
|||
}),
|
||||
);
|
||||
|
||||
assets.extend(
|
||||
reports
|
||||
.iter()
|
||||
.filter_map(|(report_collection_id, favorite_object)| {
|
||||
if *report_collection_id == collection_id {
|
||||
Some(favorite_object.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
collection_favorites.push(FavoriteObject {
|
||||
id: collection_id,
|
||||
name: collection_name,
|
||||
|
@ -568,6 +618,50 @@ async fn get_chats_from_collections(
|
|||
Ok(chat_objects)
|
||||
}
|
||||
|
||||
async fn get_reports_from_collections(
|
||||
collection_ids: &[Uuid],
|
||||
) -> Result<Vec<(Uuid, FavoriteObject)>> {
|
||||
let mut conn = match get_pg_pool().get().await {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => return Err(anyhow!("Error getting connection from pool: {:?}", e)),
|
||||
};
|
||||
|
||||
let report_records: Vec<(Uuid, Uuid, String)> = match report_files::table
|
||||
.inner_join(
|
||||
collections_to_assets::table.on(report_files::id.eq(collections_to_assets::asset_id)),
|
||||
)
|
||||
.select((
|
||||
collections_to_assets::collection_id,
|
||||
report_files::id,
|
||||
report_files::name,
|
||||
))
|
||||
.filter(collections_to_assets::collection_id.eq_any(collection_ids))
|
||||
.filter(collections_to_assets::asset_type.eq(AssetType::ReportFile))
|
||||
.filter(report_files::deleted_at.is_null())
|
||||
.filter(collections_to_assets::deleted_at.is_null())
|
||||
.load::<(Uuid, Uuid, String)>(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(report_records) => report_records,
|
||||
Err(e) => return Err(anyhow!("Error loading report records: {:?}", e)),
|
||||
};
|
||||
|
||||
let report_objects: Vec<(Uuid, FavoriteObject)> = report_records
|
||||
.iter()
|
||||
.map(|(collection_id, id, name)| {
|
||||
(
|
||||
*collection_id,
|
||||
FavoriteObject {
|
||||
id: *id,
|
||||
name: name.clone(),
|
||||
type_: AssetType::ReportFile,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok(report_objects)
|
||||
}
|
||||
|
||||
async fn get_favorite_metrics(metric_ids: Arc<Vec<Uuid>>) -> Result<Vec<FavoriteObject>> {
|
||||
let mut conn = match get_pg_pool().get().await {
|
||||
Ok(conn) => conn,
|
||||
|
@ -597,6 +691,35 @@ async fn get_favorite_metrics(metric_ids: Arc<Vec<Uuid>>) -> Result<Vec<Favorite
|
|||
Ok(favorite_metrics)
|
||||
}
|
||||
|
||||
async fn get_favorite_reports(report_ids: Arc<Vec<Uuid>>) -> Result<Vec<FavoriteObject>> {
|
||||
let mut conn = match get_pg_pool().get().await {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => return Err(anyhow!("Error getting connection from pool: {:?}", e)),
|
||||
};
|
||||
|
||||
let report_records: Vec<(Uuid, String)> = match report_files::table
|
||||
.select((report_files::id, report_files::name))
|
||||
.filter(report_files::id.eq_any(report_ids.as_ref()))
|
||||
.filter(report_files::deleted_at.is_null())
|
||||
.load::<(Uuid, String)>(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(report_records) => report_records,
|
||||
Err(diesel::NotFound) => return Err(anyhow!("Reports not found")),
|
||||
Err(e) => return Err(anyhow!("Error loading report records: {:?}", e)),
|
||||
};
|
||||
|
||||
let favorite_reports = report_records
|
||||
.iter()
|
||||
.map(|(id, name)| FavoriteObject {
|
||||
id: *id,
|
||||
name: name.clone(),
|
||||
type_: AssetType::ReportFile,
|
||||
})
|
||||
.collect();
|
||||
Ok(favorite_reports)
|
||||
}
|
||||
|
||||
pub async fn update_favorites(user: &AuthenticatedUser, favorites: &[Uuid]) -> Result<()> {
|
||||
let mut conn = match get_pg_pool().get().await {
|
||||
Ok(conn) => conn,
|
||||
|
|
|
@ -45,7 +45,8 @@ const createCspHeader = (isEmbed = false) => {
|
|||
? `frame-ancestors 'self' *`
|
||||
: "frame-ancestors 'none'",
|
||||
// Frame sources - allow embeds from accepted domains
|
||||
"frame-src 'self' https://vercel.live https://*.twitter.com https://twitter.com https://*.x.com https://x.com https://*.youtube.com https://youtube.com https://*.youtube-nocookie.com https://youtube-nocookie.com https://*.youtu.be https://youtu.be https://*.vimeo.com https://vimeo.com ${publicUrlOrigin}",
|
||||
// Escape publicUrlOrigin to ensure it is a valid CSP source value
|
||||
`frame-src 'self' https://vercel.live https://*.twitter.com https://twitter.com https://*.x.com https://x.com https://*.youtube.com https://youtube.com https://*.youtube-nocookie.com https://youtube-nocookie.com https://*.youtu.be https://youtu.be https://*.vimeo.com https://vimeo.com ${publicUrlOrigin}`,
|
||||
// Connect sources for API calls
|
||||
(() => {
|
||||
const connectSources = [
|
||||
|
|
|
@ -1,218 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import DynamicReportEditor from '@/components/ui/report/DynamicReportEditor';
|
||||
import type { IReportEditor } from '@/components/ui/report/ReportEditor';
|
||||
import { AppSplitter } from '@/components/ui/layouts/AppSplitter';
|
||||
import { useMount } from '@/hooks';
|
||||
|
||||
export default function ReportPlayground() {
|
||||
// 150 lines of markdown about Red Rising, the sci-fi series by Pierce Brown.
|
||||
// This content is used as the initial value for the report editor playground.
|
||||
|
||||
const commonClassName = 'sm:px-[max(64px,calc(50%-350px))]';
|
||||
const onChangeContent = (value: string) => {
|
||||
console.log(value);
|
||||
};
|
||||
const readOnly = false;
|
||||
const mode = 'default';
|
||||
const onReadyProp = (editor: IReportEditor) => {
|
||||
console.log(editor);
|
||||
};
|
||||
const isStreamingMessage = false;
|
||||
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
useMount(() => {
|
||||
setTimeout(() => {
|
||||
setContent(contentOld);
|
||||
}, 1);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-[700px] overflow-hidden border-2 border-blue-700">
|
||||
<DynamicReportEditor
|
||||
value={content}
|
||||
placeholder="Start typing..."
|
||||
// disabled={false}
|
||||
className={commonClassName}
|
||||
variant="default"
|
||||
useFixedToolbarKit={false}
|
||||
onValueChange={onChangeContent}
|
||||
readOnly={readOnly}
|
||||
mode={mode}
|
||||
onReady={onReadyProp}
|
||||
isStreaming={isStreamingMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const contentOld = `
|
||||
# Red Rising: A Deep Dive into Pierce Brown's Dystopian Epic
|
||||
|
||||
Red Rising is a science fiction series by Pierce Brown that has captivated readers with its blend of dystopian intrigue, political machinations, and relentless action. Set in a future where society is rigidly divided by color-coded castes, the story follows Darrow, a lowborn Red, as he infiltrates the ruling Golds to spark a revolution.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [The World of Red Rising](#the-world-of-red-rising)
|
||||
3. [The Color Hierarchy](#the-color-hierarchy)
|
||||
4. [Main Characters](#main-characters)
|
||||
5. [Plot Overview](#plot-overview)
|
||||
6. [Themes](#themes)
|
||||
7. [The Institute](#the-institute)
|
||||
8. [Political Intrigue](#political-intrigue)
|
||||
9. [Technology and Society](#technology-and-society)
|
||||
10. [The Sons of Ares](#the-sons-of-ares)
|
||||
11. [The Golds](#the-golds)
|
||||
12. [The Rebellion](#the-rebellion)
|
||||
13. [Friendship and Betrayal](#friendship-and-betrayal)
|
||||
14. [The Role of Family](#the-role-of-family)
|
||||
15. [Violence and Sacrifice](#violence-and-sacrifice)
|
||||
16. [The Sequel Series](#the-sequel-series)
|
||||
17. [Critical Reception](#critical-reception)
|
||||
18. [Adaptations](#adaptations)
|
||||
19. [Conclusion](#conclusion)
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Red Rising is more than just a dystopian adventure; it's a meditation on power, identity, and the cost of revolution. Pierce Brown crafts a world that is both familiar and alien, drawing on classical influences and modern anxieties.
|
||||
|
||||
---
|
||||
|
||||
## The World of Red Rising
|
||||
|
||||
The series is set on Mars and other planets, terraformed and colonized by humanity. Society is organized into a strict hierarchy, with each Color assigned specific roles.
|
||||
|
||||
---
|
||||
|
||||
## The Color Hierarchy
|
||||
|
||||
- **Golds**: The ruling elite, genetically engineered for strength and intelligence.
|
||||
- **Silvers**: Financiers and administrators.
|
||||
- **Coppers**: Bureaucrats and record-keepers.
|
||||
- **Blues**: Pilots and navigators.
|
||||
- **Greens**: Programmers and technical experts.
|
||||
- **Yellows**: Doctors and scientists.
|
||||
- **Oranges**: Mechanics and engineers.
|
||||
- **Violets**: Artists and designers.
|
||||
- **Obsidians**: Warriors, bred for battle.
|
||||
- **Grays**: Soldiers and police.
|
||||
- **Browns**: Servants.
|
||||
- **Pinks**: Courtesans and companions.
|
||||
- **Reds**: Miners, the lowest caste, toiling beneath the surface.
|
||||
|
||||
---
|
||||
|
||||
## Main Characters
|
||||
|
||||
- **Darrow of Lykos**: The protagonist, a Red who becomes a Gold.
|
||||
- **Eo**: Darrow's wife, whose death inspires the rebellion.
|
||||
- **Sevro au Barca**: Darrow's loyal and unpredictable friend.
|
||||
- **Mustang (Virginia au Augustus)**: A Gold with a conscience, Darrow's ally and love interest.
|
||||
- **The Jackal (Adrius au Augustus)**: Mustang's brother, a cunning and ruthless adversary.
|
||||
- **Cassius au Bellona**: Darrow's friend-turned-rival.
|
||||
|
||||
---
|
||||
|
||||
## Plot Overview
|
||||
|
||||
Darrow, a Helldiver in the Martian mines, discovers that the surface is already habitable and that the Reds have been lied to for generations. After Eo's execution, Darrow is recruited by the Sons of Ares to infiltrate the Golds. He undergoes a painful transformation and enters the Institute, where he must survive brutal trials and political games.
|
||||
|
||||
---
|
||||
|
||||
## Themes
|
||||
|
||||
- **Class Struggle**: The series explores the consequences of rigid social hierarchies.
|
||||
- **Identity**: Darrow's transformation raises questions about selfhood and authenticity.
|
||||
- **Revolution**: The cost and necessity of rebellion are central to the narrative.
|
||||
- **Loyalty and Betrayal**: Friendships are tested in the crucible of war.
|
||||
|
||||
---
|
||||
|
||||
## The Institute
|
||||
|
||||
A brutal training ground for young Golds, the Institute is a microcosm of the larger society. Here, Darrow must lead, fight, and outwit his peers to survive.
|
||||
|
||||
---
|
||||
|
||||
## Political Intrigue
|
||||
|
||||
Red Rising is rife with shifting alliances, betrayals, and power plays. The Golds' society is as cutthroat as any battlefield.
|
||||
|
||||
---
|
||||
|
||||
## Technology and Society
|
||||
|
||||
From gravity manipulation to genetic engineering, technology shapes every aspect of life. Yet, ancient traditions and rituals persist.
|
||||
|
||||
---
|
||||
|
||||
## The Sons of Ares
|
||||
|
||||
A secret organization dedicated to overthrowing the Golds, the Sons of Ares are both idealistic and ruthless.
|
||||
|
||||
---
|
||||
|
||||
## The Golds
|
||||
|
||||
Engineered to be superior, the Golds are both admirable and monstrous. Their society values strength, cunning, and beauty above all.
|
||||
|
||||
---
|
||||
|
||||
## The Rebellion
|
||||
|
||||
Darrow's journey is the spark that ignites a galaxy-wide rebellion, challenging the very foundations of society.
|
||||
|
||||
---
|
||||
|
||||
## Friendship and Betrayal
|
||||
|
||||
Alliances are fragile, and trust is a rare commodity. Darrow's relationships are tested at every turn.
|
||||
|
||||
---
|
||||
|
||||
## The Role of Family
|
||||
|
||||
Family, both biological and chosen, is a recurring motif. Darrow's love for Eo and his loyalty to his friends drive much of the plot.
|
||||
|
||||
---
|
||||
|
||||
## Violence and Sacrifice
|
||||
|
||||
The series does not shy away from the brutality of revolution. Sacrifice is a constant companion.
|
||||
|
||||
---
|
||||
|
||||
## The Sequel Series
|
||||
|
||||
The original trilogy is followed by a new series, expanding the scope and stakes of the story.
|
||||
|
||||
---
|
||||
|
||||
## Critical Reception
|
||||
|
||||
Red Rising has been praised for its world-building, complex characters, and relentless pacing. Critics have compared it to classics like *The Hunger Games* and *Ender's Game*.
|
||||
|
||||
---
|
||||
|
||||
## Adaptations
|
||||
|
||||
Film and television adaptations have been in development, though none have yet reached the screen.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Red Rising is a thrilling, thought-provoking saga that challenges readers to question the world around them. Its blend of action, philosophy, and heart ensures its place among the greats of modern science fiction.
|
||||
|
||||
---
|
||||
|
||||
*“Break the chains, my love.”*
|
||||
|
||||
`.trim();
|
|
@ -3,18 +3,20 @@ import { ChevronDown } from '@/components/ui/icons';
|
|||
import { AppTooltip } from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
export const ReasoningScrollToBottom: React.FC<{
|
||||
export const ScrollToBottomButton: React.FC<{
|
||||
isAutoScrollEnabled: boolean;
|
||||
scrollToBottom: () => void;
|
||||
}> = React.memo(({ isAutoScrollEnabled, scrollToBottom }) => {
|
||||
className?: string;
|
||||
}> = React.memo(({ isAutoScrollEnabled, scrollToBottom, className }) => {
|
||||
return (
|
||||
<div
|
||||
data-testid="reasoning-scroll-to-bottom"
|
||||
data-testid="scroll-to-bottom-button"
|
||||
className={cn(
|
||||
'absolute right-4 bottom-4 z-10 duration-300',
|
||||
isAutoScrollEnabled
|
||||
? 'pointer-events-none scale-90 opacity-0'
|
||||
: 'pointer-events-auto scale-100 cursor-pointer opacity-100'
|
||||
: 'pointer-events-auto scale-100 cursor-pointer opacity-100',
|
||||
className
|
||||
)}>
|
||||
<AppTooltip title="Stick to bottom" sideOffset={12} delayDuration={500}>
|
||||
<button
|
||||
|
@ -30,4 +32,4 @@ export const ReasoningScrollToBottom: React.FC<{
|
|||
);
|
||||
});
|
||||
|
||||
ReasoningScrollToBottom.displayName = 'ReasoningScrollToBottom';
|
||||
ScrollToBottomButton.displayName = 'ScrollToBottomButton';
|
|
@ -1,6 +1,7 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { PlateContainer } from 'platejs/react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import React from 'react';
|
||||
|
||||
interface EditorContainerProps {
|
||||
className?: string;
|
||||
|
@ -9,7 +10,7 @@ interface EditorContainerProps {
|
|||
}
|
||||
|
||||
const editorContainerVariants = cva(
|
||||
'relative w-full cursor-text bg-transparent caret-primary select-text selection:bg-brand/15 focus-visible:outline-none [&_.slate-selection-area]:z-50 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',
|
||||
'relative w-full cursor-text bg-transparent select-text selection:bg-brand/15 focus-visible:outline-none [&_.slate-selection-area]:z-50 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',
|
||||
|
||||
{
|
||||
variants: {
|
||||
|
@ -37,22 +38,24 @@ const editorContainerVariants = cva(
|
|||
}
|
||||
);
|
||||
|
||||
export function EditorContainer({
|
||||
className,
|
||||
variant,
|
||||
readOnly,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> &
|
||||
VariantProps<typeof editorContainerVariants> &
|
||||
EditorContainerProps) {
|
||||
export const EditorContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> &
|
||||
VariantProps<typeof editorContainerVariants> &
|
||||
EditorContainerProps
|
||||
>(({ className, variant, readOnly, children, ...htmlProps }, ref) => {
|
||||
return (
|
||||
<PlateContainer
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'ignore-click-outside/toolbar',
|
||||
editorContainerVariants({ variant, readOnly }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{...htmlProps}>
|
||||
<PlateContainer>{children}</PlateContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
EditorContainer.displayName = 'EditorContainer';
|
||||
|
|
|
@ -56,7 +56,6 @@ const meta = {
|
|||
args: {
|
||||
placeholder: 'Start typing...',
|
||||
readOnly: false,
|
||||
disabled: false,
|
||||
variant: 'default'
|
||||
}
|
||||
} satisfies Meta<typeof ReportEditor>;
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
import type { Value, AnyPluginConfig } from 'platejs';
|
||||
import { Plate, type TPlateEditor } from 'platejs/react';
|
||||
import React, { useImperativeHandle, useRef } from 'react';
|
||||
import React, { useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import { useDebounceFn, useMemoizedFn } from '@/hooks';
|
||||
import { useAutoScroll } from '@/hooks/useAutoScroll';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Editor } from './Editor';
|
||||
import { EditorContainer } from './EditorContainer';
|
||||
|
@ -11,7 +12,7 @@ import { ThemeWrapper } from './ThemeWrapper/ThemeWrapper';
|
|||
import { useReportEditor } from './useReportEditor';
|
||||
import type { ReportElementsWithIds, ReportElementWithId } from '@buster/server-shared/reports';
|
||||
import { platejsToMarkdown } from './plugins/markdown-kit/platejs-conversions';
|
||||
import { ShimmerText } from '@/components/ui/typography/ShimmerText';
|
||||
import { ScrollToBottomButton } from '../buttons/ScrollToBottomButton';
|
||||
|
||||
interface ReportEditorProps {
|
||||
// We accept the generic Value type but recommend using ReportTypes.Value for type safety
|
||||
|
@ -29,7 +30,7 @@ interface ReportEditorProps {
|
|||
onReady?: (editor: IReportEditor) => void;
|
||||
id?: string;
|
||||
mode?: 'export' | 'default';
|
||||
children?: React.ReactNode;
|
||||
preEditorChildren?: React.ReactNode;
|
||||
postEditorChildren?: React.ReactNode;
|
||||
}
|
||||
|
||||
|
@ -59,13 +60,21 @@ export const ReportEditor = React.memo(
|
|||
useFixedToolbarKit = false,
|
||||
readOnly = false,
|
||||
isStreaming = false,
|
||||
children,
|
||||
preEditorChildren,
|
||||
postEditorChildren
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
// Initialize the editor instance using the custom useEditor hook
|
||||
const isReady = useRef(false);
|
||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { isAutoScrollEnabled, enableAutoScroll, disableAutoScroll, scrollToBottom } =
|
||||
useAutoScroll(editorContainerRef, {
|
||||
enabled: isStreaming,
|
||||
bottomThreshold: 50,
|
||||
observeSubTree: true
|
||||
});
|
||||
|
||||
const editor = useReportEditor({
|
||||
isStreaming,
|
||||
|
@ -118,15 +127,24 @@ export const ReportEditor = React.memo(
|
|||
wait: 1500
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isStreaming) {
|
||||
enableAutoScroll();
|
||||
} else {
|
||||
disableAutoScroll();
|
||||
}
|
||||
}, [isStreaming]);
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<Plate editor={editor} onValueChange={onValueChangeDebounced}>
|
||||
<EditorContainer
|
||||
ref={editorContainerRef}
|
||||
variant={variant}
|
||||
readOnly={readOnly}
|
||||
className={cn('editor-container relative overflow-auto', containerClassName)}>
|
||||
{children}
|
||||
{preEditorChildren}
|
||||
<ThemeWrapper id={id}>
|
||||
<Editor
|
||||
style={style}
|
||||
|
@ -137,6 +155,13 @@ export const ReportEditor = React.memo(
|
|||
/>
|
||||
</ThemeWrapper>
|
||||
{postEditorChildren}
|
||||
{isStreaming && (
|
||||
<ScrollToBottomButton
|
||||
isAutoScrollEnabled={isAutoScrollEnabled}
|
||||
scrollToBottom={scrollToBottom}
|
||||
className="fixed right-8 bottom-8 z-10"
|
||||
/>
|
||||
)}
|
||||
</EditorContainer>
|
||||
</Plate>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ export function HrElement(props: PlateElementProps) {
|
|||
<div className="py-6" contentEditable={false}>
|
||||
<hr
|
||||
className={cn(
|
||||
'bg-muted h-0.5 rounded-sm border-none bg-clip-content',
|
||||
'border-border border-0 border-t',
|
||||
selected && focused && 'ring-ring ring-2 ring-offset-2',
|
||||
!readOnly && 'cursor-pointer'
|
||||
)}
|
||||
|
|
|
@ -10,7 +10,7 @@ export function HrElementStatic(props: SlateElementProps) {
|
|||
return (
|
||||
<SlateElement {...props}>
|
||||
<div className="cursor-text py-6" contentEditable={false}>
|
||||
<hr className={cn('bg-muted h-0.5 rounded-sm border-none bg-clip-content')} />
|
||||
<hr className={cn('border-border border-0 border-t')} />
|
||||
</div>
|
||||
{props.children}
|
||||
</SlateElement>
|
||||
|
|
|
@ -25,11 +25,15 @@ export const MetricContent = React.memo(
|
|||
const reportId = useChatLayoutContextSelector((x) => x.reportId) || '';
|
||||
const reportVersionNumber = useChatLayoutContextSelector((x) => x.reportVersionNumber);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const hasBeenInViewport = useRef(false);
|
||||
|
||||
const [inViewport] = useInViewport(ref, {
|
||||
threshold: 0.33
|
||||
});
|
||||
const renderChart = inViewport || isExportMode;
|
||||
if (inViewport && !hasBeenInViewport.current) {
|
||||
hasBeenInViewport.current = true;
|
||||
}
|
||||
const renderChart = inViewport || isExportMode || hasBeenInViewport.current;
|
||||
|
||||
const {
|
||||
data: metric,
|
||||
|
|
|
@ -15,7 +15,7 @@ export function StreamingText(props: PlateTextProps) {
|
|||
<PlateText
|
||||
className={cn(
|
||||
'streaming-node',
|
||||
isStreaming && ['animate-highlight-fade'],
|
||||
isStreaming && 'animate-highlight-fade',
|
||||
// Only show the animated dot on the last streaming text node
|
||||
isLastStreamingText && [
|
||||
'after:ml-1.5 after:inline-block after:h-3 after:w-3 after:animate-pulse after:rounded-full after:bg-purple-500 after:align-middle after:content-[""]'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
type MdNodeParser,
|
||||
convertChildrenDeserialize,
|
||||
deserializeMd,
|
||||
parseAttributes,
|
||||
serializeMd
|
||||
} from '@platejs/markdown';
|
||||
|
@ -21,20 +22,34 @@ export const calloutSerializer: MdNodeParser<'callout'> = {
|
|||
|
||||
return {
|
||||
type: 'html',
|
||||
value: `<callout icon="${icon}">${content}</callout>`
|
||||
value: `<callout icon="${icon}" content="${content}"></callout>`
|
||||
};
|
||||
},
|
||||
deserialize: (node, deco, options) => {
|
||||
// Extract the icon attribute from the HTML element
|
||||
const typedAttributes = parseAttributes(node.attributes) as {
|
||||
icon: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
// Return the PlateJS node structure
|
||||
return {
|
||||
type: 'callout',
|
||||
icon: typedAttributes.icon,
|
||||
children: convertChildrenDeserialize(node.children, deco, options)
|
||||
};
|
||||
if (!options.editor) {
|
||||
throw new Error('Editor is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const deserializedContent = deserializeMd(options.editor, typedAttributes.content);
|
||||
return {
|
||||
type: 'callout',
|
||||
icon: typedAttributes.icon,
|
||||
children: deserializedContent
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error deserializing content', error);
|
||||
return {
|
||||
type: 'callout',
|
||||
icon: typedAttributes.icon,
|
||||
children: [{ text: typedAttributes.content }]
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -259,6 +259,30 @@ Here's an unordered list:
|
|||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('callout', async () => {
|
||||
const markdown = `<callout icon="💡" content="Testing123"></callout>`;
|
||||
const elements = await markdownToPlatejs(editor, markdown);
|
||||
const firstElement = elements[0];
|
||||
expect(firstElement.type).toBe('callout');
|
||||
expect(firstElement.icon).toBe('💡');
|
||||
expect(firstElement.children[0]).toEqual({ type: 'p', children: [{ text: 'Testing123' }] });
|
||||
});
|
||||
|
||||
it('callout and a metric', async () => {
|
||||
const markdown = `<metric metricId="33af38a8-c40f-437d-98ed-1ec78ce35232" width="100%" caption=""></metric>
|
||||
|
||||
<callout icon="💡">Testing123
|
||||
</callout>`;
|
||||
const elements = await markdownToPlatejs(editor, markdown);
|
||||
expect(elements).toBeDefined();
|
||||
const firstElement = elements[0];
|
||||
expect(firstElement.type).toBe('metric');
|
||||
expect(firstElement.metricId).toBe('33af38a8-c40f-437d-98ed-1ec78ce35232');
|
||||
const secondElement = elements[1];
|
||||
|
||||
console.log(JSON.stringify(elements, null, 2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('platejsToMarkdown', () => {
|
||||
|
@ -329,8 +353,9 @@ describe('platejsToMarkdown', () => {
|
|||
];
|
||||
const markdownFromPlatejs = await platejsToMarkdown(editor, elements);
|
||||
|
||||
const expectedMarkdown = `<callout icon="⚠️">This is a simple paragraph.\n</callout>\n`;
|
||||
expect(markdownFromPlatejs).toBe(expectedMarkdown);
|
||||
const expectedMarkdown = `<callout icon="⚠️" content="This is a simple paragraph.
|
||||
"></callout>`;
|
||||
expect(markdownFromPlatejs.trim()).toBe(expectedMarkdown.trim());
|
||||
});
|
||||
|
||||
it('should convert callout platejs element to markdown', async () => {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
|||
import { useAutoScroll } from '@/hooks/useAutoScroll';
|
||||
import { ReasoningMessageSelector } from './ReasoningMessages';
|
||||
import { BlackBoxMessage } from './ReasoningMessages/ReasoningBlackBoxMessage';
|
||||
import { ReasoningScrollToBottom } from './ReasoningScrollToBottom';
|
||||
import { ScrollToBottomButton } from '@/components/ui/buttons/ScrollToBottomButton';
|
||||
import type { BusterChatMessage, IBusterChat } from '@/api/asset_interfaces/chat';
|
||||
|
||||
interface ReasoningControllerProps {
|
||||
|
@ -79,7 +79,7 @@ export const ReasoningController: React.FC<ReasoningControllerProps> = ({ chatId
|
|||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<ReasoningScrollToBottom
|
||||
<ScrollToBottomButton
|
||||
isAutoScrollEnabled={isAutoScrollEnabled}
|
||||
scrollToBottom={scrollToBottom}
|
||||
/>
|
||||
|
|
|
@ -10,9 +10,7 @@ import { type IReportEditor } from '@/components/ui/report/ReportEditor';
|
|||
import { ReportEditorSkeleton } from '@/components/ui/report/ReportEditorSkeleton';
|
||||
import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext';
|
||||
import { useTrackAndUpdateReportChanges } from '@/api/buster-electric/reports/hooks';
|
||||
import { ShimmerText } from '@/components/ui/typography/ShimmerText';
|
||||
import { GeneratingContent } from './GeneratingContent';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
export const ReportPageController: React.FC<{
|
||||
reportId: string;
|
||||
|
@ -73,19 +71,20 @@ export const ReportPageController: React.FC<{
|
|||
mode={mode}
|
||||
onReady={onReadyProp}
|
||||
isStreaming={isStreamingMessage}
|
||||
preEditorChildren={
|
||||
<ReportPageHeader
|
||||
name={report?.name}
|
||||
updatedAt={report?.updated_at}
|
||||
onChangeName={onChangeName}
|
||||
className={commonClassName}
|
||||
isStreaming={isStreamingMessage}
|
||||
/>
|
||||
}
|
||||
postEditorChildren={
|
||||
showGeneratingContent ? (
|
||||
<GeneratingContent messageId={messageId} className={commonClassName} />
|
||||
) : null
|
||||
}>
|
||||
<ReportPageHeader
|
||||
name={report?.name}
|
||||
updatedAt={report?.updated_at}
|
||||
onChangeName={onChangeName}
|
||||
className={commonClassName}
|
||||
isStreaming={isStreamingMessage}
|
||||
/>
|
||||
</DynamicReportEditor>
|
||||
}></DynamicReportEditor>
|
||||
) : (
|
||||
<ReportEditorSkeleton />
|
||||
)}
|
||||
|
|
|
@ -155,7 +155,7 @@ const TitleCell = React.memo<{ name: string; chatId: string }>(({ name, chatId }
|
|||
<div className="mr-2 flex items-center" onClick={onFavoriteDivClick}>
|
||||
<FavoriteStar
|
||||
id={chatId}
|
||||
type={'chat'}
|
||||
type={'report'}
|
||||
iconStyle="tertiary"
|
||||
title={name}
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
|
|
|
@ -60,21 +60,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
highlightFade animation:
|
||||
- Animates a highlight background and a bottom border only (no outline).
|
||||
- The border is only on the bottom, not using outline.
|
||||
*/
|
||||
@keyframes highlightFade {
|
||||
0% {
|
||||
/* Use highlight background, fallback to brand, then yellow */
|
||||
background-color: var(--color-highlight-background, var(--color-purple-100, yellow));
|
||||
/* Only bottom border is visible at start */
|
||||
border-bottom: 1px solid var(--color-highlight-border, var(--color-purple-200, yellow));
|
||||
/* Use box-shadow instead of border - doesn't take up space */
|
||||
box-shadow: 0 1.5px 0 0 var(--color-highlight-border, var(--color-purple-300, yellow));
|
||||
}
|
||||
100% {
|
||||
background-color: var(--color-highlight-to-background, transparent);
|
||||
border-bottom: 0px solid var(--color-highlight-to-border, transparent);
|
||||
box-shadow: 0 0 0 0 var(--color-highlight-to-border, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue