@yh-ui/hooks 0.1.10 → 0.1.12

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.
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useAiChat = useAiChat;
7
+ var _vue = require("vue");
8
+ var _useAiStream = require("./use-ai-stream.cjs");
9
+ function createTypewriter(onChar, charsPerFrame) {
10
+ const queue = [];
11
+ let rafId = null;
12
+ const schedule = () => {
13
+ rafId = requestAnimationFrame(() => {
14
+ rafId = null;
15
+ if (queue.length === 0) return;
16
+ const batch = queue.splice(0, charsPerFrame).join("");
17
+ onChar(batch);
18
+ if (queue.length > 0) schedule();
19
+ });
20
+ };
21
+ return {
22
+ push(text) {
23
+ queue.push(...text.split(""));
24
+ if (rafId === null) schedule();
25
+ },
26
+ flush() {
27
+ if (rafId !== null) {
28
+ cancelAnimationFrame(rafId);
29
+ rafId = null;
30
+ }
31
+ if (queue.length > 0) {
32
+ onChar(queue.splice(0).join(""));
33
+ }
34
+ },
35
+ cancel() {
36
+ if (rafId !== null) {
37
+ cancelAnimationFrame(rafId);
38
+ rafId = null;
39
+ }
40
+ queue.length = 0;
41
+ }
42
+ };
43
+ }
44
+ function useAiChat(options = {}) {
45
+ const {
46
+ idGenerator = () => Math.random().toString(36).substring(2, 9),
47
+ parser = _useAiStream.plainTextParser,
48
+ typewriter: enableTypewriter = true,
49
+ charsPerFrame = 3,
50
+ systemPrompt
51
+ } = options;
52
+ const messages = (0, _vue.ref)(options.initialMessages ?? []);
53
+ const isGenerating = (0, _vue.ref)(false);
54
+ const isSending = (0, _vue.computed)(() => isGenerating.value);
55
+ let abortController = null;
56
+ const stop = () => {
57
+ if (abortController && isGenerating.value) {
58
+ abortController.abort();
59
+ isGenerating.value = false;
60
+ const lastMsg = messages.value[messages.value.length - 1];
61
+ if (lastMsg?.role === "assistant" && lastMsg.status === "generating") {
62
+ lastMsg.status = "stopped";
63
+ }
64
+ }
65
+ };
66
+ const clear = () => {
67
+ stop();
68
+ messages.value = [];
69
+ };
70
+ const removeMessage = id => {
71
+ const idx = messages.value.findIndex(m => m.id === id);
72
+ if (idx !== -1) messages.value.splice(idx, 1);
73
+ };
74
+ const updateMessage = (id, patch) => {
75
+ const msg = messages.value.find(m => m.id === id);
76
+ if (msg) Object.assign(msg, patch);
77
+ };
78
+ const sendMessage = async content => {
79
+ if (!content.trim() || isGenerating.value) return;
80
+ messages.value.push({
81
+ id: idGenerator(),
82
+ role: "user",
83
+ content,
84
+ createAt: Date.now(),
85
+ status: "success"
86
+ });
87
+ if (!options.request) return;
88
+ const assId = idGenerator();
89
+ const assistantMsg = {
90
+ id: assId,
91
+ role: "assistant",
92
+ content: "",
93
+ createAt: Date.now(),
94
+ status: "loading"
95
+ };
96
+ messages.value.push(assistantMsg);
97
+ isGenerating.value = true;
98
+ abortController = new AbortController();
99
+ const history = [];
100
+ if (systemPrompt) {
101
+ history.push({
102
+ id: "system",
103
+ role: "system",
104
+ content: systemPrompt,
105
+ createAt: 0,
106
+ status: "success"
107
+ });
108
+ }
109
+ history.push(...messages.value.slice(0, -2));
110
+ try {
111
+ const response = await options.request(content, history, abortController.signal);
112
+ const targetMsg = messages.value.find(m => m.id === assId);
113
+ targetMsg.status = "generating";
114
+ let typewriterInstance = null;
115
+ if (enableTypewriter && typeof requestAnimationFrame !== "undefined") {
116
+ typewriterInstance = createTypewriter(chars => {
117
+ targetMsg.content += chars;
118
+ }, charsPerFrame);
119
+ }
120
+ const pushChunk = text => {
121
+ if (!text) return;
122
+ if (typewriterInstance) {
123
+ typewriterInstance.push(text);
124
+ } else {
125
+ targetMsg.content += text;
126
+ }
127
+ };
128
+ if (typeof response === "object" && response !== null && Symbol.asyncIterator in response) {
129
+ for await (const chunk of response) {
130
+ if (abortController.signal.aborted) break;
131
+ const parsed = parser(chunk);
132
+ if (parsed) pushChunk(parsed);
133
+ }
134
+ } else if (response instanceof Response && response.body) {
135
+ const reader = response.body.getReader();
136
+ const decoder = new TextDecoder("utf-8");
137
+ while (true) {
138
+ if (abortController.signal.aborted) {
139
+ reader.cancel();
140
+ break;
141
+ }
142
+ const {
143
+ done,
144
+ value
145
+ } = await reader.read();
146
+ if (done) break;
147
+ const chunkStr = decoder.decode(value, {
148
+ stream: true
149
+ });
150
+ const parsed = parser(chunkStr);
151
+ if (parsed) pushChunk(parsed);
152
+ }
153
+ } else if (typeof response === "string") {
154
+ pushChunk(response);
155
+ }
156
+ if (typewriterInstance) {
157
+ typewriterInstance.flush();
158
+ }
159
+ if (!abortController.signal.aborted) {
160
+ targetMsg.status = "success";
161
+ options.onFinish?.(targetMsg);
162
+ }
163
+ } catch (e) {
164
+ if (e.name !== "AbortError") {
165
+ const targetMsg = messages.value.find(m => m.id === assId);
166
+ if (targetMsg) targetMsg.status = "error";
167
+ options.onError?.(e);
168
+ }
169
+ } finally {
170
+ if (isGenerating.value) {
171
+ isGenerating.value = false;
172
+ }
173
+ }
174
+ };
175
+ return {
176
+ /** 会话历史 */
177
+ messages,
178
+ /** 是否正在生成(等同 isSending,别名友好) */
179
+ isGenerating,
180
+ /** 同 isGenerating,语义别名 */
181
+ isSending,
182
+ /** 触发发送(自动处理流、打字机) */
183
+ sendMessage,
184
+ /** 停止/中断当前生成 */
185
+ stop,
186
+ /** 移除单条消息 */
187
+ removeMessage,
188
+ /** 修改单条消息内容 */
189
+ updateMessage,
190
+ /** 重置清空所有会话 */
191
+ clear
192
+ };
193
+ }
@@ -0,0 +1,115 @@
1
+ import { type StreamChunkParser } from './use-ai-stream';
2
+ export interface AiChatMessage {
3
+ /** 唯一标识,避免使用 index 做 key */
4
+ id: string;
5
+ /** 消息发送方 */
6
+ role: 'user' | 'assistant' | 'system';
7
+ /** 消息内容 */
8
+ content: string;
9
+ /**
10
+ * 消息状态
11
+ * - loading: 初始加载中(占位)
12
+ * - generating: 流式生成中
13
+ * - success: 已成功完成
14
+ * - error: 发生错误
15
+ * - stopped: 被用户中途打断
16
+ */
17
+ status?: 'loading' | 'generating' | 'success' | 'error' | 'stopped';
18
+ /** 消息时间戳(ms) */
19
+ createAt: number;
20
+ /** 用于展示的时间字符串(可选,不传则自动格式化) */
21
+ time?: string;
22
+ /** 扩展字段 */
23
+ [key: string]: unknown;
24
+ }
25
+ export interface UseAiChatOptions {
26
+ /** 初始化的消息列表 */
27
+ initialMessages?: AiChatMessage[];
28
+ /** 自定义生成 ID 的函数 */
29
+ idGenerator?: () => string;
30
+ /**
31
+ * 请求适配器
32
+ * 支持:
33
+ * - AsyncGenerator<string>:最原始的字符流,每次 yield 一段字符
34
+ * - Promise<string>:直接返回完整回复
35
+ * - Promise<Response>:SSE 流式响应
36
+ */
37
+ request?: (message: string, history: AiChatMessage[], abortSignal: AbortSignal) => AsyncGenerator<string, void, unknown> | Promise<string | Response>;
38
+ /**
39
+ * SSE / 流式块的解析器(适配不同厂商格式)
40
+ * 传入各厂商对应的 parser(来自 useAiStream 模块)
41
+ * @default plainTextParser
42
+ */
43
+ parser?: StreamChunkParser;
44
+ /**
45
+ * 是否启用打字机平滑输出效果
46
+ * @default true
47
+ */
48
+ typewriter?: boolean;
49
+ /**
50
+ * 打字机每帧输出字符数(越大越快)
51
+ * @default 3
52
+ */
53
+ charsPerFrame?: number;
54
+ /**
55
+ * 系统 Prompt(会自动插入到历史的首位)
56
+ */
57
+ systemPrompt?: string;
58
+ /** 出现错误时的回调 */
59
+ onError?: (err: Error) => void;
60
+ /** 消息发送完成回调 */
61
+ onFinish?: (message: AiChatMessage) => void;
62
+ }
63
+ /**
64
+ * useAiChat - 核心 AI 会话管理 Hook
65
+ *
66
+ * 特性:
67
+ * - 消息列表 CRUD + 状态机管理
68
+ * - 支持流式 / 非流式响应
69
+ * - 内置多厂商适配器接口(通过 parser 选项传入)
70
+ * - rAF 打字机平滑效果(可关闭)
71
+ * - 完整的 AbortController 取消支持
72
+ * - 系统 Prompt 自动注入
73
+ */
74
+ export declare function useAiChat(options?: UseAiChatOptions): {
75
+ /** 会话历史 */
76
+ messages: import("vue").Ref<{
77
+ [x: string]: unknown;
78
+ id: string;
79
+ role: "user" | "assistant" | "system";
80
+ content: string;
81
+ status?: "loading" | "generating" | "success" | "error" | "stopped"
82
+ /** 消息时间戳(ms) */
83
+ | undefined;
84
+ createAt: number;
85
+ time?: string
86
+ /** 扩展字段 */
87
+ | undefined;
88
+ }[], AiChatMessage[] | {
89
+ [x: string]: unknown;
90
+ id: string;
91
+ role: "user" | "assistant" | "system";
92
+ content: string;
93
+ status?: "loading" | "generating" | "success" | "error" | "stopped"
94
+ /** 消息时间戳(ms) */
95
+ | undefined;
96
+ createAt: number;
97
+ time?: string
98
+ /** 扩展字段 */
99
+ | undefined;
100
+ }[]>;
101
+ /** 是否正在生成(等同 isSending,别名友好) */
102
+ isGenerating: import("vue").Ref<boolean, boolean>;
103
+ /** 同 isGenerating,语义别名 */
104
+ isSending: import("vue").ComputedRef<boolean>;
105
+ /** 触发发送(自动处理流、打字机) */
106
+ sendMessage: (content: string) => Promise<void>;
107
+ /** 停止/中断当前生成 */
108
+ stop: () => void;
109
+ /** 移除单条消息 */
110
+ removeMessage: (id: string) => void;
111
+ /** 修改单条消息内容 */
112
+ updateMessage: (id: string, patch: Partial<AiChatMessage>) => void;
113
+ /** 重置清空所有会话 */
114
+ clear: () => void;
115
+ };
@@ -0,0 +1,182 @@
1
+ import { ref, computed } from "vue";
2
+ import { plainTextParser } from "./use-ai-stream.mjs";
3
+ function createTypewriter(onChar, charsPerFrame) {
4
+ const queue = [];
5
+ let rafId = null;
6
+ const schedule = () => {
7
+ rafId = requestAnimationFrame(() => {
8
+ rafId = null;
9
+ if (queue.length === 0) return;
10
+ const batch = queue.splice(0, charsPerFrame).join("");
11
+ onChar(batch);
12
+ if (queue.length > 0) schedule();
13
+ });
14
+ };
15
+ return {
16
+ push(text) {
17
+ queue.push(...text.split(""));
18
+ if (rafId === null) schedule();
19
+ },
20
+ flush() {
21
+ if (rafId !== null) {
22
+ cancelAnimationFrame(rafId);
23
+ rafId = null;
24
+ }
25
+ if (queue.length > 0) {
26
+ onChar(queue.splice(0).join(""));
27
+ }
28
+ },
29
+ cancel() {
30
+ if (rafId !== null) {
31
+ cancelAnimationFrame(rafId);
32
+ rafId = null;
33
+ }
34
+ queue.length = 0;
35
+ }
36
+ };
37
+ }
38
+ export function useAiChat(options = {}) {
39
+ const {
40
+ idGenerator = () => Math.random().toString(36).substring(2, 9),
41
+ parser = plainTextParser,
42
+ typewriter: enableTypewriter = true,
43
+ charsPerFrame = 3,
44
+ systemPrompt
45
+ } = options;
46
+ const messages = ref(options.initialMessages ?? []);
47
+ const isGenerating = ref(false);
48
+ const isSending = computed(() => isGenerating.value);
49
+ let abortController = null;
50
+ const stop = () => {
51
+ if (abortController && isGenerating.value) {
52
+ abortController.abort();
53
+ isGenerating.value = false;
54
+ const lastMsg = messages.value[messages.value.length - 1];
55
+ if (lastMsg?.role === "assistant" && lastMsg.status === "generating") {
56
+ lastMsg.status = "stopped";
57
+ }
58
+ }
59
+ };
60
+ const clear = () => {
61
+ stop();
62
+ messages.value = [];
63
+ };
64
+ const removeMessage = (id) => {
65
+ const idx = messages.value.findIndex((m) => m.id === id);
66
+ if (idx !== -1) messages.value.splice(idx, 1);
67
+ };
68
+ const updateMessage = (id, patch) => {
69
+ const msg = messages.value.find((m) => m.id === id);
70
+ if (msg) Object.assign(msg, patch);
71
+ };
72
+ const sendMessage = async (content) => {
73
+ if (!content.trim() || isGenerating.value) return;
74
+ messages.value.push({
75
+ id: idGenerator(),
76
+ role: "user",
77
+ content,
78
+ createAt: Date.now(),
79
+ status: "success"
80
+ });
81
+ if (!options.request) return;
82
+ const assId = idGenerator();
83
+ const assistantMsg = {
84
+ id: assId,
85
+ role: "assistant",
86
+ content: "",
87
+ createAt: Date.now(),
88
+ status: "loading"
89
+ };
90
+ messages.value.push(assistantMsg);
91
+ isGenerating.value = true;
92
+ abortController = new AbortController();
93
+ const history = [];
94
+ if (systemPrompt) {
95
+ history.push({
96
+ id: "system",
97
+ role: "system",
98
+ content: systemPrompt,
99
+ createAt: 0,
100
+ status: "success"
101
+ });
102
+ }
103
+ history.push(...messages.value.slice(0, -2));
104
+ try {
105
+ const response = await options.request(content, history, abortController.signal);
106
+ const targetMsg = messages.value.find((m) => m.id === assId);
107
+ targetMsg.status = "generating";
108
+ let typewriterInstance = null;
109
+ if (enableTypewriter && typeof requestAnimationFrame !== "undefined") {
110
+ typewriterInstance = createTypewriter((chars) => {
111
+ targetMsg.content += chars;
112
+ }, charsPerFrame);
113
+ }
114
+ const pushChunk = (text) => {
115
+ if (!text) return;
116
+ if (typewriterInstance) {
117
+ typewriterInstance.push(text);
118
+ } else {
119
+ targetMsg.content += text;
120
+ }
121
+ };
122
+ if (typeof response === "object" && response !== null && Symbol.asyncIterator in response) {
123
+ for await (const chunk of response) {
124
+ if (abortController.signal.aborted) break;
125
+ const parsed = parser(chunk);
126
+ if (parsed) pushChunk(parsed);
127
+ }
128
+ } else if (response instanceof Response && response.body) {
129
+ const reader = response.body.getReader();
130
+ const decoder = new TextDecoder("utf-8");
131
+ while (true) {
132
+ if (abortController.signal.aborted) {
133
+ reader.cancel();
134
+ break;
135
+ }
136
+ const { done, value } = await reader.read();
137
+ if (done) break;
138
+ const chunkStr = decoder.decode(value, { stream: true });
139
+ const parsed = parser(chunkStr);
140
+ if (parsed) pushChunk(parsed);
141
+ }
142
+ } else if (typeof response === "string") {
143
+ pushChunk(response);
144
+ }
145
+ if (typewriterInstance) {
146
+ typewriterInstance.flush();
147
+ }
148
+ if (!abortController.signal.aborted) {
149
+ targetMsg.status = "success";
150
+ options.onFinish?.(targetMsg);
151
+ }
152
+ } catch (e) {
153
+ if (e.name !== "AbortError") {
154
+ const targetMsg = messages.value.find((m) => m.id === assId);
155
+ if (targetMsg) targetMsg.status = "error";
156
+ options.onError?.(e);
157
+ }
158
+ } finally {
159
+ if (isGenerating.value) {
160
+ isGenerating.value = false;
161
+ }
162
+ }
163
+ };
164
+ return {
165
+ /** 会话历史 */
166
+ messages,
167
+ /** 是否正在生成(等同 isSending,别名友好) */
168
+ isGenerating,
169
+ /** 同 isGenerating,语义别名 */
170
+ isSending,
171
+ /** 触发发送(自动处理流、打字机) */
172
+ sendMessage,
173
+ /** 停止/中断当前生成 */
174
+ stop,
175
+ /** 移除单条消息 */
176
+ removeMessage,
177
+ /** 修改单条消息内容 */
178
+ updateMessage,
179
+ /** 重置清空所有会话 */
180
+ clear
181
+ };
182
+ }