@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 +12 -5
- package/dist/index.js +33 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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.
|
|
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",
|