diff --git a/backend/common/target/classes/com/artedu/common/config/CorsConfig.class b/backend/common/target/classes/com/artedu/common/config/CorsConfig.class index 2c98325..7011286 100644 Binary files a/backend/common/target/classes/com/artedu/common/config/CorsConfig.class and b/backend/common/target/classes/com/artedu/common/config/CorsConfig.class differ diff --git a/backend/common/target/classes/com/artedu/common/config/MyBatisPlusConfig.class b/backend/common/target/classes/com/artedu/common/config/MyBatisPlusConfig.class index 39245fd..12f85ae 100644 Binary files a/backend/common/target/classes/com/artedu/common/config/MyBatisPlusConfig.class and b/backend/common/target/classes/com/artedu/common/config/MyBatisPlusConfig.class differ diff --git a/backend/common/target/classes/com/artedu/common/config/RabbitConfig.class b/backend/common/target/classes/com/artedu/common/config/RabbitConfig.class index 1617260..f32426a 100644 Binary files a/backend/common/target/classes/com/artedu/common/config/RabbitConfig.class and b/backend/common/target/classes/com/artedu/common/config/RabbitConfig.class differ diff --git a/backend/common/target/classes/com/artedu/common/config/RedisConfig.class b/backend/common/target/classes/com/artedu/common/config/RedisConfig.class index c202079..ca104a8 100644 Binary files a/backend/common/target/classes/com/artedu/common/config/RedisConfig.class and b/backend/common/target/classes/com/artedu/common/config/RedisConfig.class differ diff --git a/backend/common/target/classes/com/artedu/common/entity/BaseEntity.class b/backend/common/target/classes/com/artedu/common/entity/BaseEntity.class index c1e6bc2..5c0c181 100644 Binary files a/backend/common/target/classes/com/artedu/common/entity/BaseEntity.class and b/backend/common/target/classes/com/artedu/common/entity/BaseEntity.class differ diff --git a/backend/common/target/classes/com/artedu/common/exception/BusinessException.class b/backend/common/target/classes/com/artedu/common/exception/BusinessException.class index 9ed3b15..a53acc4 100644 Binary files a/backend/common/target/classes/com/artedu/common/exception/BusinessException.class and b/backend/common/target/classes/com/artedu/common/exception/BusinessException.class differ diff --git a/backend/common/target/classes/com/artedu/common/exception/GlobalExceptionHandler.class b/backend/common/target/classes/com/artedu/common/exception/GlobalExceptionHandler.class index 05d49b5..32efa25 100644 Binary files a/backend/common/target/classes/com/artedu/common/exception/GlobalExceptionHandler.class and b/backend/common/target/classes/com/artedu/common/exception/GlobalExceptionHandler.class differ diff --git a/backend/common/target/classes/com/artedu/common/handler/AutoFillHandler.class b/backend/common/target/classes/com/artedu/common/handler/AutoFillHandler.class index 112c043..d24ff82 100644 Binary files a/backend/common/target/classes/com/artedu/common/handler/AutoFillHandler.class and b/backend/common/target/classes/com/artedu/common/handler/AutoFillHandler.class differ diff --git a/backend/common/target/classes/com/artedu/common/mybatis/JsonTypeHandler.class b/backend/common/target/classes/com/artedu/common/mybatis/JsonTypeHandler.class index adaef91..dca2af2 100644 Binary files a/backend/common/target/classes/com/artedu/common/mybatis/JsonTypeHandler.class and b/backend/common/target/classes/com/artedu/common/mybatis/JsonTypeHandler.class differ diff --git a/backend/common/target/classes/com/artedu/common/result/PageResult.class b/backend/common/target/classes/com/artedu/common/result/PageResult.class index cb59d36..4db5a50 100644 Binary files a/backend/common/target/classes/com/artedu/common/result/PageResult.class and b/backend/common/target/classes/com/artedu/common/result/PageResult.class differ diff --git a/backend/common/target/classes/com/artedu/common/result/Result.class b/backend/common/target/classes/com/artedu/common/result/Result.class index 362c975..2b7ab51 100644 Binary files a/backend/common/target/classes/com/artedu/common/result/Result.class and b/backend/common/target/classes/com/artedu/common/result/Result.class differ diff --git a/backend/common/target/classes/com/artedu/common/util/JsonUtils$1.class b/backend/common/target/classes/com/artedu/common/util/JsonUtils$1.class index 31fda18..ebfbd27 100644 Binary files a/backend/common/target/classes/com/artedu/common/util/JsonUtils$1.class and b/backend/common/target/classes/com/artedu/common/util/JsonUtils$1.class differ diff --git a/backend/common/target/classes/com/artedu/common/util/JsonUtils.class b/backend/common/target/classes/com/artedu/common/util/JsonUtils.class index 53aa45f..1a0e4a0 100644 Binary files a/backend/common/target/classes/com/artedu/common/util/JsonUtils.class and b/backend/common/target/classes/com/artedu/common/util/JsonUtils.class differ diff --git a/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java b/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java index abeeb62..fba74ed 100644 --- a/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java +++ b/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java @@ -7,13 +7,19 @@ import com.artedu.generation.service.PromptEngine; import com.artedu.generation.service.QianwenClient; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.validation.constraints.NotBlank; +import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; @Slf4j @Validated @@ -30,6 +36,14 @@ public class GenerationController { @Autowired private LlmModelService llmModelService; + private final ExecutorService sseExecutor = Executors.newCachedThreadPool(); + + private static final List> STYLES = List.of( + Map.of("id", "friendly", "name", "亲和风格", "desc", "温暖亲切,像朋友聊天"), + Map.of("id", "professional", "name", "专业风格", "desc", "数据驱动,专业权威"), + Map.of("id", "urgent", "name", "促单风格", "desc", "制造紧迫感,促成决策") + ); + @PostMapping("/utterance") public Result> generateUtterance( @NotBlank(message = "message不能为空") @RequestParam("message") String message, @@ -71,6 +85,131 @@ public class GenerationController { return Result.success(result); } + /** + * 流式生成多风格话术(SSE) + * 页面打开即自动推荐 3 条不同风格话术 + */ + @GetMapping(value = "/utterance-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter streamUtterance( + @NotBlank(message = "message不能为空") @RequestParam("message") String message, + @RequestParam(value = "context", required = false) String context, + @RequestParam(value = "customerProfile", required = false) String customerProfile, + @RequestParam(value = "conversationContext", required = false) String conversationContext, + @RequestParam(value = "referenceUtterances", required = false) String referenceUtterances, + @NotBlank(message = "intent不能为空") @RequestParam("intent") String intent, + @NotBlank(message = "stage不能为空") @RequestParam("stage") String stage, + @RequestParam(value = "stageLabel", required = false) String stageLabel, + @RequestParam(value = "intentLabel", required = false) String intentLabel) { + + SseEmitter emitter = new SseEmitter(120000L); // 2分钟超时 + + sseExecutor.execute(() -> { + try { + // 1. 发送开始事件,告知前端3种风格 + Map startEvent = new HashMap<>(); + startEvent.put("type", "start"); + startEvent.put("styles", STYLES); + emitter.send(SseEmitter.event().name("start").data(startEvent)); + + // 2. 构建 system + user prompt + String systemPrompt = promptEngine.buildSystemPrompt(stageLabel, intentLabel); + String userPrompt = promptEngine.buildMultiStyleUserPrompt( + message, context, customerProfile, conversationContext, + referenceUtterances, intent, stage, stageLabel, intentLabel); + + // 3. 调用 LLM 生成(非流式,后台一次完成) + long startTime = System.currentTimeMillis(); + String generated = qianwenClient.generate(systemPrompt, userPrompt, 1200); + log.info("多风格话术生成完成, elapsed={}ms", System.currentTimeMillis() - startTime); + + // 4. 解析出3条话术 + List utterances = parseMultiStyle(generated); + if (utterances.size() < 3) { + // 如果解析失败,用回退方案:把整个内容当一条 + while (utterances.size() < 3) { + utterances.add(generated != null ? generated : "生成失败,请稍后重试"); + } + } + + // 5. 逐条逐字流式推送给前端(伪流式,打字机效果) + for (int i = 0; i < Math.min(3, utterances.size()); i++) { + String text = utterances.get(i); + String styleId = STYLES.get(i).get("id"); + + // 先发送该条话术的起始事件 + Map itemStart = new HashMap<>(); + itemStart.put("type", "item-start"); + itemStart.put("styleId", styleId); + itemStart.put("index", i); + emitter.send(SseEmitter.event().name("item-start").data(itemStart)); + + // 逐字推送(每20-40ms推送一个字,模拟打字速度) + for (int j = 0; j < text.length(); j++) { + String ch = text.substring(j, j + 1); + Map chunk = new HashMap<>(); + chunk.put("type", "chunk"); + chunk.put("styleId", styleId); + chunk.put("text", ch); + chunk.put("index", i); + emitter.send(SseEmitter.event().name("chunk").data(chunk)); + Thread.sleep(25); // 25ms/字,约 40字/秒 + } + + // 该条话术完成 + Map complete = new HashMap<>(); + complete.put("type", "complete"); + complete.put("styleId", styleId); + complete.put("fullText", text); + complete.put("index", i); + emitter.send(SseEmitter.event().name("complete").data(complete)); + } + + // 6. 全部完成 + Map done = new HashMap<>(); + done.put("type", "done"); + done.put("model", qianwenClient.getCurrentModel() != null + ? qianwenClient.getCurrentModel().getModelName() : "default"); + emitter.send(SseEmitter.event().name("done").data(done)); + emitter.complete(); + + } catch (Exception e) { + log.error("流式生成失败", e); + try { + Map error = new HashMap<>(); + error.put("type", "error"); + error.put("message", e.getMessage()); + emitter.send(SseEmitter.event().name("error").data(error)); + } catch (IOException ignored) {} + emitter.completeWithError(e); + } + }); + + return emitter; + } + + /** + * 解析多风格话术,按 "|||" 分隔 + */ + private List parseMultiStyle(String text) { + List result = new ArrayList<>(); + if (text == null || text.isEmpty()) return result; + + // 去掉常见的AI输出前缀 + String cleaned = text.replaceAll("^(风格一[::]?|风格二[::]?|风格三[::]?|亲和风格[::]?|专业风格[::]?|促单风格[::]?)", "").trim(); + + // 按 ||| 分隔 + String[] parts = cleaned.split("\\|\\|\\|"); + for (String part : parts) { + String trimmed = part.trim(); + if (!trimmed.isEmpty() && trimmed.length() > 5) { + // 去掉可能的风格标签前缀 + trimmed = trimmed.replaceAll("^(风格[一二三四五][::]?|【.*?】[::]?)", "").trim(); + result.add(trimmed); + } + } + return result; + } + /** * 通用文本生成接口(供其他服务调用) */ diff --git a/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java b/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java index 2e135d8..b0feabd 100644 --- a/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java +++ b/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java @@ -128,6 +128,59 @@ public class PromptEngine { return sb.toString(); } + /** + * 构建多风格话术的用户提示词 + * 一次生成 3 条不同风格的话术,用特定分隔符 + */ + public String buildMultiStyleUserPrompt(String message, String context, + String customerProfile, String conversationContext, + String referenceUtterances, + String intent, String stage, + String stageLabel, String intentLabel) { + StringBuilder sb = new StringBuilder(); + + if (customerProfile != null && !customerProfile.isEmpty()) { + sb.append("【学员画像】\n").append(customerProfile).append("\n\n"); + } + + String history = null; + if (conversationContext != null && !conversationContext.isEmpty()) { + history = conversationContext; + } else if (context != null && !context.isEmpty()) { + history = context; + } + if (history != null && !history.isEmpty()) { + sb.append("【最近对话】\n").append(history).append("\n\n"); + } + + String stageDisplay = (stageLabel != null && !stageLabel.isEmpty()) ? stageLabel : stage; + String intentDisplay = (intentLabel != null && !intentLabel.isEmpty()) ? intentLabel : intent; + sb.append("【当前阶段】").append(stageDisplay).append("\n"); + sb.append("【学员意图】").append(intentDisplay).append("\n\n"); + + if (referenceUtterances != null && !referenceUtterances.isEmpty()) { + sb.append("【参考话术风格】\n").append(referenceUtterances).append("\n\n"); + } + + sb.append("学员最新消息:\"").append(message).append("\"\n\n"); + sb.append("请为这条学员消息生成3种不同风格的回复话术,每种风格完全不同,让学员有更多选择:\n\n"); + sb.append("风格一【亲和风格】:语气温暖、亲切、像朋友聊天,拉近距离,适合建立好感\n"); + sb.append("风格二【专业风格】:用数据、案例、专业分析增强说服力,适合建立信任\n"); + sb.append("风格三【促单风格】:制造紧迫感,强调限时优惠或名额有限,适合促成决策\n\n"); + sb.append("要求:\n"); + sb.append("- 每条话术150-250字\n"); + sb.append("- 直接输出话术内容,不要加任何前缀或解释\n"); + sb.append("- 每条话术独立成行,用「|||」分隔\n\n"); + sb.append("输出格式示例:\n"); + sb.append("亲和风格话术内容...\n"); + sb.append("|||\n"); + sb.append("专业风格话术内容...\n"); + sb.append("|||\n"); + sb.append("促单风格话术内容..."); + + return sb.toString(); + } + public String buildSummaryPrompt(String context) { return "请对以下对话进行简要总结(50字以内),提炼学员核心需求和当前阶段:\n\n" + context; } diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class index 40286a4..f7fa1bf 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class b/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class index ce0ea19..8d9f4cd 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class and b/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class differ diff --git a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar index d979f8b..8079485 100644 Binary files a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar and b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar differ diff --git a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original index 3ab62f9..d2ffdc8 100644 Binary files a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original and b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/IntentServiceApplication.class b/backend/intent-service/target/classes/com/artedu/intent/IntentServiceApplication.class index dc8dca7..e708257 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/IntentServiceApplication.class and b/backend/intent-service/target/classes/com/artedu/intent/IntentServiceApplication.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/controller/CustomerProfileController.class b/backend/intent-service/target/classes/com/artedu/intent/controller/CustomerProfileController.class index 9840402..79b4b33 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/controller/CustomerProfileController.class and b/backend/intent-service/target/classes/com/artedu/intent/controller/CustomerProfileController.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/controller/IntentController.class b/backend/intent-service/target/classes/com/artedu/intent/controller/IntentController.class index 78775f9..888424c 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/controller/IntentController.class and b/backend/intent-service/target/classes/com/artedu/intent/controller/IntentController.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/controller/ProfileGenerationController$BatchGenerateRequest.class b/backend/intent-service/target/classes/com/artedu/intent/controller/ProfileGenerationController$BatchGenerateRequest.class index 6487855..19ca753 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/controller/ProfileGenerationController$BatchGenerateRequest.class and b/backend/intent-service/target/classes/com/artedu/intent/controller/ProfileGenerationController$BatchGenerateRequest.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/controller/ProfileGenerationController.class b/backend/intent-service/target/classes/com/artedu/intent/controller/ProfileGenerationController.class index 47ecba9..f92d662 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/controller/ProfileGenerationController.class and b/backend/intent-service/target/classes/com/artedu/intent/controller/ProfileGenerationController.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult$IntentInfo.class b/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult$IntentInfo.class index ea10301..c3f7d59 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult$IntentInfo.class and b/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult$IntentInfo.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult.class b/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult.class index a7ba670..736fabb 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult.class and b/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/entity/CustomerProfile.class b/backend/intent-service/target/classes/com/artedu/intent/entity/CustomerProfile.class index 8b8d4e8..644549a 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/entity/CustomerProfile.class and b/backend/intent-service/target/classes/com/artedu/intent/entity/CustomerProfile.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/entity/IntentCategory.class b/backend/intent-service/target/classes/com/artedu/intent/entity/IntentCategory.class index cb882c3..87534f8 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/entity/IntentCategory.class and b/backend/intent-service/target/classes/com/artedu/intent/entity/IntentCategory.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/entity/IntentRecognition.class b/backend/intent-service/target/classes/com/artedu/intent/entity/IntentRecognition.class index 68e3cde..655a1f6 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/entity/IntentRecognition.class and b/backend/intent-service/target/classes/com/artedu/intent/entity/IntentRecognition.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/entity/ProfileGenerationLog.class b/backend/intent-service/target/classes/com/artedu/intent/entity/ProfileGenerationLog.class index 402e3dc..0c23206 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/entity/ProfileGenerationLog.class and b/backend/intent-service/target/classes/com/artedu/intent/entity/ProfileGenerationLog.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/mapper/CustomerProfileMapper.class b/backend/intent-service/target/classes/com/artedu/intent/mapper/CustomerProfileMapper.class index 612a318..ab21922 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/mapper/CustomerProfileMapper.class and b/backend/intent-service/target/classes/com/artedu/intent/mapper/CustomerProfileMapper.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentCategoryMapper.class b/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentCategoryMapper.class index 399bcaa..f7b7643 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentCategoryMapper.class and b/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentCategoryMapper.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentRecognitionMapper.class b/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentRecognitionMapper.class index 545a05c..8eabddd 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentRecognitionMapper.class and b/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentRecognitionMapper.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/mapper/ProfileGenerationLogMapper.class b/backend/intent-service/target/classes/com/artedu/intent/mapper/ProfileGenerationLogMapper.class index cad832a..8306e78 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/mapper/ProfileGenerationLogMapper.class and b/backend/intent-service/target/classes/com/artedu/intent/mapper/ProfileGenerationLogMapper.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/CustomerProfileService.class b/backend/intent-service/target/classes/com/artedu/intent/service/CustomerProfileService.class index f24fb55..829ca21 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/CustomerProfileService.class and b/backend/intent-service/target/classes/com/artedu/intent/service/CustomerProfileService.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/IntentRecognitionService.class b/backend/intent-service/target/classes/com/artedu/intent/service/IntentRecognitionService.class index 568d877..fa85717 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/IntentRecognitionService.class and b/backend/intent-service/target/classes/com/artedu/intent/service/IntentRecognitionService.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/LLMIntentClient.class b/backend/intent-service/target/classes/com/artedu/intent/service/LLMIntentClient.class index 0690521..44c916b 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/LLMIntentClient.class and b/backend/intent-service/target/classes/com/artedu/intent/service/LLMIntentClient.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$FieldValue.class b/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$FieldValue.class index c2a2ffb..4b25b40 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$FieldValue.class and b/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$FieldValue.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$GenerationResult.class b/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$GenerationResult.class index 6da8b68..80a6654 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$GenerationResult.class and b/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$GenerationResult.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$ProfileParseResult.class b/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$ProfileParseResult.class index 280fc2f..d84309f 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$ProfileParseResult.class and b/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService$ProfileParseResult.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService.class b/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService.class index c6cd81b..32523ce 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService.class and b/backend/intent-service/target/classes/com/artedu/intent/service/ProfileGenerationService.class differ diff --git a/backend/intent-service/target/test-classes/com/artedu/intent/controller/IntentControllerTest.class b/backend/intent-service/target/test-classes/com/artedu/intent/controller/IntentControllerTest.class index 512b5c7..d11ce72 100644 Binary files a/backend/intent-service/target/test-classes/com/artedu/intent/controller/IntentControllerTest.class and b/backend/intent-service/target/test-classes/com/artedu/intent/controller/IntentControllerTest.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/KbAdminServiceApplication.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/KbAdminServiceApplication.class index 567f864..de7b512 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/KbAdminServiceApplication.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/KbAdminServiceApplication.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/config/SecurityConfig.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/config/SecurityConfig.class index ad553e0..0f6ea2a 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/config/SecurityConfig.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/config/SecurityConfig.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/AdminController.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/AdminController.class index 63e9efb..472702f 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/AdminController.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/AdminController.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/AuthController.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/AuthController.class index e54059f..0f3b6e9 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/AuthController.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/AuthController.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$ApproveRequest.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$ApproveRequest.class index d31716c..7b446f9 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$ApproveRequest.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$ApproveRequest.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchApproveRequest.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchApproveRequest.class index 53da716..1569ff0 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchApproveRequest.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchApproveRequest.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchRejectRequest.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchRejectRequest.class index d7b07f3..83963cb 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchRejectRequest.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchRejectRequest.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$StartMiningRequest.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$StartMiningRequest.class index 218b07a..8fb0faa 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$StartMiningRequest.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$StartMiningRequest.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController.class index 993ec96..6bbdd7f 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/KnowledgeController.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/KnowledgeController.class index 33a76fa..0fd6a12 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/KnowledgeController.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/KnowledgeController.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/UtteranceController.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/UtteranceController.class index 2aacfd2..6ec1336 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/UtteranceController.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/UtteranceController.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/Admin.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/Admin.class index 6b61bd8..7787659 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/Admin.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/Admin.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/ChatMineCandidate.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/ChatMineCandidate.class index 4e71607..cf846a6 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/ChatMineCandidate.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/ChatMineCandidate.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/KnowledgeDocument.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/KnowledgeDocument.class index d94f3f4..386e076 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/KnowledgeDocument.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/KnowledgeDocument.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/Utterance.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/Utterance.class index 084aeea..4ff1000 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/Utterance.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/Utterance.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/AdminMapper.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/AdminMapper.class index fe94e70..615a3ae 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/AdminMapper.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/AdminMapper.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.class index db7e6a5..c47189d 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/KnowledgeDocumentMapper.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/KnowledgeDocumentMapper.class index 1c29ea1..5371840 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/KnowledgeDocumentMapper.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/KnowledgeDocumentMapper.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/UtteranceMapper.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/UtteranceMapper.class index bf42437..9b7deec 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/UtteranceMapper.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/UtteranceMapper.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService$MineResult.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService$MineResult.class index 58340b3..ec63351 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService$MineResult.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService$MineResult.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService.class index 6cc0e12..35a2a91 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService.class differ diff --git a/backend/kb-admin-service/target/test-classes/com/artedu/kbadmin/controller/AuthControllerTest.class b/backend/kb-admin-service/target/test-classes/com/artedu/kbadmin/controller/AuthControllerTest.class index 21b7686..5c52b59 100644 Binary files a/backend/kb-admin-service/target/test-classes/com/artedu/kbadmin/controller/AuthControllerTest.class and b/backend/kb-admin-service/target/test-classes/com/artedu/kbadmin/controller/AuthControllerTest.class differ diff --git a/backend/kb-admin-service/target/test-classes/com/artedu/kbadmin/controller/UtteranceControllerTest.class b/backend/kb-admin-service/target/test-classes/com/artedu/kbadmin/controller/UtteranceControllerTest.class index e81cbc7..2866f66 100644 Binary files a/backend/kb-admin-service/target/test-classes/com/artedu/kbadmin/controller/UtteranceControllerTest.class and b/backend/kb-admin-service/target/test-classes/com/artedu/kbadmin/controller/UtteranceControllerTest.class differ diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..c1a9567 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1 @@ +sidebar-backup \ No newline at end of file diff --git a/frontend/sidebar/src/App.tsx b/frontend/sidebar/src/App.tsx index a43bb16..289c6aa 100644 --- a/frontend/sidebar/src/App.tsx +++ b/frontend/sidebar/src/App.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react' -import { NavBar, Tabs, Toast } from 'antd-mobile' -import ProfileCard from './components/ProfileCard' -import ScriptRecommend from './components/ScriptRecommend' +import { Tabs, Toast } from 'antd-mobile' +import ScriptRecommendStream from './components/ScriptRecommendStream' import ConversationAnalysis from './components/ConversationAnalysis' import './styles.css' @@ -10,7 +9,7 @@ function App() { const [authState, setAuthState] = useState<'loading' | 'authed' | 'error'>('loading') const [userInfo, setUserInfo] = useState(null) const [currentCustomerId, setCurrentCustomerId] = useState('') - const [manualCustomerId, setManualCustomerId] = useState('') + useEffect(() => { initWeCom() @@ -163,15 +162,6 @@ function App() { } }, [authState]) - const handleManualCustomer = () => { - if (manualCustomerId.trim()) { - localStorage.setItem('current_customer_id', manualCustomerId.trim()) - setCurrentCustomerId(manualCustomerId.trim()) - setManualCustomerId('') - Toast.show({ content: '已加载客户: ' + manualCustomerId.trim(), icon: 'success' }) - } - } - if (authState === 'loading') { return (
@@ -185,37 +175,9 @@ function App() { return (
- - AI坐席助手 - - - - {/* 手动输入客户ID(调试/降级) */} -
-
- 💡 调试模式:可手动输入客户ID加载画像 -
-
- setManualCustomerId(e.target.value)} - style={{ flex: 1, padding: '6px 10px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 13 }} - onKeyPress={(e) => e.key === 'Enter' && handleManualCustomer()} - /> - -
-
- - - + + diff --git a/frontend/sidebar/src/components/ScriptRecommendStream.tsx b/frontend/sidebar/src/components/ScriptRecommendStream.tsx new file mode 100644 index 0000000..5782653 --- /dev/null +++ b/frontend/sidebar/src/components/ScriptRecommendStream.tsx @@ -0,0 +1,403 @@ +import { useEffect, useRef, useState, useCallback } from 'react' +import { Button, Toast, SpinLoading, Tag, Space } from 'antd-mobile' + +interface StyleConfig { + id: string + name: string + desc: string + color: string + bgColor: string +} + +interface UtteranceItem { + styleId: string + text: string + isComplete: boolean + isTyping: boolean +} + +interface Props { + userInfo?: any + customerId?: string +} + +const STYLES: StyleConfig[] = [ + { id: 'friendly', name: '亲和风格', desc: '温暖亲切,像朋友聊天', color: '#1677ff', bgColor: '#e6f4ff' }, + { id: 'professional', name: '专业风格', desc: '数据驱动,专业权威', color: '#52c41a', bgColor: '#f6ffed' }, + { id: 'urgent', name: '促单风格', desc: '制造紧迫感,促成决策', color: '#fa8c16', bgColor: '#fff7e6' } +] + +export default function ScriptRecommendStream({ userInfo, customerId: propCustomerId }: Props) { + const [loading, setLoading] = useState(true) + const [generating, setGenerating] = useState(false) + const [customerMsg, setCustomerMsg] = useState('') + const [profileTags, setProfileTags] = useState([]) + const [utterances, setUtterances] = useState([]) + const [progress, setProgress] = useState(0) + const [error, setError] = useState('') + const sseRef = useRef(null) + + const sessionId = localStorage.getItem('current_session_id') || `session_${Date.now()}` + const customerId = propCustomerId || localStorage.getItem('current_customer_id') || 'wx_001' + const staffId = userInfo?.userId || 'staff_001' + const corpId = userInfo?.corpId || 'wwd483c2fba24ae30a' + + const closeSse = useCallback(() => { + if (sseRef.current) { + sseRef.current.close() + sseRef.current = null + } + }, []) + + const handleSend = (content: string) => { + const wx = (window as any).wx + if (wx && wx.invoke) { + wx.invoke('sendChatMessage', { + msgtype: 'text', + text: { content } + }, (res: any) => { + if (res?.err_msg === 'sendChatMessage:ok') { + Toast.show({ content: '发送成功', icon: 'success' }) + } else { + navigator.clipboard.writeText(content) + Toast.show({ content: '已复制(企微SDK未就绪)', icon: 'success' }) + } + }) + } else { + navigator.clipboard.writeText(content) + Toast.show({ content: '已复制到剪贴板', icon: 'success' }) + } + } + + const handleCopy = (content: string) => { + navigator.clipboard.writeText(content) + Toast.show({ content: '已复制', icon: 'success' }) + } + + const handleRegenerate = () => { + closeSse() + setUtterances([]) + setProgress(0) + setError('') + startGeneration() + } + + const startGeneration = useCallback(async () => { + if (!customerMsg.trim()) { + setError('暂无学员消息') + setLoading(false) + return + } + + setGenerating(true) + setLoading(false) + setProgress(10) + + try { + const [profileRes, contextRes, recommendRes] = await Promise.all([ + fetch(`/api/v1/intent/profile/${encodeURIComponent(customerId)}?corpId=${corpId}`), + fetch(`/api/v1/conversations/${sessionId}/context`).catch(() => null), + fetch(`/api/v1/recommend/get?${new URLSearchParams({ + sessionId, customerId, staffId, corpId, + intentCode: 'INT-COURSE-01', + stage: 'STAGE-03', + studentType: '零基础', + message: customerMsg + }).toString()}`, { method: 'POST' }).catch(() => null) + ]) + + let customerProfileStr = '' + let tags: string[] = [] + const profileData = await profileRes.json() + if (profileData.code === 0 && profileData.data) { + const p = profileData.data + if (p.currentOccupation) { tags.push(p.currentOccupation) } + if (p.studentType) { tags.push(p.studentType) } + if (p.skillLevel) { tags.push(p.skillLevel) } + if (p.intentLevel) { tags.push(p.intentLevel) } + if (p.concernFocus) { tags.push(p.concernFocus) } + if (p.decisionStage) { tags.push(p.decisionStage) } + customerProfileStr = [ + p.currentOccupation ? `身份/职业:${p.currentOccupation}` : '', + p.studentType ? `学员类型:${p.studentType}` : '', + p.skillLevel ? `基础水平:${p.skillLevel}` : '', + p.intentLevel ? `意向度:${p.intentLevel}(${p.intentScore != null ? Math.round(p.intentScore) : 0}分)` : '', + p.concernFocus ? `关注方向:${p.concernFocus}` : '', + p.decisionStage ? `决策阶段:${p.decisionStage}` : '' + ].filter(Boolean).join('\n') + } + setProfileTags(tags) + setProgress(30) + + let conversationContextStr = '' + if (contextRes) { + const contextData = await contextRes.json() + if (contextData.code === 0 && contextData.data?.messages) { + const msgs = contextData.data.messages as Array<{ role: string; content: string }> + conversationContextStr = msgs.slice(-3).map(m => { + const roleName = m.role === 'student' ? '学员' : '顾问' + return `${roleName}:${m.content}` + }).join('\n') + } + } + + let referenceUtterancesStr = '' + if (recommendRes) { + const recData = await recommendRes.json() + if (recData.code === 0 && recData.data) { + const top3 = recData.data.slice(0, 3) + referenceUtterancesStr = top3.map((s: any, i: number) => `${i + 1}. "${s.contentText || s.content || s.title}"`).join('\n') + } + } + setProgress(50) + + const params = new URLSearchParams({ + message: customerMsg, + intent: 'INT-COURSE-01', + stage: 'STAGE-03', + stageLabel: '方案呈现', + intentLabel: '课程咨询' + }) + if (customerProfileStr) params.append('customerProfile', customerProfileStr) + if (conversationContextStr) params.append('conversationContext', conversationContextStr) + if (referenceUtterancesStr) params.append('referenceUtterances', referenceUtterancesStr) + + const url = `/api/v1/generation/utterance-stream?${params.toString()}` + const es = new EventSource(url) + sseRef.current = es + + setUtterances(STYLES.map(s => ({ styleId: s.id, text: '', isComplete: false, isTyping: false }))) + + // ===== 关键修复:用 addEventListener 接收命名事件 ===== + es.addEventListener('start', (event: MessageEvent) => { + try { + const data = JSON.parse(event.data) + if (data.type === 'start') setProgress(60) + } catch (e) { console.error('SSE start error:', e) } + }) + + es.addEventListener('item-start', (event: MessageEvent) => { + try { + const data = JSON.parse(event.data) + if (data.type === 'item-start') { + setUtterances(prev => prev.map(u => + u.styleId === data.styleId ? { ...u, isTyping: true } : u + )) + } + } catch (e) { console.error('SSE item-start error:', e) } + }) + + es.addEventListener('chunk', (event: MessageEvent) => { + try { + const data = JSON.parse(event.data) + if (data.type === 'chunk') { + setUtterances(prev => prev.map(u => + u.styleId === data.styleId ? { ...u, text: u.text + data.text } : u + )) + } + } catch (e) { console.error('SSE chunk error:', e) } + }) + + es.addEventListener('complete', (event: MessageEvent) => { + try { + const data = JSON.parse(event.data) + if (data.type === 'complete') { + setUtterances(prev => prev.map(u => + u.styleId === data.styleId ? { ...u, text: data.fullText, isComplete: true, isTyping: false } : u + )) + } + } catch (e) { console.error('SSE complete error:', e) } + }) + + es.addEventListener('done', (event: MessageEvent) => { + try { + const data = JSON.parse(event.data) + if (data.type === 'done') { + setGenerating(false) + setProgress(100) + es.close() + } + } catch (e) { console.error('SSE done error:', e) } + }) + + es.addEventListener('error', (event: MessageEvent) => { + try { + const data = JSON.parse(event.data) + setError(data.message || '生成失败') + } catch (e) { + setError('连接中断') + } + setGenerating(false) + es.close() + }) + + } catch (e) { + console.error('生成失败:', e) + setError('网络错误,请重试') + setGenerating(false) + } + }, [customerMsg, customerId, corpId, sessionId, staffId]) + + useEffect(() => { + const init = async () => { + setLoading(true) + try { + const res = await fetch(`/api/v1/conversations/last-message?customerId=${customerId}&corpId=${corpId}`) + const data = await res.json() + if (data.code === 0 && data.data?.content) { + setCustomerMsg(data.data.content) + } else { + setLoading(false) + setError('暂无学员最新消息') + } + } catch (e) { + console.error('初始化失败:', e) + setLoading(false) + setError('获取学员消息失败') + } + } + init() + return () => { closeSse() } + }, [customerId, corpId, closeSse]) + + useEffect(() => { + if (customerMsg.trim() && !generating && utterances.length === 0) { + startGeneration() + } + }, [customerMsg, generating, utterances.length, startGeneration]) + + if (loading && !generating && utterances.length === 0) { + return ( +
+ +
正在分析学员消息...
+
+ ) + } + + return ( +
+ {/* 学员消息 + 标签 */} +
+
+ 💬 学员消息: + {customerMsg || '暂无'} +
+ {profileTags.length > 0 && ( + + {profileTags.map(tag => ( + {tag} + ))} + + )} +
+ + {/* 生成进度 */} + {generating && progress < 100 && ( +
+ +
+
+ {progress < 60 ? '正在分析学员画像和对话历史...' : 'AI 正在生成推荐话术...'} +
+
+
+
+
+
+ )} + + {/* 错误 */} + {error && ( +
+ ⚠️ {error} + +
+ )} + + {/* 三条话术卡片 */} + {utterances.map((item, index) => { + const style = STYLES[index] + return ( +
+ {/* 风格标签 */} +
+ {style.name} + {style.desc} + {item.isTyping && ( + 生成中... + )} + {item.isComplete && ( + ✓ 已完成 + )} +
+ + {/* 话术内容 */} +
+ {item.text} + {item.isTyping && |} + {!item.text && !item.isTyping && ( + 等待生成... + )} +
+ + {/* 操作按钮 */} + {item.isComplete && ( +
+ + +
+ )} +
+ ) + })} + + {/* 重新生成 */} + {!generating && utterances.some(u => u.isComplete) && ( +
+ +
+ )} + + +
+ ) +}