fix(desktop): DeerFlow UI — ChatArea refactor + ai-elements + dead CSS cleanup
ChatArea retry button uses setInput instead of direct sendToGateway, fix bootstrap spinner stuck for non-logged-in users, remove dead CSS (aurora-title/sidebar-open/quick-action-chips), add ai components (ReasoningBlock/StreamingText/ChatMode/ModelSelector/TaskProgress), add ClassroomPlayer + ResizableChatLayout + artifact panel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
223
desktop/src-tauri/src/classroom_commands/chat.rs
Normal file
223
desktop/src-tauri/src/classroom_commands/chat.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
//! Classroom multi-agent chat commands
|
||||
//!
|
||||
//! - `classroom_chat` — send a message and receive multi-agent responses
|
||||
//! - `classroom_chat_history` — retrieve chat history for a classroom
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
use zclaw_kernel::generation::{
|
||||
AgentProfile, AgentRole,
|
||||
ClassroomChatMessage, ClassroomChatState,
|
||||
ClassroomChatRequest,
|
||||
build_chat_prompt, parse_chat_responses,
|
||||
};
|
||||
use zclaw_runtime::CompletionRequest;
|
||||
|
||||
use super::ClassroomStore;
|
||||
use crate::kernel_commands::KernelState;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Chat state store: classroom_id → chat state
|
||||
pub type ChatStore = Arc<Mutex<std::collections::HashMap<String, ClassroomChatState>>>;
|
||||
|
||||
pub fn create_chat_state() -> ChatStore {
|
||||
Arc::new(Mutex::new(std::collections::HashMap::new()))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Request / Response
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClassroomChatCmdRequest {
|
||||
pub classroom_id: String,
|
||||
pub user_message: String,
|
||||
pub scene_context: Option<String>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Send a message in the classroom chat and get multi-agent responses.
|
||||
#[tauri::command]
|
||||
pub async fn classroom_chat(
|
||||
store: State<'_, ClassroomStore>,
|
||||
chat_store: State<'_, ChatStore>,
|
||||
kernel_state: State<'_, KernelState>,
|
||||
request: ClassroomChatCmdRequest,
|
||||
) -> Result<Vec<ClassroomChatMessage>, String> {
|
||||
if request.user_message.trim().is_empty() {
|
||||
return Err("Message cannot be empty".to_string());
|
||||
}
|
||||
|
||||
// Get classroom data
|
||||
let classroom = {
|
||||
let s = store.lock().await;
|
||||
s.get(&request.classroom_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("Classroom '{}' not found", request.classroom_id))?
|
||||
};
|
||||
|
||||
// Create user message
|
||||
let user_msg = ClassroomChatMessage::user_message(&request.user_message);
|
||||
|
||||
// Get chat history for context
|
||||
let history: Vec<ClassroomChatMessage> = {
|
||||
let cs = chat_store.lock().await;
|
||||
cs.get(&request.classroom_id)
|
||||
.map(|s| s.messages.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
// Try LLM-powered multi-agent responses, fallback to placeholder
|
||||
let agent_responses = match generate_llm_responses(&kernel_state, &classroom.agents, &request.user_message, request.scene_context.as_deref(), &history).await {
|
||||
Ok(responses) => responses,
|
||||
Err(e) => {
|
||||
tracing::warn!("LLM chat generation failed, using placeholders: {}", e);
|
||||
generate_placeholder_responses(
|
||||
&classroom.agents,
|
||||
&request.user_message,
|
||||
request.scene_context.as_deref(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Store in chat state
|
||||
{
|
||||
let mut cs = chat_store.lock().await;
|
||||
let state = cs.entry(request.classroom_id.clone())
|
||||
.or_insert_with(|| ClassroomChatState {
|
||||
messages: vec![],
|
||||
active: true,
|
||||
});
|
||||
|
||||
state.messages.push(user_msg);
|
||||
state.messages.extend(agent_responses.clone());
|
||||
}
|
||||
|
||||
Ok(agent_responses)
|
||||
}
|
||||
|
||||
/// Retrieve chat history for a classroom
|
||||
#[tauri::command]
|
||||
pub async fn classroom_chat_history(
|
||||
chat_store: State<'_, ChatStore>,
|
||||
classroom_id: String,
|
||||
) -> Result<Vec<ClassroomChatMessage>, String> {
|
||||
let cs = chat_store.lock().await;
|
||||
Ok(cs.get(&classroom_id)
|
||||
.map(|s| s.messages.clone())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Placeholder response generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn generate_placeholder_responses(
|
||||
agents: &[AgentProfile],
|
||||
user_message: &str,
|
||||
scene_context: Option<&str>,
|
||||
) -> Vec<ClassroomChatMessage> {
|
||||
let mut responses = Vec::new();
|
||||
|
||||
// Teacher always responds
|
||||
if let Some(teacher) = agents.iter().find(|a| a.role == AgentRole::Teacher) {
|
||||
let context_hint = scene_context
|
||||
.map(|ctx| format!("关于「{}」,", ctx))
|
||||
.unwrap_or_default();
|
||||
|
||||
responses.push(ClassroomChatMessage::agent_message(
|
||||
teacher,
|
||||
&format!("{}这是一个很好的问题!让我来详细解释一下「{}」的核心概念...", context_hint, user_message),
|
||||
));
|
||||
}
|
||||
|
||||
// Assistant chimes in
|
||||
if let Some(assistant) = agents.iter().find(|a| a.role == AgentRole::Assistant) {
|
||||
responses.push(ClassroomChatMessage::agent_message(
|
||||
assistant,
|
||||
"我来补充一下要点 📌",
|
||||
));
|
||||
}
|
||||
|
||||
// One student responds
|
||||
if let Some(student) = agents.iter().find(|a| a.role == AgentRole::Student) {
|
||||
responses.push(ClassroomChatMessage::agent_message(
|
||||
student,
|
||||
&format!("谢谢老师!我大概理解了{}", user_message),
|
||||
));
|
||||
}
|
||||
|
||||
responses
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LLM-powered response generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn generate_llm_responses(
|
||||
kernel_state: &State<'_, KernelState>,
|
||||
agents: &[AgentProfile],
|
||||
user_message: &str,
|
||||
scene_context: Option<&str>,
|
||||
history: &[ClassroomChatMessage],
|
||||
) -> Result<Vec<ClassroomChatMessage>, String> {
|
||||
let driver = {
|
||||
let ks = kernel_state.lock().await;
|
||||
ks.as_ref()
|
||||
.map(|k| k.driver())
|
||||
.ok_or_else(|| "Kernel not initialized".to_string())?
|
||||
};
|
||||
|
||||
if !driver.is_configured() {
|
||||
return Err("LLM driver not configured".to_string());
|
||||
}
|
||||
|
||||
// Build the chat request for prompt generation (include history)
|
||||
let chat_request = ClassroomChatRequest {
|
||||
classroom_id: String::new(),
|
||||
user_message: user_message.to_string(),
|
||||
agents: agents.to_vec(),
|
||||
scene_context: scene_context.map(|s| s.to_string()),
|
||||
history: history.to_vec(),
|
||||
};
|
||||
|
||||
let prompt = build_chat_prompt(&chat_request);
|
||||
|
||||
let request = CompletionRequest {
|
||||
model: "default".to_string(),
|
||||
system: Some("你是一个课堂多智能体讨论的协调器。".to_string()),
|
||||
messages: vec![zclaw_types::Message::User {
|
||||
content: prompt,
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let response = driver.complete(request).await
|
||||
.map_err(|e| format!("LLM call failed: {}", e))?;
|
||||
|
||||
// Extract text from response
|
||||
let text = response.content.iter()
|
||||
.filter_map(|block| match block {
|
||||
zclaw_runtime::ContentBlock::Text { text } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
|
||||
let responses = parse_chat_responses(&text, agents);
|
||||
if responses.is_empty() {
|
||||
return Err("LLM returned no parseable agent responses".to_string());
|
||||
}
|
||||
|
||||
Ok(responses)
|
||||
}
|
||||
Reference in New Issue
Block a user