@surfmate.team/digital-human-conversation 0.1.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/README.md +13 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +578 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @surfmate.team/digital-human-conversation
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm i @surfmate.team/digital-human-conversation
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Part of the **@surfmate.team digital-human** suite (ports/DI — the package owns its data + UI/logic; the app injects adapters).
|
|
10
|
+
|
|
11
|
+
## License
|
|
12
|
+
|
|
13
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ChatPort } from '@surfmate.team/digital-human-ports';
|
|
2
|
+
export { ChatPort } from '@surfmate.team/digital-human-ports';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
type ChatRole = 'user' | 'assistant';
|
|
6
|
+
type ChatMessage = {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly role: ChatRole;
|
|
9
|
+
readonly content: string;
|
|
10
|
+
/** Epoch ms, set at send time. UI-only (calc ignores it). */
|
|
11
|
+
readonly timestamp?: number | undefined;
|
|
12
|
+
/** Voice message: a playable audio URL + its length in seconds (content = transcript). */
|
|
13
|
+
readonly voiceUrl?: string | undefined;
|
|
14
|
+
readonly voiceDuration?: number | undefined;
|
|
15
|
+
};
|
|
16
|
+
/** The emotional tone the LLM reports for a reply (drives mood-tagged video later). */
|
|
17
|
+
type ChatMood = 'happy' | 'calm' | 'angry' | 'sad' | 'neutral';
|
|
18
|
+
/** The character context that shapes the system prompt + the chat header/avatar. */
|
|
19
|
+
type CharacterPersona = {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly description?: string | undefined;
|
|
22
|
+
readonly personality?: string | undefined;
|
|
23
|
+
readonly greeting?: string | undefined;
|
|
24
|
+
/** Shown as the assistant avatar in the bubbles. */
|
|
25
|
+
readonly avatarUrl?: string | undefined;
|
|
26
|
+
/** Pre-generated greeting video (baked audio). The video call plays it on open. */
|
|
27
|
+
readonly greetingVideoUrl?: string | undefined;
|
|
28
|
+
};
|
|
29
|
+
/** A parsed assistant reply — text + the mood the model chose. */
|
|
30
|
+
type ChatReply = {
|
|
31
|
+
readonly text: string;
|
|
32
|
+
readonly mood: ChatMood;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** How many trailing messages of history to include for context. */
|
|
36
|
+
declare const HISTORY_WINDOW = 5;
|
|
37
|
+
declare function buildPrompt(persona: CharacterPersona, history: readonly ChatMessage[], userMessage: string): string;
|
|
38
|
+
|
|
39
|
+
declare function parseReply(raw: string): ChatReply;
|
|
40
|
+
|
|
41
|
+
type Props = {
|
|
42
|
+
/** The character to chat with — shapes the system prompt + the header/avatar. */
|
|
43
|
+
readonly persona: CharacterPersona;
|
|
44
|
+
/** Injected LLM backend. Without it, the input is disabled. */
|
|
45
|
+
readonly port?: ChatPort | undefined;
|
|
46
|
+
/** Fired after each assistant reply (e.g. to drive mood-tagged video later). */
|
|
47
|
+
readonly onReply?: ((reply: ChatReply) => void) | undefined;
|
|
48
|
+
/** Read an assistant message aloud (e.g. the voice synth). No prop → no speaker / auto-play. */
|
|
49
|
+
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
|
+
/** Avatar for the user's own bubbles. */
|
|
55
|
+
readonly userAvatarUrl?: string | undefined;
|
|
56
|
+
/** Speech-to-text language for the mic. */
|
|
57
|
+
readonly speechLang?: string | undefined;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Full-screen text chat — a faithful clone of mate's ChatPage: a gradient header
|
|
61
|
+
* (back · avatar with online dot · name + personality · auto-play-voice toggle ·
|
|
62
|
+
* clear), avatar/bubble message rows with timestamps + per-assistant read-aloud,
|
|
63
|
+
* the purple typing dots, and an emoji + mic (speech-to-text) input toolbar.
|
|
64
|
+
* Self-contained: owns the conversation, seeds the greeting, builds the prompt
|
|
65
|
+
* (pure) → ChatPort → parses (pure). In-memory (no persistence yet).
|
|
66
|
+
*/
|
|
67
|
+
declare function ChatWindow({ persona, port, onReply, onSpeak, onClose, onCall, userAvatarUrl, speechLang }: Props): react_jsx_runtime.JSX.Element;
|
|
68
|
+
|
|
69
|
+
export { type CharacterPersona, type ChatMessage, type ChatMood, type ChatReply, type ChatRole, ChatWindow, HISTORY_WINDOW, buildPrompt, parseReply };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
// src/calc/buildPrompt.ts
|
|
2
|
+
var HISTORY_WINDOW = 5;
|
|
3
|
+
function buildPrompt(persona, history, userMessage) {
|
|
4
|
+
const historyContext = history.slice(-HISTORY_WINDOW).map((m) => `${m.role === "user" ? "User" : persona.name}: ${m.content}`).join("\n");
|
|
5
|
+
return `You are ${persona.name}.
|
|
6
|
+
|
|
7
|
+
Character background: ${persona.description ?? ""}
|
|
8
|
+
|
|
9
|
+
Personality traits: ${persona.personality ?? ""}
|
|
10
|
+
|
|
11
|
+
Your greeting message: "${persona.greeting ?? ""}"
|
|
12
|
+
|
|
13
|
+
Conversation history:
|
|
14
|
+
${historyContext}
|
|
15
|
+
|
|
16
|
+
User: ${userMessage}
|
|
17
|
+
|
|
18
|
+
Respond 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.
|
|
19
|
+
|
|
20
|
+
IMPORTANT: Respond in JSON format with your reply text and current mood:
|
|
21
|
+
{"text": "your response here", "mood": "happy|calm|angry|sad"}
|
|
22
|
+
|
|
23
|
+
Choose the mood that best matches the emotional tone of your response. Only use: happy, calm, angry, or sad.`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/calc/parseReply.ts
|
|
27
|
+
var VALID_MOODS = ["happy", "calm", "angry", "sad"];
|
|
28
|
+
function parseReply(raw) {
|
|
29
|
+
const match = raw.match(/\{[\s\S]*"text"[\s\S]*\}/);
|
|
30
|
+
if (match) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(match[0]);
|
|
33
|
+
const text = typeof parsed.text === "string" && parsed.text ? parsed.text : raw;
|
|
34
|
+
const mood = VALID_MOODS.includes(parsed.mood) ? parsed.mood : "neutral";
|
|
35
|
+
return { text, mood };
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { text: raw, mood: "neutral" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/ui/ChatWindow.tsx
|
|
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";
|
|
45
|
+
|
|
46
|
+
// src/ui/EmojiPicker.tsx
|
|
47
|
+
import { jsx } from "react/jsx-runtime";
|
|
48
|
+
var EMOJIS = [
|
|
49
|
+
"\u{1F60A}",
|
|
50
|
+
"\u{1F602}",
|
|
51
|
+
"\u2764\uFE0F",
|
|
52
|
+
"\u{1F44D}",
|
|
53
|
+
"\u{1F64F}",
|
|
54
|
+
"\u{1F60D}",
|
|
55
|
+
"\u{1F389}",
|
|
56
|
+
"\u{1F622}",
|
|
57
|
+
"\u{1F62E}",
|
|
58
|
+
"\u{1F525}",
|
|
59
|
+
"\u2728",
|
|
60
|
+
"\u{1F605}",
|
|
61
|
+
"\u{1F914}",
|
|
62
|
+
"\u{1F44F}",
|
|
63
|
+
"\u{1F970}",
|
|
64
|
+
"\u{1F60E}",
|
|
65
|
+
"\u{1F62D}",
|
|
66
|
+
"\u{1F644}",
|
|
67
|
+
"\u{1F4AA}",
|
|
68
|
+
"\u{1F31F}",
|
|
69
|
+
"\u{1F634}",
|
|
70
|
+
"\u{1F917}",
|
|
71
|
+
"\u{1F61C}",
|
|
72
|
+
"\u{1F929}",
|
|
73
|
+
"\u{1F440}",
|
|
74
|
+
"\u{1F44B}",
|
|
75
|
+
"\u{1F4AF}",
|
|
76
|
+
"\u{1F64C}",
|
|
77
|
+
"\u{1F607}",
|
|
78
|
+
"\u{1F91D}",
|
|
79
|
+
"\u{1FAF6}",
|
|
80
|
+
"\u{1F606}"
|
|
81
|
+
];
|
|
82
|
+
function EmojiPicker({ onSelect }) {
|
|
83
|
+
return /* @__PURE__ */ jsx("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", children: EMOJIS.map((e, i) => /* @__PURE__ */ jsx(
|
|
84
|
+
"button",
|
|
85
|
+
{
|
|
86
|
+
type: "button",
|
|
87
|
+
onClick: () => onSelect(e),
|
|
88
|
+
className: "rounded p-1 text-lg leading-none transition hover:bg-gray-100",
|
|
89
|
+
children: e
|
|
90
|
+
},
|
|
91
|
+
`${e}-${i}`
|
|
92
|
+
)) });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/ui/VoiceRecordingOverlay.tsx
|
|
96
|
+
import { Mic } from "lucide-react";
|
|
97
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
98
|
+
function fmt(seconds) {
|
|
99
|
+
const m = Math.floor(seconds / 60);
|
|
100
|
+
const s = seconds % 60;
|
|
101
|
+
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
102
|
+
}
|
|
103
|
+
function VoiceRecordingOverlay({ recording, seconds, transcript = "", onStop }) {
|
|
104
|
+
if (!recording) return null;
|
|
105
|
+
return /* @__PURE__ */ jsx2(
|
|
106
|
+
"div",
|
|
107
|
+
{
|
|
108
|
+
onClick: onStop,
|
|
109
|
+
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",
|
|
110
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-8", children: [
|
|
111
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
112
|
+
/* @__PURE__ */ jsx2("div", { className: "absolute inset-0 animate-ping", children: /* @__PURE__ */ jsx2("div", { className: "mx-auto h-32 w-32 rounded-full bg-white/20" }) }),
|
|
113
|
+
/* @__PURE__ */ jsx2("div", { className: "absolute inset-0 animate-pulse", children: /* @__PURE__ */ jsx2("div", { className: "mx-auto h-32 w-32 rounded-full bg-white/30" }) }),
|
|
114
|
+
/* @__PURE__ */ jsx2("div", { className: "relative flex h-32 w-32 items-center justify-center rounded-full bg-white/40", children: /* @__PURE__ */ jsx2(Mic, { className: "h-16 w-16 text-white" }) })
|
|
115
|
+
] }),
|
|
116
|
+
/* @__PURE__ */ jsxs("div", { className: "text-center text-white", children: [
|
|
117
|
+
/* @__PURE__ */ jsx2("h2", { className: "mb-2 text-3xl font-semibold", children: "\u5F55\u97F3\u4E2D\u2026" }),
|
|
118
|
+
/* @__PURE__ */ jsx2("p", { className: "font-mono text-xl text-white/90", children: fmt(seconds) }),
|
|
119
|
+
/* @__PURE__ */ jsx2("p", { className: "mt-4 text-sm text-white/70", children: "\u5BF9\u7740\u9EA6\u514B\u98CE\u6E05\u6670\u5730\u8BF4\u8BDD,\u70B9\u4EFB\u610F\u5904\u505C\u6B62" })
|
|
120
|
+
] }),
|
|
121
|
+
transcript ? /* @__PURE__ */ jsx2("div", { className: "mt-2 w-full max-w-2xl px-6", children: /* @__PURE__ */ jsxs("div", { className: "rounded-2xl border border-white/20 bg-white/10 p-6 backdrop-blur-md", children: [
|
|
122
|
+
/* @__PURE__ */ jsx2("p", { className: "mb-2 text-xs uppercase tracking-wider text-white/60", children: "\u4F60\u8BF4\u7684:" }),
|
|
123
|
+
/* @__PURE__ */ jsx2("p", { className: "whitespace-pre-wrap text-lg leading-relaxed text-white", children: transcript })
|
|
124
|
+
] }) }) : null
|
|
125
|
+
] })
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/ui/VoiceMessageBubble.tsx
|
|
131
|
+
import { useEffect, useRef, useState } from "react";
|
|
132
|
+
import { Play, Pause } from "lucide-react";
|
|
133
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
134
|
+
function fmt2(seconds) {
|
|
135
|
+
const m = Math.floor(seconds / 60);
|
|
136
|
+
const s = Math.floor(seconds % 60);
|
|
137
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
138
|
+
}
|
|
139
|
+
function VoiceMessageBubble({ audioUrl, duration, isUser = false }) {
|
|
140
|
+
const [playing, setPlaying] = useState(false);
|
|
141
|
+
const [current, setCurrent] = useState(0);
|
|
142
|
+
const audioRef = useRef(null);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const audio = new Audio(audioUrl);
|
|
145
|
+
audioRef.current = audio;
|
|
146
|
+
const onTime = () => setCurrent(audio.currentTime);
|
|
147
|
+
const onEnd = () => {
|
|
148
|
+
setPlaying(false);
|
|
149
|
+
setCurrent(0);
|
|
150
|
+
};
|
|
151
|
+
audio.addEventListener("timeupdate", onTime);
|
|
152
|
+
audio.addEventListener("ended", onEnd);
|
|
153
|
+
return () => {
|
|
154
|
+
audio.pause();
|
|
155
|
+
audio.removeEventListener("timeupdate", onTime);
|
|
156
|
+
audio.removeEventListener("ended", onEnd);
|
|
157
|
+
audio.src = "";
|
|
158
|
+
};
|
|
159
|
+
}, [audioUrl]);
|
|
160
|
+
const toggle = () => {
|
|
161
|
+
const audio = audioRef.current;
|
|
162
|
+
if (!audio) return;
|
|
163
|
+
if (playing) audio.pause();
|
|
164
|
+
else void audio.play();
|
|
165
|
+
setPlaying(!playing);
|
|
166
|
+
};
|
|
167
|
+
const progress = duration > 0 ? Math.min(100, current / duration * 100) : 0;
|
|
168
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-3", children: [
|
|
169
|
+
/* @__PURE__ */ jsx3(
|
|
170
|
+
"button",
|
|
171
|
+
{
|
|
172
|
+
type: "button",
|
|
173
|
+
onClick: toggle,
|
|
174
|
+
className: `flex h-10 w-10 shrink-0 items-center justify-center rounded-full transition hover:scale-105 ${isUser ? "bg-gray-600/20 hover:bg-gray-600/30" : "bg-purple-100 hover:bg-purple-200"}`,
|
|
175
|
+
children: playing ? /* @__PURE__ */ jsx3(Pause, { className: `h-5 w-5 ${isUser ? "text-gray-700" : "text-purple-600"}` }) : /* @__PURE__ */ jsx3(Play, { className: `h-5 w-5 ${isUser ? "text-gray-700" : "text-purple-600"}` })
|
|
176
|
+
}
|
|
177
|
+
),
|
|
178
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex flex-1 flex-col gap-1", children: [
|
|
179
|
+
/* @__PURE__ */ jsx3("div", { className: "relative h-1 overflow-hidden rounded-full bg-gray-300", children: /* @__PURE__ */ jsx3(
|
|
180
|
+
"div",
|
|
181
|
+
{
|
|
182
|
+
className: `absolute left-0 top-0 h-full transition-all ${isUser ? "bg-gray-600" : "bg-purple-500"}`,
|
|
183
|
+
style: { width: `${progress}%` }
|
|
184
|
+
}
|
|
185
|
+
) }),
|
|
186
|
+
/* @__PURE__ */ jsxs2("div", { className: `text-xs ${isUser ? "text-gray-600" : "text-gray-500"}`, children: [
|
|
187
|
+
fmt2(current),
|
|
188
|
+
" / ",
|
|
189
|
+
fmt2(duration)
|
|
190
|
+
] })
|
|
191
|
+
] })
|
|
192
|
+
] });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/ui/useSpeechRecognition.ts
|
|
196
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
197
|
+
function getCtor() {
|
|
198
|
+
if (typeof window === "undefined") return null;
|
|
199
|
+
const w = window;
|
|
200
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
201
|
+
}
|
|
202
|
+
function useSpeechRecognition(onTranscript, lang = "zh-CN") {
|
|
203
|
+
const [listening, setListening] = useState2(false);
|
|
204
|
+
const [seconds, setSeconds] = useState2(0);
|
|
205
|
+
const recRef = useRef2(null);
|
|
206
|
+
const timerRef = useRef2(null);
|
|
207
|
+
const supported = getCtor() !== null;
|
|
208
|
+
const clearTimer = () => {
|
|
209
|
+
if (timerRef.current) {
|
|
210
|
+
clearInterval(timerRef.current);
|
|
211
|
+
timerRef.current = null;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const stop = () => {
|
|
215
|
+
recRef.current?.stop();
|
|
216
|
+
recRef.current = null;
|
|
217
|
+
clearTimer();
|
|
218
|
+
setListening(false);
|
|
219
|
+
setSeconds(0);
|
|
220
|
+
};
|
|
221
|
+
const start = () => {
|
|
222
|
+
const Ctor = getCtor();
|
|
223
|
+
if (!Ctor) {
|
|
224
|
+
window.alert("\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u8BED\u97F3\u8F93\u5165(\u8BF7\u7528 Chrome / Edge)\u3002");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const rec = new Ctor();
|
|
228
|
+
rec.lang = lang;
|
|
229
|
+
rec.continuous = true;
|
|
230
|
+
rec.interimResults = true;
|
|
231
|
+
rec.onresult = (e) => {
|
|
232
|
+
let text = "";
|
|
233
|
+
for (let i = 0; i < e.results.length; i++) {
|
|
234
|
+
const alt = e.results[i]?.[0];
|
|
235
|
+
if (alt) text += alt.transcript;
|
|
236
|
+
}
|
|
237
|
+
onTranscript(text);
|
|
238
|
+
};
|
|
239
|
+
rec.onend = () => {
|
|
240
|
+
clearTimer();
|
|
241
|
+
setListening(false);
|
|
242
|
+
setSeconds(0);
|
|
243
|
+
recRef.current = null;
|
|
244
|
+
};
|
|
245
|
+
rec.onerror = () => {
|
|
246
|
+
clearTimer();
|
|
247
|
+
setListening(false);
|
|
248
|
+
setSeconds(0);
|
|
249
|
+
recRef.current = null;
|
|
250
|
+
};
|
|
251
|
+
rec.start();
|
|
252
|
+
recRef.current = rec;
|
|
253
|
+
setSeconds(0);
|
|
254
|
+
setListening(true);
|
|
255
|
+
timerRef.current = setInterval(() => setSeconds((s) => s + 1), 1e3);
|
|
256
|
+
};
|
|
257
|
+
useEffect2(() => () => stop(), []);
|
|
258
|
+
return { supported, listening, seconds, start, stop };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/ui/useAudioRecorder.ts
|
|
262
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
263
|
+
function useAudioRecorder() {
|
|
264
|
+
const [recording, setRecording] = useState3(false);
|
|
265
|
+
const [duration, setDuration] = useState3(0);
|
|
266
|
+
const recRef = useRef3(null);
|
|
267
|
+
const chunksRef = useRef3([]);
|
|
268
|
+
const startedRef = useRef3(0);
|
|
269
|
+
const timerRef = useRef3(null);
|
|
270
|
+
const resolveRef = useRef3(null);
|
|
271
|
+
const clearTimer = () => {
|
|
272
|
+
if (timerRef.current) {
|
|
273
|
+
clearInterval(timerRef.current);
|
|
274
|
+
timerRef.current = null;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const start = async () => {
|
|
278
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) return false;
|
|
279
|
+
try {
|
|
280
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
281
|
+
const mr = new MediaRecorder(stream);
|
|
282
|
+
chunksRef.current = [];
|
|
283
|
+
mr.ondataavailable = (e) => {
|
|
284
|
+
if (e.data.size > 0) chunksRef.current.push(e.data);
|
|
285
|
+
};
|
|
286
|
+
mr.onstop = () => {
|
|
287
|
+
const blob = new Blob(chunksRef.current, { type: mr.mimeType || "audio/webm" });
|
|
288
|
+
const url = URL.createObjectURL(blob);
|
|
289
|
+
const dur = Math.max(1, Math.round((Date.now() - startedRef.current) / 1e3));
|
|
290
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
291
|
+
clearTimer();
|
|
292
|
+
setRecording(false);
|
|
293
|
+
resolveRef.current?.({ url, duration: dur });
|
|
294
|
+
resolveRef.current = null;
|
|
295
|
+
};
|
|
296
|
+
mr.start();
|
|
297
|
+
recRef.current = mr;
|
|
298
|
+
startedRef.current = Date.now();
|
|
299
|
+
setDuration(0);
|
|
300
|
+
setRecording(true);
|
|
301
|
+
timerRef.current = setInterval(() => setDuration((d) => d + 1), 1e3);
|
|
302
|
+
return true;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const stop = () => new Promise((resolve) => {
|
|
308
|
+
const mr = recRef.current;
|
|
309
|
+
if (!mr || mr.state === "inactive") {
|
|
310
|
+
resolve(null);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
resolveRef.current = resolve;
|
|
314
|
+
mr.stop();
|
|
315
|
+
recRef.current = null;
|
|
316
|
+
});
|
|
317
|
+
useEffect3(
|
|
318
|
+
() => () => {
|
|
319
|
+
recRef.current?.stop();
|
|
320
|
+
clearTimer();
|
|
321
|
+
},
|
|
322
|
+
[]
|
|
323
|
+
);
|
|
324
|
+
return { recording, duration, start, stop };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/ui/ChatWindow.tsx
|
|
328
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
329
|
+
var GREETING_ID = "greeting";
|
|
330
|
+
var DEFAULT_USER_AVATAR = "https://api.dicebear.com/7.x/avataaars/svg?seed=user";
|
|
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 }) {
|
|
333
|
+
const [messages, setMessages] = useState4(() => seedGreeting(persona));
|
|
334
|
+
const [input, setInput] = useState4("");
|
|
335
|
+
const [sending, setSending] = useState4(false);
|
|
336
|
+
const [speakingId, setSpeakingId] = useState4(null);
|
|
337
|
+
const [autoPlay, setAutoPlay] = useState4(false);
|
|
338
|
+
const [showEmoji, setShowEmoji] = useState4(false);
|
|
339
|
+
const [pendingVoice, setPendingVoice] = useState4(null);
|
|
340
|
+
const endRef = useRef4(null);
|
|
341
|
+
const speech = useSpeechRecognition(setInput, speechLang);
|
|
342
|
+
const recorder = useAudioRecorder();
|
|
343
|
+
const micActive = recorder.recording || speech.listening;
|
|
344
|
+
const toggleMic = async () => {
|
|
345
|
+
if (micActive) {
|
|
346
|
+
speech.stop();
|
|
347
|
+
const v = await recorder.stop();
|
|
348
|
+
if (v) setPendingVoice(v);
|
|
349
|
+
} else {
|
|
350
|
+
setPendingVoice(null);
|
|
351
|
+
void recorder.start();
|
|
352
|
+
speech.start();
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
useEffect4(() => {
|
|
356
|
+
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
357
|
+
}, [messages.length, sending]);
|
|
358
|
+
const speak = async (id, text) => {
|
|
359
|
+
if (!onSpeak) return;
|
|
360
|
+
setSpeakingId(id);
|
|
361
|
+
try {
|
|
362
|
+
await onSpeak(text);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.warn("[chat] speak failed:", err);
|
|
365
|
+
} finally {
|
|
366
|
+
setSpeakingId(null);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const send = async () => {
|
|
370
|
+
const text = input.trim();
|
|
371
|
+
if (!port || !text || sending) return;
|
|
372
|
+
let voice = pendingVoice;
|
|
373
|
+
if (recorder.recording) {
|
|
374
|
+
speech.stop();
|
|
375
|
+
const v = await recorder.stop();
|
|
376
|
+
if (v) voice = v;
|
|
377
|
+
}
|
|
378
|
+
setPendingVoice(null);
|
|
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
|
+
]);
|
|
390
|
+
setInput("");
|
|
391
|
+
setSending(true);
|
|
392
|
+
try {
|
|
393
|
+
const raw = await port.complete(buildPrompt(persona, history, text));
|
|
394
|
+
const reply = parseReply(raw);
|
|
395
|
+
const id = crypto.randomUUID();
|
|
396
|
+
setMessages((m) => [...m, { id, role: "assistant", content: reply.text, timestamp: Date.now() }]);
|
|
397
|
+
onReply?.(reply);
|
|
398
|
+
if (autoPlay && onSpeak) void speak(id, reply.text);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
401
|
+
setMessages((m) => [...m, { id: crypto.randomUUID(), role: "assistant", content: `(\u51FA\u9519:${msg})`, timestamp: Date.now() }]);
|
|
402
|
+
} finally {
|
|
403
|
+
setSending(false);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const onKeyDown = (e) => {
|
|
407
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
void send();
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
const inputDisabled = !port || sending;
|
|
413
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex h-full min-h-0 flex-col bg-gradient-to-b from-purple-50 to-white", children: [
|
|
414
|
+
/* @__PURE__ */ jsx4(
|
|
415
|
+
VoiceRecordingOverlay,
|
|
416
|
+
{
|
|
417
|
+
recording: micActive,
|
|
418
|
+
seconds: recorder.recording ? recorder.duration : speech.seconds,
|
|
419
|
+
transcript: input,
|
|
420
|
+
onStop: () => void toggleMic()
|
|
421
|
+
}
|
|
422
|
+
),
|
|
423
|
+
/* @__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,
|
|
425
|
+
/* @__PURE__ */ jsxs3("div", { className: "relative shrink-0", children: [
|
|
426
|
+
/* @__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
|
+
/* @__PURE__ */ jsx4("span", { className: "absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 ring-2 ring-white" })
|
|
428
|
+
] }),
|
|
429
|
+
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
430
|
+
/* @__PURE__ */ jsx4("h1", { className: "truncate text-lg font-semibold text-gray-900", children: persona.name }),
|
|
431
|
+
/* @__PURE__ */ jsx4("p", { className: "truncate text-xs text-gray-500", children: persona.personality ?? persona.description ?? "" })
|
|
432
|
+
] }),
|
|
433
|
+
onSpeak ? /* @__PURE__ */ jsxs3(
|
|
434
|
+
"button",
|
|
435
|
+
{
|
|
436
|
+
type: "button",
|
|
437
|
+
onClick: () => setAutoPlay((v) => !v),
|
|
438
|
+
title: "\u81EA\u52A8\u6717\u8BFB\u56DE\u590D",
|
|
439
|
+
className: `flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition ${autoPlay ? "bg-purple-100 text-purple-700" : "bg-gray-100 text-gray-600 hover:bg-gray-200"}`,
|
|
440
|
+
children: [
|
|
441
|
+
autoPlay ? /* @__PURE__ */ jsx4(Volume2, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx4(VolumeX, { className: "h-3.5 w-3.5" }),
|
|
442
|
+
/* @__PURE__ */ jsx4("span", { className: "hidden sm:inline", children: "\u81EA\u52A8\u8BED\u97F3" })
|
|
443
|
+
]
|
|
444
|
+
}
|
|
445
|
+
) : 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
|
+
/* @__PURE__ */ jsx4(
|
|
457
|
+
"button",
|
|
458
|
+
{
|
|
459
|
+
type: "button",
|
|
460
|
+
onClick: () => setMessages(seedGreeting(persona)),
|
|
461
|
+
title: "\u6E05\u7A7A\u5BF9\u8BDD",
|
|
462
|
+
className: "rounded-full p-1.5 text-gray-500 transition hover:bg-red-100 hover:text-red-600",
|
|
463
|
+
children: /* @__PURE__ */ jsx4(Trash2, { className: "h-4 w-4" })
|
|
464
|
+
}
|
|
465
|
+
)
|
|
466
|
+
] }),
|
|
467
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex-1 space-y-4 overflow-y-auto p-3 sm:p-6", children: [
|
|
468
|
+
messages.map((m) => {
|
|
469
|
+
const isUser = m.role === "user";
|
|
470
|
+
return /* @__PURE__ */ jsxs3("div", { className: `flex gap-2.5 ${isUser ? "flex-row-reverse" : "flex-row"}`, children: [
|
|
471
|
+
/* @__PURE__ */ jsx4(
|
|
472
|
+
"img",
|
|
473
|
+
{
|
|
474
|
+
src: isUser ? userAvatarUrl ?? DEFAULT_USER_AVATAR : persona.avatarUrl ?? DEFAULT_USER_AVATAR,
|
|
475
|
+
alt: isUser ? "You" : persona.name,
|
|
476
|
+
className: "h-8 w-8 shrink-0 rounded-full bg-gray-100 object-cover shadow-sm ring-2 ring-white"
|
|
477
|
+
}
|
|
478
|
+
),
|
|
479
|
+
/* @__PURE__ */ jsx4("div", { className: "flex max-w-[75%] flex-col", children: /* @__PURE__ */ jsxs3(
|
|
480
|
+
"div",
|
|
481
|
+
{
|
|
482
|
+
className: `rounded-2xl px-3.5 py-2.5 shadow-sm transition-shadow hover:shadow-md ${isUser ? "bg-gray-100 text-gray-900" : "border border-gray-100 bg-white text-gray-900"}`,
|
|
483
|
+
children: [
|
|
484
|
+
m.voiceUrl && m.voiceDuration ? /* @__PURE__ */ jsx4("div", { className: "mb-2 min-w-[12rem]", children: /* @__PURE__ */ jsx4(VoiceMessageBubble, { audioUrl: m.voiceUrl, duration: m.voiceDuration, isUser }) }) : null,
|
|
485
|
+
/* @__PURE__ */ jsx4("p", { className: "whitespace-pre-wrap break-words text-sm leading-relaxed", children: m.content }),
|
|
486
|
+
/* @__PURE__ */ jsxs3("div", { className: "mt-1 flex items-center gap-2", children: [
|
|
487
|
+
/* @__PURE__ */ jsx4("span", { className: "text-[10px] text-gray-400", children: m.timestamp ? new Date(m.timestamp).toLocaleTimeString() : "" }),
|
|
488
|
+
!isUser && onSpeak ? /* @__PURE__ */ jsx4(
|
|
489
|
+
"button",
|
|
490
|
+
{
|
|
491
|
+
type: "button",
|
|
492
|
+
onClick: () => void speak(m.id, m.content),
|
|
493
|
+
disabled: speakingId !== null && speakingId !== m.id,
|
|
494
|
+
title: "\u6717\u8BFB",
|
|
495
|
+
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",
|
|
496
|
+
children: speakingId === m.id ? /* @__PURE__ */ jsx4(VolumeX, { className: "h-3.5 w-3.5 text-purple-600" }) : /* @__PURE__ */ jsx4(Volume2, { className: "h-3.5 w-3.5" })
|
|
497
|
+
}
|
|
498
|
+
) : null
|
|
499
|
+
] })
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
) })
|
|
503
|
+
] }, m.id);
|
|
504
|
+
}),
|
|
505
|
+
sending ? /* @__PURE__ */ jsx4("div", { className: "flex justify-start", children: /* @__PURE__ */ jsx4("div", { className: "rounded-2xl border border-gray-100 bg-white px-4 py-2.5 shadow-sm", children: /* @__PURE__ */ jsxs3("span", { className: "flex gap-1.5", children: [
|
|
506
|
+
/* @__PURE__ */ jsx4("span", { className: "h-1.5 w-1.5 animate-bounce rounded-full bg-purple-400 [animation-delay:0ms]" }),
|
|
507
|
+
/* @__PURE__ */ jsx4("span", { className: "h-1.5 w-1.5 animate-bounce rounded-full bg-purple-400 [animation-delay:150ms]" }),
|
|
508
|
+
/* @__PURE__ */ jsx4("span", { className: "h-1.5 w-1.5 animate-bounce rounded-full bg-purple-400 [animation-delay:300ms]" })
|
|
509
|
+
] }) }) }) : null,
|
|
510
|
+
/* @__PURE__ */ jsx4("div", { ref: endRef })
|
|
511
|
+
] }),
|
|
512
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-center gap-1 border-t border-gray-100 bg-white px-3 py-1.5", children: [
|
|
513
|
+
/* @__PURE__ */ jsxs3("div", { className: "relative", children: [
|
|
514
|
+
/* @__PURE__ */ jsx4(
|
|
515
|
+
"button",
|
|
516
|
+
{
|
|
517
|
+
type: "button",
|
|
518
|
+
onClick: () => setShowEmoji((v) => !v),
|
|
519
|
+
className: "rounded-lg p-1.5 text-gray-500 transition hover:bg-purple-50 hover:text-purple-600",
|
|
520
|
+
title: "\u8868\u60C5",
|
|
521
|
+
children: /* @__PURE__ */ jsx4(Smile, { className: "h-5 w-5" })
|
|
522
|
+
}
|
|
523
|
+
),
|
|
524
|
+
showEmoji ? /* @__PURE__ */ jsx4(
|
|
525
|
+
EmojiPicker,
|
|
526
|
+
{
|
|
527
|
+
onSelect: (e) => {
|
|
528
|
+
setInput((v) => v + e);
|
|
529
|
+
setShowEmoji(false);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
) : null
|
|
533
|
+
] }),
|
|
534
|
+
/* @__PURE__ */ jsx4(
|
|
535
|
+
"button",
|
|
536
|
+
{
|
|
537
|
+
type: "button",
|
|
538
|
+
onClick: () => void toggleMic(),
|
|
539
|
+
disabled: inputDisabled,
|
|
540
|
+
className: `rounded-lg p-1.5 transition ${micActive ? "animate-pulse bg-red-50 text-red-600" : "text-gray-500 hover:bg-purple-50 hover:text-purple-600"} disabled:cursor-not-allowed disabled:opacity-40`,
|
|
541
|
+
title: micActive ? "\u505C\u6B62\u5F55\u97F3" : "\u8BED\u97F3\u6D88\u606F(\u5F55\u97F3 + \u8F6C\u5199)",
|
|
542
|
+
children: /* @__PURE__ */ jsx4(Mic2, { className: "h-5 w-5" })
|
|
543
|
+
}
|
|
544
|
+
)
|
|
545
|
+
] }),
|
|
546
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-end gap-2 bg-white px-3 pb-3 pt-1", children: [
|
|
547
|
+
/* @__PURE__ */ jsx4("div", { className: "flex-1 rounded-2xl border border-gray-200 bg-gray-50 p-2.5", children: /* @__PURE__ */ jsx4(
|
|
548
|
+
"textarea",
|
|
549
|
+
{
|
|
550
|
+
rows: 1,
|
|
551
|
+
value: input,
|
|
552
|
+
disabled: inputDisabled,
|
|
553
|
+
onChange: (e) => setInput(e.target.value),
|
|
554
|
+
onKeyDown,
|
|
555
|
+
placeholder: port ? `\u7ED9 ${persona.name} \u53D1\u6D88\u606F\u2026(Enter \u53D1\u9001)` : "\u9700\u6CE8\u5165 ChatPort \u624D\u80FD\u5BF9\u8BDD",
|
|
556
|
+
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"
|
|
557
|
+
}
|
|
558
|
+
) }),
|
|
559
|
+
/* @__PURE__ */ jsx4(
|
|
560
|
+
"button",
|
|
561
|
+
{
|
|
562
|
+
type: "button",
|
|
563
|
+
onClick: () => void send(),
|
|
564
|
+
disabled: inputDisabled || input.trim().length === 0,
|
|
565
|
+
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",
|
|
566
|
+
children: sending ? /* @__PURE__ */ jsx4(Loader2, { className: "h-5 w-5 animate-spin" }) : /* @__PURE__ */ jsx4(Send, { className: "h-5 w-5" })
|
|
567
|
+
}
|
|
568
|
+
)
|
|
569
|
+
] })
|
|
570
|
+
] });
|
|
571
|
+
}
|
|
572
|
+
export {
|
|
573
|
+
ChatWindow,
|
|
574
|
+
HISTORY_WINDOW,
|
|
575
|
+
buildPrompt,
|
|
576
|
+
parseReply
|
|
577
|
+
};
|
|
578
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@surfmate.team/digital-human-conversation",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"lucide-react": "^0.460.0",
|
|
24
|
+
"@surfmate.team/digital-human-ports": "0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "^18.3.0",
|
|
28
|
+
"react": "^18.3.1",
|
|
29
|
+
"tsup": "^8.5.0",
|
|
30
|
+
"typescript": "^5.9.0",
|
|
31
|
+
"vitest": "^3.2.0"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run"
|
|
40
|
+
}
|
|
41
|
+
}
|