1use dioxus::prelude::*;
30use logicaffeine_language::{compile_with_options, CompileOptions, OutputFormat, Interner, socratic_explanation};
31
32const LICENSE_VALIDATOR_URL: &str = "https://api.logicaffeine.com/validate";
34
35const VALIDATION_INTERVAL_MS: f64 = 24.0 * 60.0 * 60.0 * 1000.0;
37
38#[derive(Clone, PartialEq, Debug)]
42pub enum LicensePlan {
43 None,
45 Free,
47 Supporter,
49 Pro,
51 Premium,
53 Lifetime,
55 Enterprise,
57}
58
59impl LicensePlan {
60 pub fn from_str(s: &str) -> Self {
62 match s.to_lowercase().as_str() {
63 "free" => Self::Free,
64 "supporter" => Self::Supporter,
65 "pro" => Self::Pro,
66 "premium" => Self::Premium,
67 "lifetime" => Self::Lifetime,
68 "enterprise" => Self::Enterprise,
69 _ => Self::None,
70 }
71 }
72
73 pub fn is_commercial(&self) -> bool {
75 matches!(self, Self::Pro | Self::Premium | Self::Lifetime | Self::Enterprise)
76 }
77
78 pub fn is_paid(&self) -> bool {
80 !matches!(self, Self::None | Self::Free)
81 }
82}
83
84#[derive(Clone, PartialEq)]
89pub struct LicenseState {
90 pub key: Signal<Option<String>>,
92 pub plan: Signal<LicensePlan>,
94 pub is_valid: Signal<bool>,
96 pub validated_at: Signal<Option<f64>>,
98 pub is_validating: Signal<bool>,
100}
101
102impl LicenseState {
103 pub fn new() -> Self {
105 let (key, plan, validated_at) = load_license_from_storage();
106
107 Self {
108 key: Signal::new(key),
109 plan: Signal::new(plan),
110 is_valid: Signal::new(false),
111 validated_at: Signal::new(validated_at),
112 is_validating: Signal::new(false),
113 }
114 }
115
116 pub fn has_license(&self) -> bool {
117 self.key.read().is_some()
118 }
119
120 pub fn is_commercial(&self) -> bool {
121 self.plan.read().is_commercial() && *self.is_valid.read()
122 }
123
124 pub fn needs_revalidation(&self) -> bool {
125 match *self.validated_at.read() {
126 Some(timestamp) => {
127 let now = js_sys::Date::now();
128 now - timestamp > VALIDATION_INTERVAL_MS
129 }
130 None => true,
131 }
132 }
133
134 pub fn set_license(&mut self, license_key: String, plan: LicensePlan) {
135 self.key.set(Some(license_key.clone()));
136 self.plan.set(plan.clone());
137 self.is_valid.set(true);
138 let now = js_sys::Date::now();
139 self.validated_at.set(Some(now));
140
141 save_license_to_storage(&license_key, &plan, now);
142 }
143
144 pub fn clear_license(&mut self) {
145 self.key.set(None);
146 self.plan.set(LicensePlan::None);
147 self.is_valid.set(false);
148 self.validated_at.set(None);
149
150 clear_license_from_storage();
151 }
152
153 pub async fn validate(&mut self) {
154 let license_key = match self.key.read().clone() {
155 Some(key) => key,
156 None => return,
157 };
158
159 self.is_validating.set(true);
160
161 match validate_license_async(&license_key).await {
162 Ok((is_valid, plan)) => {
163 self.is_valid.set(is_valid);
164 if is_valid {
165 self.plan.set(plan);
166 let now = js_sys::Date::now();
167 self.validated_at.set(Some(now));
168 save_license_to_storage(&license_key, &self.plan.read(), now);
169 }
170 }
171 Err(_) => {
172 self.is_valid.set(false);
173 }
174 }
175
176 self.is_validating.set(false);
177 }
178}
179
180async fn validate_license_async(license_key: &str) -> Result<(bool, LicensePlan), String> {
181 use gloo_net::http::Request;
182
183 let body = serde_json::json!({ "licenseKey": license_key });
184
185 let response = Request::post(LICENSE_VALIDATOR_URL)
186 .header("Content-Type", "application/json")
187 .body(body.to_string())
188 .map_err(|e| e.to_string())?
189 .send()
190 .await
191 .map_err(|e| e.to_string())?;
192
193 if !response.ok() {
194 return Ok((false, LicensePlan::None));
195 }
196
197 let data: serde_json::Value = response
198 .json()
199 .await
200 .map_err(|e| e.to_string())?;
201
202 let is_valid = data["valid"].as_bool().unwrap_or(false);
203 let plan_str = data["plan"].as_str().unwrap_or("none");
204 let plan = LicensePlan::from_str(plan_str);
205
206 Ok((is_valid, plan))
207}
208
209fn load_license_from_storage() -> (Option<String>, LicensePlan, Option<f64>) {
210 #[cfg(target_arch = "wasm32")]
211 {
212 if let Some(window) = web_sys::window() {
213 if let Ok(Some(storage)) = window.local_storage() {
214 let key = storage.get_item("logos_license_key").ok().flatten();
215 let plan_str = storage.get_item("logos_license_plan").ok().flatten().unwrap_or_default();
216 let validated_at = storage
217 .get_item("logos_license_validated_at")
218 .ok()
219 .flatten()
220 .and_then(|s| s.parse::<f64>().ok());
221
222 let plan = LicensePlan::from_str(&plan_str);
223 return (key, plan, validated_at);
224 }
225 }
226 }
227 (None, LicensePlan::None, None)
228}
229
230fn save_license_to_storage(key: &str, plan: &LicensePlan, validated_at: f64) {
231 #[cfg(target_arch = "wasm32")]
232 {
233 if let Some(window) = web_sys::window() {
234 if let Ok(Some(storage)) = window.local_storage() {
235 let _ = storage.set_item("logos_license_key", key);
236 let plan_str = format!("{:?}", plan).to_lowercase();
237 let _ = storage.set_item("logos_license_plan", &plan_str);
238 let _ = storage.set_item("logos_license_validated_at", &validated_at.to_string());
239 }
240 }
241 }
242 #[cfg(not(target_arch = "wasm32"))]
243 let _ = (key, plan, validated_at);
244}
245
246fn clear_license_from_storage() {
247 #[cfg(target_arch = "wasm32")]
248 {
249 if let Some(window) = web_sys::window() {
250 if let Ok(Some(storage)) = window.local_storage() {
251 let _ = storage.remove_item("logos_license_key");
252 let _ = storage.remove_item("logos_license_plan");
253 let _ = storage.remove_item("logos_license_validated_at");
254 }
255 }
256 }
257}
258
259#[derive(Clone, PartialEq)]
261pub struct ChatMessage {
262 pub role: Role,
264 pub content: String,
266}
267
268#[derive(Clone, PartialEq)]
270pub enum Role {
271 User,
273 System,
275 Error,
277}
278
279#[derive(Clone, Copy)]
283pub struct AppState {
284 history: Signal<Vec<ChatMessage>>,
285}
286
287impl AppState {
288 pub fn new() -> Self {
289 Self {
290 history: Signal::new(vec![ChatMessage {
291 role: Role::System,
292 content: "The Council is assembled. State your premise.".to_string(),
293 }]),
294 }
295 }
296
297 pub fn add_user_message(&mut self, text: String) {
298 self.history.write().push(ChatMessage {
299 role: Role::User,
300 content: text.clone(),
301 });
302 self.process_logic(text);
303 }
304
305 fn process_logic(&mut self, input: String) {
306 let options = CompileOptions { format: OutputFormat::Unicode, pragmatic: false };
307
308 let response = match compile_with_options(&input, options) {
309 Ok(logic) => ChatMessage {
310 role: Role::System,
311 content: logic,
312 },
313 Err(e) => {
314 let interner = Interner::new();
315 let advice = socratic_explanation(&e, &interner);
316 ChatMessage {
317 role: Role::Error,
318 content: advice,
319 }
320 }
321 };
322 self.history.write().push(response);
323 }
324
325 pub fn get_history(&self) -> Vec<ChatMessage> {
326 self.history.read().clone()
327 }
328}
329
330const REGISTRY_API_URL: &str = "https://registry.logicaffeine.com";
336
337#[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
339pub struct GitHubUser {
340 pub id: String,
342 pub login: String,
344 pub name: Option<String>,
346 pub avatar_url: Option<String>,
348}
349
350#[derive(Clone, PartialEq)]
354pub struct RegistryAuthState {
355 pub user: Signal<Option<GitHubUser>>,
357 pub token: Signal<Option<String>>,
359 pub is_loading: Signal<bool>,
361}
362
363impl RegistryAuthState {
364 pub fn new() -> Self {
365 let (token, user) = load_registry_auth_from_storage();
366 Self {
367 user: Signal::new(user),
368 token: Signal::new(token),
369 is_loading: Signal::new(false),
370 }
371 }
372
373 pub fn is_authenticated(&self) -> bool {
374 self.token.read().is_some()
375 }
376
377 pub fn login(&mut self, token: String, user: GitHubUser) {
378 self.token.set(Some(token.clone()));
379 self.user.set(Some(user.clone()));
380 save_registry_auth_to_storage(&token, &user);
381 }
382
383 pub fn logout(&mut self) {
384 self.token.set(None);
385 self.user.set(None);
386 clear_registry_auth_from_storage();
387 }
388
389 pub fn get_auth_url() -> String {
390 format!("{}/auth/github", REGISTRY_API_URL)
391 }
392}
393
394fn load_registry_auth_from_storage() -> (Option<String>, Option<GitHubUser>) {
395 #[cfg(target_arch = "wasm32")]
396 {
397 if let Some(window) = web_sys::window() {
398 if let Ok(Some(storage)) = window.local_storage() {
399 let token = storage.get_item("logos_registry_token").ok().flatten();
400 let user_json = storage.get_item("logos_registry_user").ok().flatten();
401 let user = user_json.and_then(|j| serde_json::from_str(&j).ok());
402 return (token, user);
403 }
404 }
405 }
406 (None, None)
407}
408
409fn save_registry_auth_to_storage(token: &str, user: &GitHubUser) {
410 #[cfg(target_arch = "wasm32")]
411 {
412 if let Some(window) = web_sys::window() {
413 if let Ok(Some(storage)) = window.local_storage() {
414 let _ = storage.set_item("logos_registry_token", token);
415 if let Ok(json) = serde_json::to_string(user) {
416 let _ = storage.set_item("logos_registry_user", &json);
417 }
418 }
419 }
420 }
421}
422
423fn clear_registry_auth_from_storage() {
424 #[cfg(target_arch = "wasm32")]
425 {
426 if let Some(window) = web_sys::window() {
427 if let Ok(Some(storage)) = window.local_storage() {
428 let _ = storage.remove_item("logos_registry_token");
429 let _ = storage.remove_item("logos_registry_user");
430 }
431 }
432 }
433}
434
435#[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
441pub struct RegistryPackage {
442 pub name: String,
444 pub description: Option<String>,
446 pub latest_version: Option<String>,
448 pub owner: String,
450 pub owner_avatar: Option<String>,
452 pub verified: bool,
454 pub downloads: u64,
456 pub keywords: Vec<String>,
458}
459
460#[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
462pub struct PackageVersion {
463 pub version: String,
465 pub published_at: String,
467 pub size: u64,
469 pub yanked: bool,
471}
472
473#[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
475pub struct PackageDetails {
476 pub name: String,
478 pub description: Option<String>,
480 pub owner: String,
482 pub owner_avatar: Option<String>,
484 pub repository: Option<String>,
486 pub homepage: Option<String>,
488 pub license: Option<String>,
490 pub keywords: Vec<String>,
492 pub verified: bool,
494 pub downloads: u64,
496 pub readme: Option<String>,
498 pub versions: Vec<PackageVersion>,
500}
501
502#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
508pub enum StudioMode {
509 #[default]
511 Logic,
512 Code,
514 Math,
516 Hardware,
519}
520
521impl StudioMode {
522 pub fn extension(&self) -> &'static str {
524 match self {
525 StudioMode::Logic => "logic",
526 StudioMode::Code => "logos",
527 StudioMode::Math => "math",
528 StudioMode::Hardware => "hw",
529 }
530 }
531
532 pub fn display_name(&self) -> &'static str {
534 match self {
535 StudioMode::Logic => "Logic",
536 StudioMode::Code => "Code",
537 StudioMode::Math => "Math",
538 StudioMode::Hardware => "Hardware",
539 }
540 }
541
542 pub fn from_extension(ext: &str) -> Option<Self> {
544 match ext.to_lowercase().as_str() {
545 "logic" => Some(StudioMode::Logic),
546 "logos" => Some(StudioMode::Code),
547 "math" => Some(StudioMode::Math),
548 "hw" => Some(StudioMode::Hardware),
549 _ => None,
550 }
551 }
552}
553
554#[derive(Clone, PartialEq, Debug)]
556pub struct FileNode {
557 pub name: String,
559 pub path: String,
561 pub is_directory: bool,
563 pub children: Vec<FileNode>,
565 pub expanded: bool,
567}
568
569impl FileNode {
570 pub fn file(name: String, path: String) -> Self {
572 Self {
573 name,
574 path,
575 is_directory: false,
576 children: Vec::new(),
577 expanded: false,
578 }
579 }
580
581 pub fn directory(name: String, path: String) -> Self {
583 Self {
584 name,
585 path,
586 is_directory: true,
587 children: Vec::new(),
588 expanded: true,
589 }
590 }
591
592 pub fn root() -> Self {
594 Self {
595 name: "workspace".to_string(),
596 path: "/".to_string(),
597 is_directory: true,
598 children: Vec::new(),
599 expanded: true,
600 }
601 }
602
603 pub fn toggle_expanded(&mut self) {
605 if self.is_directory {
606 self.expanded = !self.expanded;
607 }
608 }
609
610 pub fn find_mut(&mut self, path: &str) -> Option<&mut FileNode> {
612 if self.path == path {
613 return Some(self);
614 }
615 for child in &mut self.children {
616 if let Some(found) = child.find_mut(path) {
617 return Some(found);
618 }
619 }
620 None
621 }
622}
623
624#[derive(Clone, PartialEq, Debug)]
626pub struct ReplLine {
627 pub input: String,
629 pub output: Result<String, String>,
631 pub success: bool,
633}
634
635impl ReplLine {
636 pub fn success(input: String, output: String) -> Self {
637 Self {
638 input,
639 output: Ok(output),
640 success: true,
641 }
642 }
643
644 pub fn error(input: String, error: String) -> Self {
645 Self {
646 input,
647 output: Err(error),
648 success: false,
649 }
650 }
651}
652
653#[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
655pub struct MathFormula {
656 pub latex: String,
658 pub label: Option<String>,
660}
661
662#[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
664pub struct MathDocument {
665 pub name: String,
667 pub formulas: Vec<MathFormula>,
669 #[serde(default)]
671 pub active_index: usize,
672}
673
674impl Default for MathDocument {
675 fn default() -> Self {
676 Self {
677 name: "Untitled".to_string(),
678 formulas: vec![MathFormula {
679 latex: String::new(),
680 label: None,
681 }],
682 active_index: 0,
683 }
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690
691 #[test]
692 fn test_license_plan_from_str() {
693 assert_eq!(LicensePlan::from_str("free"), LicensePlan::Free);
694 assert_eq!(LicensePlan::from_str("FREE"), LicensePlan::Free);
695 assert_eq!(LicensePlan::from_str("Free"), LicensePlan::Free);
696 assert_eq!(LicensePlan::from_str("supporter"), LicensePlan::Supporter);
697 assert_eq!(LicensePlan::from_str("pro"), LicensePlan::Pro);
698 assert_eq!(LicensePlan::from_str("premium"), LicensePlan::Premium);
699 assert_eq!(LicensePlan::from_str("lifetime"), LicensePlan::Lifetime);
700 assert_eq!(LicensePlan::from_str("enterprise"), LicensePlan::Enterprise);
701 assert_eq!(LicensePlan::from_str("unknown"), LicensePlan::None);
702 assert_eq!(LicensePlan::from_str(""), LicensePlan::None);
703 }
704
705 #[test]
706 fn test_license_plan_is_commercial() {
707 assert!(!LicensePlan::None.is_commercial());
708 assert!(!LicensePlan::Free.is_commercial());
709 assert!(!LicensePlan::Supporter.is_commercial());
710 assert!(LicensePlan::Pro.is_commercial());
711 assert!(LicensePlan::Premium.is_commercial());
712 assert!(LicensePlan::Lifetime.is_commercial());
713 assert!(LicensePlan::Enterprise.is_commercial());
714 }
715
716 #[test]
717 fn test_license_plan_is_paid() {
718 assert!(!LicensePlan::None.is_paid());
719 assert!(!LicensePlan::Free.is_paid());
720 assert!(LicensePlan::Supporter.is_paid());
721 assert!(LicensePlan::Pro.is_paid());
722 assert!(LicensePlan::Premium.is_paid());
723 assert!(LicensePlan::Lifetime.is_paid());
724 assert!(LicensePlan::Enterprise.is_paid());
725 }
726
727 #[test]
728 fn test_studio_mode_hardware() {
729 assert_eq!(StudioMode::Hardware.extension(), "hw");
731 assert_eq!(StudioMode::Hardware.display_name(), "Hardware");
732 assert_eq!(StudioMode::from_extension("hw"), Some(StudioMode::Hardware));
733 assert_eq!(StudioMode::from_extension("HW"), Some(StudioMode::Hardware));
734 assert_eq!(StudioMode::from_extension("logic"), Some(StudioMode::Logic));
736 assert_eq!(StudioMode::from_extension("logos"), Some(StudioMode::Code));
737 assert_eq!(StudioMode::from_extension("math"), Some(StudioMode::Math));
738 assert_eq!(StudioMode::from_extension("txt"), None);
739 }
740}