mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Refactor code for improved readability and functionality
- Simplified error handling in `useModelStatus` and `ModelProvider` by consolidating dispatch calls. - Streamlined greeting array initialization in `Welcome` component for better readability. - Enhanced `SkillProvider` to listen for skill state changes and dispatch updates accordingly. - Updated Rust backend to sync published state and emit events for skill state changes. - Introduced a `dirty` flag in `SkillState` to track modifications for efficient state management.
This commit is contained in:
@@ -256,7 +256,7 @@ impl QjsSkillInstance {
|
||||
log::info!("[skill:{}] Running (QuickJS)", config.skill_id);
|
||||
|
||||
// Run the event loop
|
||||
run_event_loop(&rt, &ctx, &mut rx, &state, &config.skill_id, &timer_state).await;
|
||||
run_event_loop(&rt, &ctx, &mut rx, &state, &config.skill_id, &timer_state, &published_state, _deps.app_handle.as_ref()).await;
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -270,7 +270,8 @@ impl QjsSkillInstance {
|
||||
/// 1. Polls for ready timers and fires their callbacks
|
||||
/// 2. Checks for incoming messages (non-blocking)
|
||||
/// 3. Runs the QuickJS job queue for promises/async ops
|
||||
/// 4. Sleeps efficiently when idle
|
||||
/// 4. Syncs published state from ops → instance and emits Tauri events
|
||||
/// 5. Sleeps efficiently when idle
|
||||
async fn run_event_loop(
|
||||
rt: &rquickjs::AsyncRuntime,
|
||||
ctx: &rquickjs::AsyncContext,
|
||||
@@ -278,6 +279,8 @@ async fn run_event_loop(
|
||||
state: &Arc<RwLock<SkillState>>,
|
||||
skill_id: &str,
|
||||
timer_state: &Arc<RwLock<qjs_ops::TimerState>>,
|
||||
ops_state: &Arc<RwLock<qjs_ops::SkillState>>,
|
||||
app_handle: Option<&tauri::AppHandle>,
|
||||
) {
|
||||
// Maximum sleep duration when no timers are pending
|
||||
const MAX_IDLE_SLEEP: Duration = Duration::from_millis(100);
|
||||
@@ -317,7 +320,34 @@ async fn run_event_loop(
|
||||
// 3. Drive QuickJS job queue (process pending promises)
|
||||
drive_jobs(rt).await;
|
||||
|
||||
// 4. Calculate sleep duration based on next timer
|
||||
// 4. Sync ops-level published state → instance published_state + emit event
|
||||
{
|
||||
let mut ops = ops_state.write();
|
||||
if ops.dirty {
|
||||
ops.dirty = false;
|
||||
// Convert serde_json::Map → HashMap for the instance snapshot
|
||||
let new_map: HashMap<String, serde_json::Value> = ops
|
||||
.data
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
state.write().published_state = new_map.clone();
|
||||
|
||||
// Emit Tauri event so the frontend picks up the change
|
||||
if let Some(handle) = app_handle {
|
||||
use tauri::Emitter;
|
||||
let _ = handle.emit(
|
||||
"skill-state-changed",
|
||||
serde_json::json!({
|
||||
"skillId": skill_id,
|
||||
"state": new_map,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Calculate sleep duration based on next timer
|
||||
let sleep_duration = {
|
||||
let (_, next_timer) = qjs_ops::poll_timers(timer_state);
|
||||
match next_timer {
|
||||
|
||||
@@ -35,6 +35,7 @@ pub fn register<'js>(
|
||||
serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?;
|
||||
let mut state = ss.write();
|
||||
state.data.insert(key, value);
|
||||
state.dirty = true;
|
||||
Ok(())
|
||||
},
|
||||
))?;
|
||||
@@ -50,6 +51,7 @@ pub fn register<'js>(
|
||||
for (k, v) in partial {
|
||||
state.data.insert(k, v);
|
||||
}
|
||||
state.dirty = true;
|
||||
Ok(())
|
||||
},
|
||||
))?;
|
||||
|
||||
@@ -86,12 +86,16 @@ pub struct SkillContext {
|
||||
pub struct SkillState {
|
||||
#[serde(flatten)]
|
||||
pub data: serde_json::Map<String, serde_json::Value>,
|
||||
/// Set to true when data is modified; the event loop clears it after syncing.
|
||||
#[serde(skip)]
|
||||
pub dirty: bool,
|
||||
}
|
||||
|
||||
impl Default for SkillState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data: serde_json::Map::new(),
|
||||
dirty: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useCallback } from 'react';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import {
|
||||
type ModelStatus,
|
||||
setDownloadTriggered,
|
||||
setModelError,
|
||||
setModelLoading,
|
||||
setModelStatus,
|
||||
type ModelStatus,
|
||||
} from '../store/modelSlice';
|
||||
|
||||
/**
|
||||
@@ -39,9 +39,7 @@ export const useModelStatus = () => {
|
||||
await fetchStatus();
|
||||
} catch (error) {
|
||||
console.error('[useModelStatus] Failed to start download:', error);
|
||||
dispatch(
|
||||
setModelError(error instanceof Error ? error.message : 'Failed to download model')
|
||||
);
|
||||
dispatch(setModelError(error instanceof Error ? error.message : 'Failed to download model'));
|
||||
}
|
||||
}, [dispatch, fetchStatus]);
|
||||
|
||||
|
||||
@@ -10,11 +10,7 @@ interface WelcomeProps {
|
||||
}
|
||||
|
||||
const Welcome = ({ isWeb }: WelcomeProps) => {
|
||||
const greetings = [
|
||||
'Hello HAL9000! 👋',
|
||||
"Let's cook! 🔥",
|
||||
'The A-Team is here! 👊',
|
||||
];
|
||||
const greetings = ['Hello HAL9000! 👋', "Let's cook! 🔥", 'The A-Team is here! 👊'];
|
||||
|
||||
const { isAvailable, isDownloaded, isLoading, downloadProgress, error, startDownload } =
|
||||
useModelStatus();
|
||||
@@ -64,9 +60,7 @@ const Welcome = ({ isWeb }: WelcomeProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPreparing && (
|
||||
<p className="text-xs opacity-50">Preparing AI model download...</p>
|
||||
)}
|
||||
{isPreparing && <p className="text-xs opacity-50">Preparing AI model download...</p>}
|
||||
|
||||
{error && !isLoading && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -4,11 +4,11 @@ import { useEffect } from 'react';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import {
|
||||
type ModelStatus,
|
||||
setDownloadTriggered,
|
||||
setModelError,
|
||||
setModelLoading,
|
||||
setModelStatus,
|
||||
type ModelStatus,
|
||||
} from '../store/modelSlice';
|
||||
|
||||
const POLL_INTERVAL = 1000;
|
||||
@@ -93,8 +93,7 @@ const ModelProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
if (!cancelled) dispatch(setModelStatus(finalStatus));
|
||||
} catch (err) {
|
||||
console.error('[ModelProvider] Download failed:', err);
|
||||
if (!cancelled)
|
||||
dispatch(setModelError(err instanceof Error ? err.message : String(err)));
|
||||
if (!cancelled) dispatch(setModelError(err instanceof Error ? err.message : String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
* engine, registers them in Redux, and auto-starts skills with completed setup.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { type ReactNode, useEffect, useRef } from 'react';
|
||||
|
||||
import { skillManager } from '../lib/skills/manager';
|
||||
import type { SkillManifest } from '../lib/skills/types';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import { setSkillState } from '../store/skillsSlice';
|
||||
import { DEV_AUTO_LOAD_SKILL, IS_DEV } from '../utils/config';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -53,8 +55,32 @@ async function discoverSkills(): Promise<SkillManifest[]> {
|
||||
export default function SkillProvider({ children }: { children: ReactNode }) {
|
||||
const { token } = useAppSelector(state => state.auth);
|
||||
const skillsState = useAppSelector(state => state.skills.skills);
|
||||
const dispatch = useAppDispatch();
|
||||
const initRef = useRef(false);
|
||||
|
||||
// Listen for skill state changes emitted from the Rust runtime event loop
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined;
|
||||
|
||||
listen<{ skillId: string; state: Record<string, unknown> }>(
|
||||
'skill-state-changed',
|
||||
event => {
|
||||
const { skillId, state: newState } = event.payload;
|
||||
dispatch(setSkillState({ skillId, state: newState }));
|
||||
}
|
||||
)
|
||||
.then(fn => {
|
||||
unlisten = fn;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[SkillProvider] Failed to listen for skill-state-changed:', err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten?.();
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
if (initRef.current) return;
|
||||
|
||||
Reference in New Issue
Block a user