@surfmate.team/digital-human-conversation 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -45,12 +45,19 @@ type Props = {
45
45
  readonly port?: ChatPort | undefined;
46
46
  /** Fired after each assistant reply (e.g. to drive mood-tagged video later). */
47
47
  readonly onReply?: ((reply: ChatReply) => void) | undefined;
48
+ /** Emitted after each completed turn (user said X → assistant replied Y). The
49
+ * host persists it if it wants — ChatWindow itself stays in-memory. Aligns
50
+ * with VoiceCall / VideoCall's onTurn. */
51
+ readonly onTurn?: ((user: ChatMessage, ai: ChatReply) => void) | undefined;
48
52
  /** Read an assistant message aloud (e.g. the voice synth). No prop → no speaker / auto-play. */
49
53
  readonly onSpeak?: ((text: string) => void | Promise<void>) | undefined;
50
- /** Back button in the header. No prop no back button. */
51
- readonly onClose?: (() => void) | undefined;
52
- /** Start a video call (header phone button). No prop no call button. */
53
- readonly onCall?: (() => void) | undefined;
54
+ /** Back button in the header; receives the transcript (greeting excluded) so
55
+ * the host can persist it on exit. No prop → no back button. */
56
+ readonly onClose?: ((history: readonly ChatMessage[]) => void) | undefined;
57
+ /** Start a voice-only call (toolbar phone button). No prop → no button. */
58
+ readonly onVoiceCall?: (() => void) | undefined;
59
+ /** Start a talking-head video call (toolbar video button). No prop → no button. */
60
+ readonly onVideoCall?: (() => void) | undefined;
54
61
  /** Avatar for the user's own bubbles. */
55
62
  readonly userAvatarUrl?: string | undefined;
56
63
  /** Speech-to-text language for the mic. */
@@ -64,6 +71,6 @@ type Props = {
64
71
  * Self-contained: owns the conversation, seeds the greeting, builds the prompt
65
72
  * (pure) → ChatPort → parses (pure). In-memory (no persistence yet).
66
73
  */
67
- declare function ChatWindow({ persona, port, onReply, onSpeak, onClose, onCall, userAvatarUrl, speechLang }: Props): react_jsx_runtime.JSX.Element;
74
+ declare function ChatWindow({ persona, port, onReply, onTurn, onSpeak, onClose, onVoiceCall, onVideoCall, userAvatarUrl, speechLang }: Props): react_jsx_runtime.JSX.Element;
68
75
 
69
76
  export { type CharacterPersona, type ChatMessage, type ChatMood, type ChatReply, type ChatRole, ChatWindow, HISTORY_WINDOW, buildPrompt, parseReply };
package/dist/index.js CHANGED
@@ -41,7 +41,7 @@ function parseReply(raw) {
41
41
 
42
42
  // src/ui/ChatWindow.tsx
43
43
  import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
44
- import { Send, Loader2, Volume2, VolumeX, Smile, Mic as Mic2, ArrowLeft, Trash2, Phone } from "lucide-react";
44
+ import { Send, Loader2, Volume2, VolumeX, Smile, Mic as Mic2, ArrowLeft, Trash2, Phone, Video } from "lucide-react";
45
45
 
46
46
  // src/ui/EmojiPicker.tsx
47
47
  import { jsx } from "react/jsx-runtime";
@@ -329,7 +329,7 @@ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
329
329
  var GREETING_ID = "greeting";
330
330
  var DEFAULT_USER_AVATAR = "https://api.dicebear.com/7.x/avataaars/svg?seed=user";
331
331
  var seedGreeting = (persona) => persona.greeting ? [{ id: GREETING_ID, role: "assistant", content: persona.greeting, timestamp: Date.now() }] : [];
332
- function ChatWindow({ persona, port, onReply, onSpeak, onClose, onCall, userAvatarUrl, speechLang }) {
332
+ function ChatWindow({ persona, port, onReply, onTurn, onSpeak, onClose, onVoiceCall, onVideoCall, userAvatarUrl, speechLang }) {
333
333
  const [messages, setMessages] = useState4(() => seedGreeting(persona));
334
334
  const [input, setInput] = useState4("");
335
335
  const [sending, setSending] = useState4(false);
@@ -377,16 +377,14 @@ function ChatWindow({ persona, port, onReply, onSpeak, onClose, onCall, userAvat
377
377
  }
378
378
  setPendingVoice(null);
379
379
  const history = messages.filter((m) => m.id !== GREETING_ID);
380
- setMessages((m) => [
381
- ...m,
382
- {
383
- id: crypto.randomUUID(),
384
- role: "user",
385
- content: text,
386
- timestamp: Date.now(),
387
- ...voice ? { voiceUrl: voice.url, voiceDuration: voice.duration } : {}
388
- }
389
- ]);
380
+ const userMsg = {
381
+ id: crypto.randomUUID(),
382
+ role: "user",
383
+ content: text,
384
+ timestamp: Date.now(),
385
+ ...voice ? { voiceUrl: voice.url, voiceDuration: voice.duration } : {}
386
+ };
387
+ setMessages((m) => [...m, userMsg]);
390
388
  setInput("");
391
389
  setSending(true);
392
390
  try {
@@ -395,6 +393,7 @@ function ChatWindow({ persona, port, onReply, onSpeak, onClose, onCall, userAvat
395
393
  const id = crypto.randomUUID();
396
394
  setMessages((m) => [...m, { id, role: "assistant", content: reply.text, timestamp: Date.now() }]);
397
395
  onReply?.(reply);
396
+ onTurn?.(userMsg, reply);
398
397
  if (autoPlay && onSpeak) void speak(id, reply.text);
399
398
  } catch (err) {
400
399
  const msg = err instanceof Error ? err.message : String(err);
@@ -421,7 +420,7 @@ function ChatWindow({ persona, port, onReply, onSpeak, onClose, onCall, userAvat
421
420
  }
422
421
  ),
423
422
  /* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-purple-50 to-white px-4 py-3", children: [
424
- onClose ? /* @__PURE__ */ jsx4("button", { type: "button", onClick: onClose, className: "rounded-full p-1.5 text-gray-600 transition hover:bg-purple-100", title: "\u8FD4\u56DE", children: /* @__PURE__ */ jsx4(ArrowLeft, { className: "h-4 w-4" }) }) : null,
423
+ onClose ? /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => onClose(messages.filter((m) => m.id !== GREETING_ID)), className: "rounded-full p-1.5 text-gray-600 transition hover:bg-purple-100", title: "\u8FD4\u56DE", children: /* @__PURE__ */ jsx4(ArrowLeft, { className: "h-4 w-4" }) }) : null,
425
424
  /* @__PURE__ */ jsxs3("div", { className: "relative shrink-0", children: [
426
425
  /* @__PURE__ */ jsx4("img", { src: persona.avatarUrl ?? DEFAULT_USER_AVATAR, alt: persona.name, className: "h-12 w-12 rounded-full object-cover ring-2 ring-purple-100" }),
427
426
  /* @__PURE__ */ jsx4("span", { className: "absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 ring-2 ring-white" })
@@ -443,16 +442,6 @@ function ChatWindow({ persona, port, onReply, onSpeak, onClose, onCall, userAvat
443
442
  ]
444
443
  }
445
444
  ) : null,
446
- onCall ? /* @__PURE__ */ jsx4(
447
- "button",
448
- {
449
- type: "button",
450
- onClick: onCall,
451
- title: "\u89C6\u9891\u901A\u8BDD",
452
- className: "rounded-full p-1.5 text-gray-500 transition hover:bg-purple-100 hover:text-purple-600",
453
- children: /* @__PURE__ */ jsx4(Phone, { className: "h-4 w-4" })
454
- }
455
- ) : null,
456
445
  /* @__PURE__ */ jsx4(
457
446
  "button",
458
447
  {
@@ -541,7 +530,27 @@ function ChatWindow({ persona, port, onReply, onSpeak, onClose, onCall, userAvat
541
530
  title: micActive ? "\u505C\u6B62\u5F55\u97F3" : "\u8BED\u97F3\u6D88\u606F(\u5F55\u97F3 + \u8F6C\u5199)",
542
531
  children: /* @__PURE__ */ jsx4(Mic2, { className: "h-5 w-5" })
543
532
  }
544
- )
533
+ ),
534
+ onVoiceCall ? /* @__PURE__ */ jsx4(
535
+ "button",
536
+ {
537
+ type: "button",
538
+ onClick: onVoiceCall,
539
+ title: "\u8BED\u97F3\u901A\u8BDD",
540
+ className: "ml-auto rounded-lg p-1.5 text-gray-500 transition hover:bg-purple-50 hover:text-purple-600",
541
+ children: /* @__PURE__ */ jsx4(Phone, { className: "h-5 w-5" })
542
+ }
543
+ ) : null,
544
+ onVideoCall ? /* @__PURE__ */ jsx4(
545
+ "button",
546
+ {
547
+ type: "button",
548
+ onClick: onVideoCall,
549
+ title: "\u89C6\u9891\u901A\u8BDD",
550
+ className: `rounded-lg p-1.5 text-gray-500 transition hover:bg-purple-50 hover:text-purple-600 ${onVoiceCall ? "" : "ml-auto"}`,
551
+ children: /* @__PURE__ */ jsx4(Video, { className: "h-5 w-5" })
552
+ }
553
+ ) : null
545
554
  ] }),
546
555
  /* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-end gap-2 bg-white px-3 pb-3 pt-1", children: [
547
556
  /* @__PURE__ */ jsx4("div", { className: "flex-1 rounded-2xl border border-gray-200 bg-gray-50 p-2.5", children: /* @__PURE__ */ jsx4(
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/calc/buildPrompt.ts","../src/calc/parseReply.ts","../src/ui/ChatWindow.tsx","../src/ui/EmojiPicker.tsx","../src/ui/VoiceRecordingOverlay.tsx","../src/ui/VoiceMessageBubble.tsx","../src/ui/useSpeechRecognition.ts","../src/ui/useAudioRecorder.ts"],"sourcesContent":["// Pure: turn the persona + recent history + the new user message into the single\n// prompt string the LLM gets. Mirrors mate's grokAI.buildPrompt — including the\n// \"reply in JSON {text, mood}\" instruction the parser depends on, and the 1-3\n// sentence length guidance (which keeps replies short enough for clip matching).\n\nimport type { CharacterPersona, ChatMessage } from '../data'\n\n/** How many trailing messages of history to include for context. */\nexport const HISTORY_WINDOW = 5\n\nexport function buildPrompt(\n persona: CharacterPersona,\n history: readonly ChatMessage[],\n userMessage: string,\n): string {\n const historyContext = history\n .slice(-HISTORY_WINDOW)\n .map((m) => `${m.role === 'user' ? 'User' : persona.name}: ${m.content}`)\n .join('\\n')\n\n return `You are ${persona.name}.\n\nCharacter background: ${persona.description ?? ''}\n\nPersonality traits: ${persona.personality ?? ''}\n\nYour greeting message: \"${persona.greeting ?? ''}\"\n\nConversation history:\n${historyContext}\n\nUser: ${userMessage}\n\nRespond as ${persona.name} would, staying in character with the background and personality described above. Keep responses natural, engaging, and consistent. Use 1-3 sentences unless a longer response is clearly needed.\n\nIMPORTANT: Respond in JSON format with your reply text and current mood:\n{\"text\": \"your response here\", \"mood\": \"happy|calm|angry|sad\"}\n\nChoose the mood that best matches the emotional tone of your response. Only use: happy, calm, angry, or sad.`\n}\n","// Pure: extract { text, mood } from the LLM's raw completion. The model is asked\n// to answer as JSON {\"text\": ..., \"mood\": ...}; we pull the first JSON object\n// containing \"text\", validate the mood, and fall back to the raw text + neutral\n// mood if anything is off. Mirrors mate's grokAI.parseAIResponse.\n\nimport type { ChatMood, ChatReply } from '../data'\n\nconst VALID_MOODS: ChatMood[] = ['happy', 'calm', 'angry', 'sad']\n\nexport function parseReply(raw: string): ChatReply {\n const match = raw.match(/\\{[\\s\\S]*\"text\"[\\s\\S]*\\}/)\n if (match) {\n try {\n const parsed = JSON.parse(match[0]) as { text?: unknown; mood?: unknown }\n const text = typeof parsed.text === 'string' && parsed.text ? parsed.text : raw\n const mood = VALID_MOODS.includes(parsed.mood as ChatMood) ? (parsed.mood as ChatMood) : 'neutral'\n return { text, mood }\n } catch {\n /* malformed JSON — fall through */\n }\n }\n return { text: raw, mood: 'neutral' }\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { Send, Loader2, Volume2, VolumeX, Smile, Mic, ArrowLeft, Trash2, Phone } from 'lucide-react'\nimport type { CharacterPersona, ChatMessage, ChatReply } from '../data'\nimport { buildPrompt } from '../calc/buildPrompt'\nimport { parseReply } from '../calc/parseReply'\nimport type { ChatPort } from '../port/ChatPort'\nimport { EmojiPicker } from './EmojiPicker'\nimport { VoiceRecordingOverlay } from './VoiceRecordingOverlay'\nimport { VoiceMessageBubble } from './VoiceMessageBubble'\nimport { useSpeechRecognition } from './useSpeechRecognition'\nimport { useAudioRecorder, type RecordedAudio } from './useAudioRecorder'\n\ntype Props = {\n /** The character to chat with — shapes the system prompt + the header/avatar. */\n readonly persona: CharacterPersona\n /** Injected LLM backend. Without it, the input is disabled. */\n readonly port?: ChatPort | undefined\n /** Fired after each assistant reply (e.g. to drive mood-tagged video later). */\n readonly onReply?: ((reply: ChatReply) => void) | undefined\n /** Read an assistant message aloud (e.g. the voice synth). No prop → no speaker / auto-play. */\n readonly onSpeak?: ((text: string) => void | Promise<void>) | undefined\n /** Back button in the header. No prop → no back button. */\n readonly onClose?: (() => void) | undefined\n /** Start a video call (header phone button). No prop → no call button. */\n readonly onCall?: (() => void) | undefined\n /** Avatar for the user's own bubbles. */\n readonly userAvatarUrl?: string | undefined\n /** Speech-to-text language for the mic. */\n readonly speechLang?: string | undefined\n}\n\nconst GREETING_ID = 'greeting'\nconst DEFAULT_USER_AVATAR = 'https://api.dicebear.com/7.x/avataaars/svg?seed=user'\n\nconst seedGreeting = (persona: CharacterPersona): ChatMessage[] =>\n persona.greeting\n ? [{ id: GREETING_ID, role: 'assistant', content: persona.greeting, timestamp: Date.now() }]\n : []\n\n/**\n * Full-screen text chat — a faithful clone of mate's ChatPage: a gradient header\n * (back · avatar with online dot · name + personality · auto-play-voice toggle ·\n * clear), avatar/bubble message rows with timestamps + per-assistant read-aloud,\n * the purple typing dots, and an emoji + mic (speech-to-text) input toolbar.\n * Self-contained: owns the conversation, seeds the greeting, builds the prompt\n * (pure) → ChatPort → parses (pure). In-memory (no persistence yet).\n */\nexport function ChatWindow({ persona, port, onReply, onSpeak, onClose, onCall, userAvatarUrl, speechLang }: Props) {\n const [messages, setMessages] = useState<ChatMessage[]>(() => seedGreeting(persona))\n const [input, setInput] = useState('')\n const [sending, setSending] = useState(false)\n const [speakingId, setSpeakingId] = useState<string | null>(null)\n const [autoPlay, setAutoPlay] = useState(false)\n const [showEmoji, setShowEmoji] = useState(false)\n const [pendingVoice, setPendingVoice] = useState<RecordedAudio | null>(null)\n const endRef = useRef<HTMLDivElement>(null)\n const speech = useSpeechRecognition(setInput, speechLang)\n const recorder = useAudioRecorder()\n const micActive = recorder.recording || speech.listening\n\n // The mic records audio AND transcribes (speech-to-text) at once, so a sent\n // message carries both the playable voice clip and its transcript.\n const toggleMic = async () => {\n if (micActive) {\n speech.stop()\n const v = await recorder.stop()\n if (v) setPendingVoice(v)\n } else {\n setPendingVoice(null)\n void recorder.start()\n speech.start()\n }\n }\n\n useEffect(() => {\n endRef.current?.scrollIntoView({ behavior: 'smooth' })\n }, [messages.length, sending])\n\n const speak = async (id: string, text: string) => {\n if (!onSpeak) return\n setSpeakingId(id)\n try {\n await onSpeak(text)\n } catch (err) {\n console.warn('[chat] speak failed:', err)\n } finally {\n setSpeakingId(null)\n }\n }\n\n const send = async () => {\n const text = input.trim()\n if (!port || !text || sending) return\n // Finalize any in-progress recording so the message carries its audio.\n let voice = pendingVoice\n if (recorder.recording) {\n speech.stop()\n const v = await recorder.stop()\n if (v) voice = v\n }\n setPendingVoice(null)\n const history = messages.filter((m) => m.id !== GREETING_ID) // greeting is virtual\n setMessages((m) => [\n ...m,\n {\n id: crypto.randomUUID(),\n role: 'user',\n content: text,\n timestamp: Date.now(),\n ...(voice ? { voiceUrl: voice.url, voiceDuration: voice.duration } : {}),\n },\n ])\n setInput('')\n setSending(true)\n try {\n const raw = await port.complete(buildPrompt(persona, history, text))\n const reply = parseReply(raw)\n const id = crypto.randomUUID()\n setMessages((m) => [...m, { id, role: 'assistant', content: reply.text, timestamp: Date.now() }])\n onReply?.(reply)\n if (autoPlay && onSpeak) void speak(id, reply.text)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n setMessages((m) => [...m, { id: crypto.randomUUID(), role: 'assistant', content: `(出错:${msg})`, timestamp: Date.now() }])\n } finally {\n setSending(false)\n }\n }\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n void send()\n }\n }\n\n const inputDisabled = !port || sending\n\n return (\n <div className=\"flex h-full min-h-0 flex-col bg-gradient-to-b from-purple-50 to-white\">\n <VoiceRecordingOverlay\n recording={micActive}\n seconds={recorder.recording ? recorder.duration : speech.seconds}\n transcript={input}\n onStop={() => void toggleMic()}\n />\n\n {/* Header */}\n <div className=\"flex shrink-0 items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-purple-50 to-white px-4 py-3\">\n {onClose ? (\n <button type=\"button\" onClick={onClose} className=\"rounded-full p-1.5 text-gray-600 transition hover:bg-purple-100\" title=\"返回\">\n <ArrowLeft className=\"h-4 w-4\" />\n </button>\n ) : null}\n <div className=\"relative shrink-0\">\n <img src={persona.avatarUrl ?? DEFAULT_USER_AVATAR} alt={persona.name} className=\"h-12 w-12 rounded-full object-cover ring-2 ring-purple-100\" />\n <span className=\"absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 ring-2 ring-white\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <h1 className=\"truncate text-lg font-semibold text-gray-900\">{persona.name}</h1>\n <p className=\"truncate text-xs text-gray-500\">{persona.personality ?? persona.description ?? ''}</p>\n </div>\n {onSpeak ? (\n <button\n type=\"button\"\n onClick={() => setAutoPlay((v) => !v)}\n title=\"自动朗读回复\"\n className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition ${\n autoPlay ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'\n }`}\n >\n {autoPlay ? <Volume2 className=\"h-3.5 w-3.5\" /> : <VolumeX className=\"h-3.5 w-3.5\" />}\n <span className=\"hidden sm:inline\">自动语音</span>\n </button>\n ) : null}\n {onCall ? (\n <button\n type=\"button\"\n onClick={onCall}\n title=\"视频通话\"\n className=\"rounded-full p-1.5 text-gray-500 transition hover:bg-purple-100 hover:text-purple-600\"\n >\n <Phone className=\"h-4 w-4\" />\n </button>\n ) : null}\n <button\n type=\"button\"\n onClick={() => setMessages(seedGreeting(persona))}\n title=\"清空对话\"\n className=\"rounded-full p-1.5 text-gray-500 transition hover:bg-red-100 hover:text-red-600\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n\n {/* Messages */}\n <div className=\"flex-1 space-y-4 overflow-y-auto p-3 sm:p-6\">\n {messages.map((m) => {\n const isUser = m.role === 'user'\n return (\n <div key={m.id} className={`flex gap-2.5 ${isUser ? 'flex-row-reverse' : 'flex-row'}`}>\n <img\n src={isUser ? (userAvatarUrl ?? DEFAULT_USER_AVATAR) : (persona.avatarUrl ?? DEFAULT_USER_AVATAR)}\n alt={isUser ? 'You' : persona.name}\n className=\"h-8 w-8 shrink-0 rounded-full bg-gray-100 object-cover shadow-sm ring-2 ring-white\"\n />\n <div className=\"flex max-w-[75%] flex-col\">\n <div\n className={`rounded-2xl px-3.5 py-2.5 shadow-sm transition-shadow hover:shadow-md ${\n isUser ? 'bg-gray-100 text-gray-900' : 'border border-gray-100 bg-white text-gray-900'\n }`}\n >\n {m.voiceUrl && m.voiceDuration ? (\n <div className=\"mb-2 min-w-[12rem]\">\n <VoiceMessageBubble audioUrl={m.voiceUrl} duration={m.voiceDuration} isUser={isUser} />\n </div>\n ) : null}\n <p className=\"whitespace-pre-wrap break-words text-sm leading-relaxed\">{m.content}</p>\n <div className=\"mt-1 flex items-center gap-2\">\n <span className=\"text-[10px] text-gray-400\">{m.timestamp ? new Date(m.timestamp).toLocaleTimeString() : ''}</span>\n {!isUser && onSpeak ? (\n <button\n type=\"button\"\n onClick={() => void speak(m.id, m.content)}\n disabled={speakingId !== null && speakingId !== m.id}\n title=\"朗读\"\n className=\"ml-auto inline-flex h-6 w-6 items-center justify-center rounded-full text-gray-500 transition hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-30\"\n >\n {speakingId === m.id ? <VolumeX className=\"h-3.5 w-3.5 text-purple-600\" /> : <Volume2 className=\"h-3.5 w-3.5\" />}\n </button>\n ) : null}\n </div>\n </div>\n </div>\n </div>\n )\n })}\n {sending ? (\n <div className=\"flex justify-start\">\n <div className=\"rounded-2xl border border-gray-100 bg-white px-4 py-2.5 shadow-sm\">\n <span className=\"flex gap-1.5\">\n <span className=\"h-1.5 w-1.5 animate-bounce rounded-full bg-purple-400 [animation-delay:0ms]\" />\n <span className=\"h-1.5 w-1.5 animate-bounce rounded-full bg-purple-400 [animation-delay:150ms]\" />\n <span className=\"h-1.5 w-1.5 animate-bounce rounded-full bg-purple-400 [animation-delay:300ms]\" />\n </span>\n </div>\n </div>\n ) : null}\n <div ref={endRef} />\n </div>\n\n {/* Toolbar */}\n <div className=\"flex shrink-0 items-center gap-1 border-t border-gray-100 bg-white px-3 py-1.5\">\n <div className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setShowEmoji((v) => !v)}\n className=\"rounded-lg p-1.5 text-gray-500 transition hover:bg-purple-50 hover:text-purple-600\"\n title=\"表情\"\n >\n <Smile className=\"h-5 w-5\" />\n </button>\n {showEmoji ? (\n <EmojiPicker\n onSelect={(e) => {\n setInput((v) => v + e)\n setShowEmoji(false)\n }}\n />\n ) : null}\n </div>\n <button\n type=\"button\"\n onClick={() => void toggleMic()}\n disabled={inputDisabled}\n className={`rounded-lg p-1.5 transition ${\n micActive ? 'animate-pulse bg-red-50 text-red-600' : 'text-gray-500 hover:bg-purple-50 hover:text-purple-600'\n } disabled:cursor-not-allowed disabled:opacity-40`}\n title={micActive ? '停止录音' : '语音消息(录音 + 转写)'}\n >\n <Mic className=\"h-5 w-5\" />\n </button>\n </div>\n\n {/* Input */}\n <div className=\"flex shrink-0 items-end gap-2 bg-white px-3 pb-3 pt-1\">\n <div className=\"flex-1 rounded-2xl border border-gray-200 bg-gray-50 p-2.5\">\n <textarea\n rows={1}\n value={input}\n disabled={inputDisabled}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder={port ? `给 ${persona.name} 发消息…(Enter 发送)` : '需注入 ChatPort 才能对话'}\n className=\"max-h-[140px] min-h-[40px] w-full resize-none overflow-y-auto border-0 bg-transparent p-0 text-sm leading-relaxed text-gray-900 placeholder-gray-400 outline-none\"\n />\n </div>\n <button\n type=\"button\"\n onClick={() => void send()}\n disabled={inputDisabled || input.trim().length === 0}\n className=\"inline-flex h-11 w-11 shrink-0 items-center justify-center self-end rounded-xl bg-slate-900 text-white shadow-sm transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {sending ? <Loader2 className=\"h-5 w-5 animate-spin\" /> : <Send className=\"h-5 w-5\" />}\n </button>\n </div>\n </div>\n )\n}\n","// A small inline emoji grid (no external dep). mate uses a full EmojiPicker; this\n// covers the common set for the chat input.\nconst EMOJIS = [\n '😊', '😂', '❤️', '👍', '🙏', '😍', '🎉', '😢', '😮', '🔥', '✨', '😅', '🤔', '👏', '🥰', '😎',\n '😭', '🙄', '💪', '🌟', '😴', '🤗', '😜', '🤩', '👀', '👋', '💯', '🙌', '😇', '🤝', '🫶', '😆',\n] as const\n\nexport function EmojiPicker({ onSelect }: { onSelect: (emoji: string) => void }) {\n return (\n <div className=\"absolute bottom-9 left-0 z-20 grid w-64 grid-cols-8 gap-0.5 rounded-xl border border-gray-200 bg-white p-2 shadow-lg\">\n {EMOJIS.map((e, i) => (\n <button\n key={`${e}-${i}`}\n type=\"button\"\n onClick={() => onSelect(e)}\n className=\"rounded p-1 text-lg leading-none transition hover:bg-gray-100\"\n >\n {e}\n </button>\n ))}\n </div>\n )\n}\n","import { Mic } from 'lucide-react'\n\n// Full-screen purple recording overlay with a pulsing mic + duration + the live\n// transcript (mirrors mate's VoiceRecordingOverlay).\nfunction fmt(seconds: number): string {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n}\n\ntype Props = {\n readonly recording: boolean\n readonly seconds: number\n readonly transcript?: string | undefined\n readonly onStop: () => void\n}\n\nexport function VoiceRecordingOverlay({ recording, seconds, transcript = '', onStop }: Props) {\n if (!recording) return null\n return (\n <div\n onClick={onStop}\n className=\"fixed inset-0 z-[60] flex flex-col items-center justify-center bg-gradient-to-br from-purple-600/95 to-purple-800/95 backdrop-blur-sm\"\n >\n <div className=\"flex flex-col items-center gap-8\">\n <div className=\"relative\">\n <div className=\"absolute inset-0 animate-ping\">\n <div className=\"mx-auto h-32 w-32 rounded-full bg-white/20\" />\n </div>\n <div className=\"absolute inset-0 animate-pulse\">\n <div className=\"mx-auto h-32 w-32 rounded-full bg-white/30\" />\n </div>\n <div className=\"relative flex h-32 w-32 items-center justify-center rounded-full bg-white/40\">\n <Mic className=\"h-16 w-16 text-white\" />\n </div>\n </div>\n <div className=\"text-center text-white\">\n <h2 className=\"mb-2 text-3xl font-semibold\">录音中…</h2>\n <p className=\"font-mono text-xl text-white/90\">{fmt(seconds)}</p>\n <p className=\"mt-4 text-sm text-white/70\">对着麦克风清晰地说话,点任意处停止</p>\n </div>\n {transcript ? (\n <div className=\"mt-2 w-full max-w-2xl px-6\">\n <div className=\"rounded-2xl border border-white/20 bg-white/10 p-6 backdrop-blur-md\">\n <p className=\"mb-2 text-xs uppercase tracking-wider text-white/60\">你说的:</p>\n <p className=\"whitespace-pre-wrap text-lg leading-relaxed text-white\">{transcript}</p>\n </div>\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { Play, Pause } from 'lucide-react'\n\n// A playable voice-message row: play/pause + progress bar + \"m:ss / m:ss\"\n// (mirrors mate's VoiceMessageBubble). Rendered inside a chat bubble above the\n// transcript text.\nfunction fmt(seconds: number): string {\n const m = Math.floor(seconds / 60)\n const s = Math.floor(seconds % 60)\n return `${m}:${String(s).padStart(2, '0')}`\n}\n\ntype Props = {\n readonly audioUrl: string\n readonly duration: number\n readonly isUser?: boolean\n}\n\nexport function VoiceMessageBubble({ audioUrl, duration, isUser = false }: Props) {\n const [playing, setPlaying] = useState(false)\n const [current, setCurrent] = useState(0)\n const audioRef = useRef<HTMLAudioElement | null>(null)\n\n useEffect(() => {\n const audio = new Audio(audioUrl)\n audioRef.current = audio\n const onTime = () => setCurrent(audio.currentTime)\n const onEnd = () => {\n setPlaying(false)\n setCurrent(0)\n }\n audio.addEventListener('timeupdate', onTime)\n audio.addEventListener('ended', onEnd)\n return () => {\n audio.pause()\n audio.removeEventListener('timeupdate', onTime)\n audio.removeEventListener('ended', onEnd)\n audio.src = ''\n }\n }, [audioUrl])\n\n const toggle = () => {\n const audio = audioRef.current\n if (!audio) return\n if (playing) audio.pause()\n else void audio.play()\n setPlaying(!playing)\n }\n\n const progress = duration > 0 ? Math.min(100, (current / duration) * 100) : 0\n\n return (\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={toggle}\n className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-full transition hover:scale-105 ${\n isUser ? 'bg-gray-600/20 hover:bg-gray-600/30' : 'bg-purple-100 hover:bg-purple-200'\n }`}\n >\n {playing ? (\n <Pause className={`h-5 w-5 ${isUser ? 'text-gray-700' : 'text-purple-600'}`} />\n ) : (\n <Play className={`h-5 w-5 ${isUser ? 'text-gray-700' : 'text-purple-600'}`} />\n )}\n </button>\n <div className=\"flex flex-1 flex-col gap-1\">\n <div className=\"relative h-1 overflow-hidden rounded-full bg-gray-300\">\n <div\n className={`absolute left-0 top-0 h-full transition-all ${isUser ? 'bg-gray-600' : 'bg-purple-500'}`}\n style={{ width: `${progress}%` }}\n />\n </div>\n <div className={`text-xs ${isUser ? 'text-gray-600' : 'text-gray-500'}`}>\n {fmt(current)} / {fmt(duration)}\n </div>\n </div>\n </div>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\n\n// Voice input via the browser SpeechRecognition API (Chrome/Edge). The mic\n// transcribes speech to text live; the caller drops the transcript into the chat\n// input (mirrors mate's speech-to-text mic). No backend, no key.\n\n// The Web Speech API isn't in the standard TS DOM lib — type it loosely.\ntype SpeechRecognitionLike = {\n lang: string\n continuous: boolean\n interimResults: boolean\n onresult: ((e: { results: ArrayLike<ArrayLike<{ transcript: string }>> }) => void) | null\n onend: (() => void) | null\n onerror: (() => void) | null\n start(): void\n stop(): void\n}\n\nfunction getCtor(): (new () => SpeechRecognitionLike) | null {\n if (typeof window === 'undefined') return null\n const w = window as unknown as Record<string, unknown>\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as (new () => SpeechRecognitionLike) | null\n}\n\nexport function useSpeechRecognition(onTranscript: (text: string) => void, lang = 'zh-CN') {\n const [listening, setListening] = useState(false)\n const [seconds, setSeconds] = useState(0)\n const recRef = useRef<SpeechRecognitionLike | null>(null)\n const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const supported = getCtor() !== null\n\n const clearTimer = () => {\n if (timerRef.current) {\n clearInterval(timerRef.current)\n timerRef.current = null\n }\n }\n\n const stop = () => {\n recRef.current?.stop()\n recRef.current = null\n clearTimer()\n setListening(false)\n setSeconds(0)\n }\n\n const start = () => {\n const Ctor = getCtor()\n if (!Ctor) {\n window.alert('当前浏览器不支持语音输入(请用 Chrome / Edge)。')\n return\n }\n const rec = new Ctor()\n rec.lang = lang\n rec.continuous = true\n rec.interimResults = true\n rec.onresult = (e) => {\n let text = ''\n for (let i = 0; i < e.results.length; i++) {\n const alt = e.results[i]?.[0]\n if (alt) text += alt.transcript\n }\n onTranscript(text)\n }\n rec.onend = () => {\n clearTimer()\n setListening(false)\n setSeconds(0)\n recRef.current = null\n }\n rec.onerror = () => {\n clearTimer()\n setListening(false)\n setSeconds(0)\n recRef.current = null\n }\n rec.start()\n recRef.current = rec\n setSeconds(0)\n setListening(true)\n timerRef.current = setInterval(() => setSeconds((s) => s + 1), 1000)\n }\n\n // Clean up on unmount.\n useEffect(() => () => stop(), [])\n\n return { supported, listening, seconds, start, stop }\n}\n","import { useEffect, useRef, useState } from 'react'\n\n// Records mic audio via MediaRecorder → a blob: URL + duration (mirrors mate's\n// useAudioRecorder). Runs alongside speech recognition so a voice message\n// carries both the audio and its transcript.\n\nexport type RecordedAudio = { url: string; duration: number }\n\nexport function useAudioRecorder() {\n const [recording, setRecording] = useState(false)\n const [duration, setDuration] = useState(0)\n const recRef = useRef<MediaRecorder | null>(null)\n const chunksRef = useRef<Blob[]>([])\n const startedRef = useRef(0)\n const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const resolveRef = useRef<((a: RecordedAudio | null) => void) | null>(null)\n\n const clearTimer = () => {\n if (timerRef.current) {\n clearInterval(timerRef.current)\n timerRef.current = null\n }\n }\n\n const start = async (): Promise<boolean> => {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) return false\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true })\n const mr = new MediaRecorder(stream)\n chunksRef.current = []\n mr.ondataavailable = (e) => {\n if (e.data.size > 0) chunksRef.current.push(e.data)\n }\n mr.onstop = () => {\n const blob = new Blob(chunksRef.current, { type: mr.mimeType || 'audio/webm' })\n const url = URL.createObjectURL(blob)\n const dur = Math.max(1, Math.round((Date.now() - startedRef.current) / 1000))\n stream.getTracks().forEach((t) => t.stop())\n clearTimer()\n setRecording(false)\n resolveRef.current?.({ url, duration: dur })\n resolveRef.current = null\n }\n mr.start()\n recRef.current = mr\n startedRef.current = Date.now()\n setDuration(0)\n setRecording(true)\n timerRef.current = setInterval(() => setDuration((d) => d + 1), 1000)\n return true\n } catch {\n return false\n }\n }\n\n /** Stop and resolve with the recorded audio (null if not recording). */\n const stop = (): Promise<RecordedAudio | null> =>\n new Promise((resolve) => {\n const mr = recRef.current\n if (!mr || mr.state === 'inactive') {\n resolve(null)\n return\n }\n resolveRef.current = resolve\n mr.stop()\n recRef.current = null\n })\n\n useEffect(\n () => () => {\n recRef.current?.stop()\n clearTimer()\n },\n [],\n )\n\n return { recording, duration, start, stop }\n}\n"],"mappings":"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","names":["useEffect","useRef","useState","Mic","jsx","jsx","jsxs","fmt","useEffect","useRef","useState","useEffect","useRef","useState","jsx","jsxs","useState","useRef","useEffect","Mic"]}
1
+ {"version":3,"sources":["../src/calc/buildPrompt.ts","../src/calc/parseReply.ts","../src/ui/ChatWindow.tsx","../src/ui/EmojiPicker.tsx","../src/ui/VoiceRecordingOverlay.tsx","../src/ui/VoiceMessageBubble.tsx","../src/ui/useSpeechRecognition.ts","../src/ui/useAudioRecorder.ts"],"sourcesContent":["// Pure: turn the persona + recent history + the new user message into the single\n// prompt string the LLM gets. Mirrors mate's grokAI.buildPrompt — including the\n// \"reply in JSON {text, mood}\" instruction the parser depends on, and the 1-3\n// sentence length guidance (which keeps replies short enough for clip matching).\n\nimport type { CharacterPersona, ChatMessage } from '../data'\n\n/** How many trailing messages of history to include for context. */\nexport const HISTORY_WINDOW = 5\n\nexport function buildPrompt(\n persona: CharacterPersona,\n history: readonly ChatMessage[],\n userMessage: string,\n): string {\n const historyContext = history\n .slice(-HISTORY_WINDOW)\n .map((m) => `${m.role === 'user' ? 'User' : persona.name}: ${m.content}`)\n .join('\\n')\n\n return `You are ${persona.name}.\n\nCharacter background: ${persona.description ?? ''}\n\nPersonality traits: ${persona.personality ?? ''}\n\nYour greeting message: \"${persona.greeting ?? ''}\"\n\nConversation history:\n${historyContext}\n\nUser: ${userMessage}\n\nRespond as ${persona.name} would, staying in character with the background and personality described above. Keep responses natural, engaging, and consistent. Use 1-3 sentences unless a longer response is clearly needed.\n\nIMPORTANT: Respond in JSON format with your reply text and current mood:\n{\"text\": \"your response here\", \"mood\": \"happy|calm|angry|sad\"}\n\nChoose the mood that best matches the emotional tone of your response. Only use: happy, calm, angry, or sad.`\n}\n","// Pure: extract { text, mood } from the LLM's raw completion. The model is asked\n// to answer as JSON {\"text\": ..., \"mood\": ...}; we pull the first JSON object\n// containing \"text\", validate the mood, and fall back to the raw text + neutral\n// mood if anything is off. Mirrors mate's grokAI.parseAIResponse.\n\nimport type { ChatMood, ChatReply } from '../data'\n\nconst VALID_MOODS: ChatMood[] = ['happy', 'calm', 'angry', 'sad']\n\nexport function parseReply(raw: string): ChatReply {\n const match = raw.match(/\\{[\\s\\S]*\"text\"[\\s\\S]*\\}/)\n if (match) {\n try {\n const parsed = JSON.parse(match[0]) as { text?: unknown; mood?: unknown }\n const text = typeof parsed.text === 'string' && parsed.text ? parsed.text : raw\n const mood = VALID_MOODS.includes(parsed.mood as ChatMood) ? (parsed.mood as ChatMood) : 'neutral'\n return { text, mood }\n } catch {\n /* malformed JSON — fall through */\n }\n }\n return { text: raw, mood: 'neutral' }\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { Send, Loader2, Volume2, VolumeX, Smile, Mic, ArrowLeft, Trash2, Phone, Video } from 'lucide-react'\nimport type { CharacterPersona, ChatMessage, ChatReply } from '../data'\nimport { buildPrompt } from '../calc/buildPrompt'\nimport { parseReply } from '../calc/parseReply'\nimport type { ChatPort } from '../port/ChatPort'\nimport { EmojiPicker } from './EmojiPicker'\nimport { VoiceRecordingOverlay } from './VoiceRecordingOverlay'\nimport { VoiceMessageBubble } from './VoiceMessageBubble'\nimport { useSpeechRecognition } from './useSpeechRecognition'\nimport { useAudioRecorder, type RecordedAudio } from './useAudioRecorder'\n\ntype Props = {\n /** The character to chat with — shapes the system prompt + the header/avatar. */\n readonly persona: CharacterPersona\n /** Injected LLM backend. Without it, the input is disabled. */\n readonly port?: ChatPort | undefined\n /** Fired after each assistant reply (e.g. to drive mood-tagged video later). */\n readonly onReply?: ((reply: ChatReply) => void) | undefined\n /** Emitted after each completed turn (user said X → assistant replied Y). The\n * host persists it if it wants — ChatWindow itself stays in-memory. Aligns\n * with VoiceCall / VideoCall's onTurn. */\n readonly onTurn?: ((user: ChatMessage, ai: ChatReply) => void) | undefined\n /** Read an assistant message aloud (e.g. the voice synth). No prop → no speaker / auto-play. */\n readonly onSpeak?: ((text: string) => void | Promise<void>) | undefined\n /** Back button in the header; receives the transcript (greeting excluded) so\n * the host can persist it on exit. No prop → no back button. */\n readonly onClose?: ((history: readonly ChatMessage[]) => void) | undefined\n /** Start a voice-only call (toolbar phone button). No prop → no button. */\n readonly onVoiceCall?: (() => void) | undefined\n /** Start a talking-head video call (toolbar video button). No prop → no button. */\n readonly onVideoCall?: (() => void) | undefined\n /** Avatar for the user's own bubbles. */\n readonly userAvatarUrl?: string | undefined\n /** Speech-to-text language for the mic. */\n readonly speechLang?: string | undefined\n}\n\nconst GREETING_ID = 'greeting'\nconst DEFAULT_USER_AVATAR = 'https://api.dicebear.com/7.x/avataaars/svg?seed=user'\n\nconst seedGreeting = (persona: CharacterPersona): ChatMessage[] =>\n persona.greeting\n ? [{ id: GREETING_ID, role: 'assistant', content: persona.greeting, timestamp: Date.now() }]\n : []\n\n/**\n * Full-screen text chat — a faithful clone of mate's ChatPage: a gradient header\n * (back · avatar with online dot · name + personality · auto-play-voice toggle ·\n * clear), avatar/bubble message rows with timestamps + per-assistant read-aloud,\n * the purple typing dots, and an emoji + mic (speech-to-text) input toolbar.\n * Self-contained: owns the conversation, seeds the greeting, builds the prompt\n * (pure) → ChatPort → parses (pure). In-memory (no persistence yet).\n */\nexport function ChatWindow({ persona, port, onReply, onTurn, onSpeak, onClose, onVoiceCall, onVideoCall, userAvatarUrl, speechLang }: Props) {\n const [messages, setMessages] = useState<ChatMessage[]>(() => seedGreeting(persona))\n const [input, setInput] = useState('')\n const [sending, setSending] = useState(false)\n const [speakingId, setSpeakingId] = useState<string | null>(null)\n const [autoPlay, setAutoPlay] = useState(false)\n const [showEmoji, setShowEmoji] = useState(false)\n const [pendingVoice, setPendingVoice] = useState<RecordedAudio | null>(null)\n const endRef = useRef<HTMLDivElement>(null)\n const speech = useSpeechRecognition(setInput, speechLang)\n const recorder = useAudioRecorder()\n const micActive = recorder.recording || speech.listening\n\n // The mic records audio AND transcribes (speech-to-text) at once, so a sent\n // message carries both the playable voice clip and its transcript.\n const toggleMic = async () => {\n if (micActive) {\n speech.stop()\n const v = await recorder.stop()\n if (v) setPendingVoice(v)\n } else {\n setPendingVoice(null)\n void recorder.start()\n speech.start()\n }\n }\n\n useEffect(() => {\n endRef.current?.scrollIntoView({ behavior: 'smooth' })\n }, [messages.length, sending])\n\n const speak = async (id: string, text: string) => {\n if (!onSpeak) return\n setSpeakingId(id)\n try {\n await onSpeak(text)\n } catch (err) {\n console.warn('[chat] speak failed:', err)\n } finally {\n setSpeakingId(null)\n }\n }\n\n const send = async () => {\n const text = input.trim()\n if (!port || !text || sending) return\n // Finalize any in-progress recording so the message carries its audio.\n let voice = pendingVoice\n if (recorder.recording) {\n speech.stop()\n const v = await recorder.stop()\n if (v) voice = v\n }\n setPendingVoice(null)\n const history = messages.filter((m) => m.id !== GREETING_ID) // greeting is virtual\n const userMsg: ChatMessage = {\n id: crypto.randomUUID(),\n role: 'user',\n content: text,\n timestamp: Date.now(),\n ...(voice ? { voiceUrl: voice.url, voiceDuration: voice.duration } : {}),\n }\n setMessages((m) => [...m, userMsg])\n setInput('')\n setSending(true)\n try {\n const raw = await port.complete(buildPrompt(persona, history, text))\n const reply = parseReply(raw)\n const id = crypto.randomUUID()\n setMessages((m) => [...m, { id, role: 'assistant', content: reply.text, timestamp: Date.now() }])\n onReply?.(reply)\n onTurn?.(userMsg, reply)\n if (autoPlay && onSpeak) void speak(id, reply.text)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n setMessages((m) => [...m, { id: crypto.randomUUID(), role: 'assistant', content: `(出错:${msg})`, timestamp: Date.now() }])\n } finally {\n setSending(false)\n }\n }\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n void send()\n }\n }\n\n const inputDisabled = !port || sending\n\n return (\n <div className=\"flex h-full min-h-0 flex-col bg-gradient-to-b from-purple-50 to-white\">\n <VoiceRecordingOverlay\n recording={micActive}\n seconds={recorder.recording ? recorder.duration : speech.seconds}\n transcript={input}\n onStop={() => void toggleMic()}\n />\n\n {/* Header */}\n <div className=\"flex shrink-0 items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-purple-50 to-white px-4 py-3\">\n {onClose ? (\n <button type=\"button\" onClick={() => onClose(messages.filter((m) => m.id !== GREETING_ID))} className=\"rounded-full p-1.5 text-gray-600 transition hover:bg-purple-100\" title=\"返回\">\n <ArrowLeft className=\"h-4 w-4\" />\n </button>\n ) : null}\n <div className=\"relative shrink-0\">\n <img src={persona.avatarUrl ?? DEFAULT_USER_AVATAR} alt={persona.name} className=\"h-12 w-12 rounded-full object-cover ring-2 ring-purple-100\" />\n <span className=\"absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 ring-2 ring-white\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <h1 className=\"truncate text-lg font-semibold text-gray-900\">{persona.name}</h1>\n <p className=\"truncate text-xs text-gray-500\">{persona.personality ?? persona.description ?? ''}</p>\n </div>\n {onSpeak ? (\n <button\n type=\"button\"\n onClick={() => setAutoPlay((v) => !v)}\n title=\"自动朗读回复\"\n className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition ${\n autoPlay ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'\n }`}\n >\n {autoPlay ? <Volume2 className=\"h-3.5 w-3.5\" /> : <VolumeX className=\"h-3.5 w-3.5\" />}\n <span className=\"hidden sm:inline\">自动语音</span>\n </button>\n ) : null}\n <button\n type=\"button\"\n onClick={() => setMessages(seedGreeting(persona))}\n title=\"清空对话\"\n className=\"rounded-full p-1.5 text-gray-500 transition hover:bg-red-100 hover:text-red-600\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n\n {/* Messages */}\n <div className=\"flex-1 space-y-4 overflow-y-auto p-3 sm:p-6\">\n {messages.map((m) => {\n const isUser = m.role === 'user'\n return (\n <div key={m.id} className={`flex gap-2.5 ${isUser ? 'flex-row-reverse' : 'flex-row'}`}>\n <img\n src={isUser ? (userAvatarUrl ?? DEFAULT_USER_AVATAR) : (persona.avatarUrl ?? DEFAULT_USER_AVATAR)}\n alt={isUser ? 'You' : persona.name}\n className=\"h-8 w-8 shrink-0 rounded-full bg-gray-100 object-cover shadow-sm ring-2 ring-white\"\n />\n <div className=\"flex max-w-[75%] flex-col\">\n <div\n className={`rounded-2xl px-3.5 py-2.5 shadow-sm transition-shadow hover:shadow-md ${\n isUser ? 'bg-gray-100 text-gray-900' : 'border border-gray-100 bg-white text-gray-900'\n }`}\n >\n {m.voiceUrl && m.voiceDuration ? (\n <div className=\"mb-2 min-w-[12rem]\">\n <VoiceMessageBubble audioUrl={m.voiceUrl} duration={m.voiceDuration} isUser={isUser} />\n </div>\n ) : null}\n <p className=\"whitespace-pre-wrap break-words text-sm leading-relaxed\">{m.content}</p>\n <div className=\"mt-1 flex items-center gap-2\">\n <span className=\"text-[10px] text-gray-400\">{m.timestamp ? new Date(m.timestamp).toLocaleTimeString() : ''}</span>\n {!isUser && onSpeak ? (\n <button\n type=\"button\"\n onClick={() => void speak(m.id, m.content)}\n disabled={speakingId !== null && speakingId !== m.id}\n title=\"朗读\"\n className=\"ml-auto inline-flex h-6 w-6 items-center justify-center rounded-full text-gray-500 transition hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-30\"\n >\n {speakingId === m.id ? <VolumeX className=\"h-3.5 w-3.5 text-purple-600\" /> : <Volume2 className=\"h-3.5 w-3.5\" />}\n </button>\n ) : null}\n </div>\n </div>\n </div>\n </div>\n )\n })}\n {sending ? (\n <div className=\"flex justify-start\">\n <div className=\"rounded-2xl border border-gray-100 bg-white px-4 py-2.5 shadow-sm\">\n <span className=\"flex gap-1.5\">\n <span className=\"h-1.5 w-1.5 animate-bounce rounded-full bg-purple-400 [animation-delay:0ms]\" />\n <span className=\"h-1.5 w-1.5 animate-bounce rounded-full bg-purple-400 [animation-delay:150ms]\" />\n <span className=\"h-1.5 w-1.5 animate-bounce rounded-full bg-purple-400 [animation-delay:300ms]\" />\n </span>\n </div>\n </div>\n ) : null}\n <div ref={endRef} />\n </div>\n\n {/* Toolbar */}\n <div className=\"flex shrink-0 items-center gap-1 border-t border-gray-100 bg-white px-3 py-1.5\">\n <div className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setShowEmoji((v) => !v)}\n className=\"rounded-lg p-1.5 text-gray-500 transition hover:bg-purple-50 hover:text-purple-600\"\n title=\"表情\"\n >\n <Smile className=\"h-5 w-5\" />\n </button>\n {showEmoji ? (\n <EmojiPicker\n onSelect={(e) => {\n setInput((v) => v + e)\n setShowEmoji(false)\n }}\n />\n ) : null}\n </div>\n <button\n type=\"button\"\n onClick={() => void toggleMic()}\n disabled={inputDisabled}\n className={`rounded-lg p-1.5 transition ${\n micActive ? 'animate-pulse bg-red-50 text-red-600' : 'text-gray-500 hover:bg-purple-50 hover:text-purple-600'\n } disabled:cursor-not-allowed disabled:opacity-40`}\n title={micActive ? '停止录音' : '语音消息(录音 + 转写)'}\n >\n <Mic className=\"h-5 w-5\" />\n </button>\n\n {/* Call launchers, pushed to the toolbar's right edge. */}\n {onVoiceCall ? (\n <button\n type=\"button\"\n onClick={onVoiceCall}\n title=\"语音通话\"\n className=\"ml-auto rounded-lg p-1.5 text-gray-500 transition hover:bg-purple-50 hover:text-purple-600\"\n >\n <Phone className=\"h-5 w-5\" />\n </button>\n ) : null}\n {onVideoCall ? (\n <button\n type=\"button\"\n onClick={onVideoCall}\n title=\"视频通话\"\n className={`rounded-lg p-1.5 text-gray-500 transition hover:bg-purple-50 hover:text-purple-600 ${onVoiceCall ? '' : 'ml-auto'}`}\n >\n <Video className=\"h-5 w-5\" />\n </button>\n ) : null}\n </div>\n\n {/* Input */}\n <div className=\"flex shrink-0 items-end gap-2 bg-white px-3 pb-3 pt-1\">\n <div className=\"flex-1 rounded-2xl border border-gray-200 bg-gray-50 p-2.5\">\n <textarea\n rows={1}\n value={input}\n disabled={inputDisabled}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder={port ? `给 ${persona.name} 发消息…(Enter 发送)` : '需注入 ChatPort 才能对话'}\n className=\"max-h-[140px] min-h-[40px] w-full resize-none overflow-y-auto border-0 bg-transparent p-0 text-sm leading-relaxed text-gray-900 placeholder-gray-400 outline-none\"\n />\n </div>\n <button\n type=\"button\"\n onClick={() => void send()}\n disabled={inputDisabled || input.trim().length === 0}\n className=\"inline-flex h-11 w-11 shrink-0 items-center justify-center self-end rounded-xl bg-slate-900 text-white shadow-sm transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {sending ? <Loader2 className=\"h-5 w-5 animate-spin\" /> : <Send className=\"h-5 w-5\" />}\n </button>\n </div>\n </div>\n )\n}\n","// A small inline emoji grid (no external dep). mate uses a full EmojiPicker; this\n// covers the common set for the chat input.\nconst EMOJIS = [\n '😊', '😂', '❤️', '👍', '🙏', '😍', '🎉', '😢', '😮', '🔥', '✨', '😅', '🤔', '👏', '🥰', '😎',\n '😭', '🙄', '💪', '🌟', '😴', '🤗', '😜', '🤩', '👀', '👋', '💯', '🙌', '😇', '🤝', '🫶', '😆',\n] as const\n\nexport function EmojiPicker({ onSelect }: { onSelect: (emoji: string) => void }) {\n return (\n <div className=\"absolute bottom-9 left-0 z-20 grid w-64 grid-cols-8 gap-0.5 rounded-xl border border-gray-200 bg-white p-2 shadow-lg\">\n {EMOJIS.map((e, i) => (\n <button\n key={`${e}-${i}`}\n type=\"button\"\n onClick={() => onSelect(e)}\n className=\"rounded p-1 text-lg leading-none transition hover:bg-gray-100\"\n >\n {e}\n </button>\n ))}\n </div>\n )\n}\n","import { Mic } from 'lucide-react'\n\n// Full-screen purple recording overlay with a pulsing mic + duration + the live\n// transcript (mirrors mate's VoiceRecordingOverlay).\nfunction fmt(seconds: number): string {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n}\n\ntype Props = {\n readonly recording: boolean\n readonly seconds: number\n readonly transcript?: string | undefined\n readonly onStop: () => void\n}\n\nexport function VoiceRecordingOverlay({ recording, seconds, transcript = '', onStop }: Props) {\n if (!recording) return null\n return (\n <div\n onClick={onStop}\n className=\"fixed inset-0 z-[60] flex flex-col items-center justify-center bg-gradient-to-br from-purple-600/95 to-purple-800/95 backdrop-blur-sm\"\n >\n <div className=\"flex flex-col items-center gap-8\">\n <div className=\"relative\">\n <div className=\"absolute inset-0 animate-ping\">\n <div className=\"mx-auto h-32 w-32 rounded-full bg-white/20\" />\n </div>\n <div className=\"absolute inset-0 animate-pulse\">\n <div className=\"mx-auto h-32 w-32 rounded-full bg-white/30\" />\n </div>\n <div className=\"relative flex h-32 w-32 items-center justify-center rounded-full bg-white/40\">\n <Mic className=\"h-16 w-16 text-white\" />\n </div>\n </div>\n <div className=\"text-center text-white\">\n <h2 className=\"mb-2 text-3xl font-semibold\">录音中…</h2>\n <p className=\"font-mono text-xl text-white/90\">{fmt(seconds)}</p>\n <p className=\"mt-4 text-sm text-white/70\">对着麦克风清晰地说话,点任意处停止</p>\n </div>\n {transcript ? (\n <div className=\"mt-2 w-full max-w-2xl px-6\">\n <div className=\"rounded-2xl border border-white/20 bg-white/10 p-6 backdrop-blur-md\">\n <p className=\"mb-2 text-xs uppercase tracking-wider text-white/60\">你说的:</p>\n <p className=\"whitespace-pre-wrap text-lg leading-relaxed text-white\">{transcript}</p>\n </div>\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { Play, Pause } from 'lucide-react'\n\n// A playable voice-message row: play/pause + progress bar + \"m:ss / m:ss\"\n// (mirrors mate's VoiceMessageBubble). Rendered inside a chat bubble above the\n// transcript text.\nfunction fmt(seconds: number): string {\n const m = Math.floor(seconds / 60)\n const s = Math.floor(seconds % 60)\n return `${m}:${String(s).padStart(2, '0')}`\n}\n\ntype Props = {\n readonly audioUrl: string\n readonly duration: number\n readonly isUser?: boolean\n}\n\nexport function VoiceMessageBubble({ audioUrl, duration, isUser = false }: Props) {\n const [playing, setPlaying] = useState(false)\n const [current, setCurrent] = useState(0)\n const audioRef = useRef<HTMLAudioElement | null>(null)\n\n useEffect(() => {\n const audio = new Audio(audioUrl)\n audioRef.current = audio\n const onTime = () => setCurrent(audio.currentTime)\n const onEnd = () => {\n setPlaying(false)\n setCurrent(0)\n }\n audio.addEventListener('timeupdate', onTime)\n audio.addEventListener('ended', onEnd)\n return () => {\n audio.pause()\n audio.removeEventListener('timeupdate', onTime)\n audio.removeEventListener('ended', onEnd)\n audio.src = ''\n }\n }, [audioUrl])\n\n const toggle = () => {\n const audio = audioRef.current\n if (!audio) return\n if (playing) audio.pause()\n else void audio.play()\n setPlaying(!playing)\n }\n\n const progress = duration > 0 ? Math.min(100, (current / duration) * 100) : 0\n\n return (\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={toggle}\n className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-full transition hover:scale-105 ${\n isUser ? 'bg-gray-600/20 hover:bg-gray-600/30' : 'bg-purple-100 hover:bg-purple-200'\n }`}\n >\n {playing ? (\n <Pause className={`h-5 w-5 ${isUser ? 'text-gray-700' : 'text-purple-600'}`} />\n ) : (\n <Play className={`h-5 w-5 ${isUser ? 'text-gray-700' : 'text-purple-600'}`} />\n )}\n </button>\n <div className=\"flex flex-1 flex-col gap-1\">\n <div className=\"relative h-1 overflow-hidden rounded-full bg-gray-300\">\n <div\n className={`absolute left-0 top-0 h-full transition-all ${isUser ? 'bg-gray-600' : 'bg-purple-500'}`}\n style={{ width: `${progress}%` }}\n />\n </div>\n <div className={`text-xs ${isUser ? 'text-gray-600' : 'text-gray-500'}`}>\n {fmt(current)} / {fmt(duration)}\n </div>\n </div>\n </div>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\n\n// Voice input via the browser SpeechRecognition API (Chrome/Edge). The mic\n// transcribes speech to text live; the caller drops the transcript into the chat\n// input (mirrors mate's speech-to-text mic). No backend, no key.\n\n// The Web Speech API isn't in the standard TS DOM lib — type it loosely.\ntype SpeechRecognitionLike = {\n lang: string\n continuous: boolean\n interimResults: boolean\n onresult: ((e: { results: ArrayLike<ArrayLike<{ transcript: string }>> }) => void) | null\n onend: (() => void) | null\n onerror: (() => void) | null\n start(): void\n stop(): void\n}\n\nfunction getCtor(): (new () => SpeechRecognitionLike) | null {\n if (typeof window === 'undefined') return null\n const w = window as unknown as Record<string, unknown>\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as (new () => SpeechRecognitionLike) | null\n}\n\nexport function useSpeechRecognition(onTranscript: (text: string) => void, lang = 'zh-CN') {\n const [listening, setListening] = useState(false)\n const [seconds, setSeconds] = useState(0)\n const recRef = useRef<SpeechRecognitionLike | null>(null)\n const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const supported = getCtor() !== null\n\n const clearTimer = () => {\n if (timerRef.current) {\n clearInterval(timerRef.current)\n timerRef.current = null\n }\n }\n\n const stop = () => {\n recRef.current?.stop()\n recRef.current = null\n clearTimer()\n setListening(false)\n setSeconds(0)\n }\n\n const start = () => {\n const Ctor = getCtor()\n if (!Ctor) {\n window.alert('当前浏览器不支持语音输入(请用 Chrome / Edge)。')\n return\n }\n const rec = new Ctor()\n rec.lang = lang\n rec.continuous = true\n rec.interimResults = true\n rec.onresult = (e) => {\n let text = ''\n for (let i = 0; i < e.results.length; i++) {\n const alt = e.results[i]?.[0]\n if (alt) text += alt.transcript\n }\n onTranscript(text)\n }\n rec.onend = () => {\n clearTimer()\n setListening(false)\n setSeconds(0)\n recRef.current = null\n }\n rec.onerror = () => {\n clearTimer()\n setListening(false)\n setSeconds(0)\n recRef.current = null\n }\n rec.start()\n recRef.current = rec\n setSeconds(0)\n setListening(true)\n timerRef.current = setInterval(() => setSeconds((s) => s + 1), 1000)\n }\n\n // Clean up on unmount.\n useEffect(() => () => stop(), [])\n\n return { supported, listening, seconds, start, stop }\n}\n","import { useEffect, useRef, useState } from 'react'\n\n// Records mic audio via MediaRecorder → a blob: URL + duration (mirrors mate's\n// useAudioRecorder). Runs alongside speech recognition so a voice message\n// carries both the audio and its transcript.\n\nexport type RecordedAudio = { url: string; duration: number }\n\nexport function useAudioRecorder() {\n const [recording, setRecording] = useState(false)\n const [duration, setDuration] = useState(0)\n const recRef = useRef<MediaRecorder | null>(null)\n const chunksRef = useRef<Blob[]>([])\n const startedRef = useRef(0)\n const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const resolveRef = useRef<((a: RecordedAudio | null) => void) | null>(null)\n\n const clearTimer = () => {\n if (timerRef.current) {\n clearInterval(timerRef.current)\n timerRef.current = null\n }\n }\n\n const start = async (): Promise<boolean> => {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) return false\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true })\n const mr = new MediaRecorder(stream)\n chunksRef.current = []\n mr.ondataavailable = (e) => {\n if (e.data.size > 0) chunksRef.current.push(e.data)\n }\n mr.onstop = () => {\n const blob = new Blob(chunksRef.current, { type: mr.mimeType || 'audio/webm' })\n const url = URL.createObjectURL(blob)\n const dur = Math.max(1, Math.round((Date.now() - startedRef.current) / 1000))\n stream.getTracks().forEach((t) => t.stop())\n clearTimer()\n setRecording(false)\n resolveRef.current?.({ url, duration: dur })\n resolveRef.current = null\n }\n mr.start()\n recRef.current = mr\n startedRef.current = Date.now()\n setDuration(0)\n setRecording(true)\n timerRef.current = setInterval(() => setDuration((d) => d + 1), 1000)\n return true\n } catch {\n return false\n }\n }\n\n /** Stop and resolve with the recorded audio (null if not recording). */\n const stop = (): Promise<RecordedAudio | null> =>\n new Promise((resolve) => {\n const mr = recRef.current\n if (!mr || mr.state === 'inactive') {\n resolve(null)\n return\n }\n resolveRef.current = resolve\n mr.stop()\n recRef.current = null\n })\n\n useEffect(\n () => () => {\n recRef.current?.stop()\n clearTimer()\n },\n [],\n )\n\n return { recording, duration, start, stop }\n}\n"],"mappings":"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","names":["useEffect","useRef","useState","Mic","jsx","jsx","jsxs","fmt","useEffect","useRef","useState","useEffect","useRef","useState","jsx","jsxs","useState","useRef","useEffect","Mic"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@surfmate.team/digital-human-conversation",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Text-chat feature: ChatWindow UI + ChatPort (the LLM boundary) + pure prompt-building / reply-parsing (calc). Self-contained; the app injects an LLM adapter. Voice/video chat + persistence are later passes.",
5
5
  "license": "MIT",
6
6
  "type": "module",