feat(s2s-text): dedicated text-mode prompt + Markdown rendering

Architecture fix: voice and text mode now have completely separate prompts.

Backend:
- VoiceAssistantProfileSupport.buildTextSystemRole: dedicated text-mode system
  role that inherits all business rules (identity, KB-first, sensitive topics,
  sales guidance, personal info) but removes voice-specific constraints (short
  sentences, colloquial, single-line conclusion).
- DEFAULT_TEXT_SPEAKING_STYLE: text-specific style demanding detailed,
  structured, Markdown-formatted answers with complete information.
- VoiceGatewayService.handleStart: switch between voice/text system role and
  speaking style based on state.textMode.
- VoiceGatewayService.buildStartSessionPayload: preserve Markdown in text mode
  (voice mode still strips asterisks/backticks via normalizeTextForSpeech to
  avoid TTS pronouncing format chars).

Frontend:
- Added react-markdown@9 + remark-gfm@4 dependencies.
- ChatPanel renders assistant messages (non-voice) with ReactMarkdown:
  headings, lists (ul/ol), bold, italic, inline/block code, tables, blockquote,
  links, horizontal rules — all styled with Tailwind classes matching the dark
  theme.
- User messages and voice-handoff messages remain plain text.

Verification: mvn test VoiceGatewaySmokeTest 20/20 pass, vite build succeeds.
This commit is contained in:
User
2026-04-17 10:10:20 +08:00
parent 4b78f81cbc
commit e145f1d97e
5 changed files with 1576 additions and 13 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,9 @@
"axios": "^1.6.2",
"lucide-react": "^0.344.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-markdown": "^9.1.0",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",

View File

@@ -1,5 +1,7 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import { Send, Bot, User, Loader2, ArrowLeft, Sparkles, Wrench, StopCircle } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { startChatSession, sendMessageStream } from '../services/chatApi';
import { getSessionHistory } from '../services/voiceApi';
import { NativeVoiceService } from '../services/nativeVoiceService';
@@ -368,7 +370,38 @@ export default function ChatPanel({ sessionId, voiceSubtitles, settings, onBack,
: 'bg-slate-700/50 text-slate-200 rounded-tl-sm'
}`}
>
{msg.content}
{msg.role === 'assistant' && !msg.fromVoice ? (
<div className="markdown-body">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({ node, ...props }) => <p className="mb-2 last:mb-0" {...props} />,
ul: ({ node, ...props }) => <ul className="list-disc pl-5 mb-2 space-y-0.5" {...props} />,
ol: ({ node, ...props }) => <ol className="list-decimal pl-5 mb-2 space-y-0.5" {...props} />,
li: ({ node, ...props }) => <li className="leading-relaxed" {...props} />,
h1: ({ node, ...props }) => <h1 className="text-base font-bold mt-2 mb-1.5" {...props} />,
h2: ({ node, ...props }) => <h2 className="text-sm font-bold mt-2 mb-1.5" {...props} />,
h3: ({ node, ...props }) => <h3 className="text-sm font-semibold mt-2 mb-1" {...props} />,
strong: ({ node, ...props }) => <strong className="font-semibold text-white" {...props} />,
em: ({ node, ...props }) => <em className="italic" {...props} />,
code: ({ node, inline, ...props }) => inline
? <code className="px-1 py-0.5 rounded bg-slate-900/60 text-violet-300 text-[12px]" {...props} />
: <code className="block px-2 py-1.5 rounded bg-slate-900/80 text-violet-200 text-[12px] overflow-x-auto my-1.5" {...props} />,
pre: ({ node, ...props }) => <pre className="my-1.5" {...props} />,
table: ({ node, ...props }) => <div className="overflow-x-auto my-2"><table className="min-w-full text-xs border-collapse" {...props} /></div>,
th: ({ node, ...props }) => <th className="border border-slate-600/40 px-2 py-1 bg-slate-900/40 font-semibold text-left" {...props} />,
td: ({ node, ...props }) => <td className="border border-slate-600/40 px-2 py-1" {...props} />,
blockquote: ({ node, ...props }) => <blockquote className="border-l-2 border-violet-500/50 pl-2 italic text-slate-300 my-1.5" {...props} />,
a: ({ node, ...props }) => <a className="text-violet-400 hover:text-violet-300 underline" target="_blank" rel="noopener noreferrer" {...props} />,
hr: () => <hr className="my-2 border-slate-600/30" />,
}}
>
{msg.content || ''}
</ReactMarkdown>
</div>
) : (
msg.content
)}
{msg.fromVoice && (
<span className="ml-1.5 text-[9px] text-slate-600 align-middle">🎙</span>
)}