@kine-design/ai-chat 0.0.1-beta.1
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/components/KChatInput.tsx +58 -0
- package/components/KChatPanel.tsx +55 -0
- package/components/KMessageBubble.tsx +35 -0
- package/components/KMessageCluster.tsx +29 -0
- package/components/KPhaseIndicator.tsx +39 -0
- package/components/chat-input.css +60 -0
- package/components/chat-panel.css +16 -0
- package/components/message-bubble.css +34 -0
- package/components/message-cluster.css +17 -0
- package/components/phase-indicator.css +8 -0
- package/composables/createWebSocketTransport.ts +104 -0
- package/composables/types.ts +117 -0
- package/composables/useChat.ts +139 -0
- package/composables/useChatHistory.ts +81 -0
- package/dist/ai-chat.css +136 -0
- package/dist/ai-chat.js +436 -0
- package/dist/components/KChatInput.d.ts +24 -0
- package/dist/components/KChatPanel.d.ts +23 -0
- package/dist/components/KMessageBubble.d.ts +13 -0
- package/dist/components/KMessageCluster.d.ts +13 -0
- package/dist/components/KPhaseIndicator.d.ts +13 -0
- package/dist/composables/createWebSocketTransport.d.ts +2 -0
- package/dist/composables/types.d.ts +94 -0
- package/dist/composables/useChat.d.ts +2 -0
- package/dist/composables/useChatHistory.d.ts +2 -0
- package/dist/index.d.ts +17 -0
- package/dist/vite.config.build.d.ts +2 -0
- package/index.ts +39 -0
- package/package.json +30 -0
- package/tsconfig.json +12 -0
- package/vite.config.build.ts +36 -0
package/dist/ai-chat.js
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { computed, createTextVNode, createVNode, defineComponent, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
2
|
+
import KLoading from "kine-ui/components/loading/KLoading.tsx";
|
|
3
|
+
//#region composables/useChat.ts
|
|
4
|
+
/**
|
|
5
|
+
* @description Core chat composable — manages message clusters and server events
|
|
6
|
+
* @author 阿怪
|
|
7
|
+
* @date 2026/5/7
|
|
8
|
+
* @version v0.0.1
|
|
9
|
+
*
|
|
10
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
11
|
+
*/
|
|
12
|
+
function useChat(options) {
|
|
13
|
+
const { transport } = options;
|
|
14
|
+
const clusters = ref([]);
|
|
15
|
+
const phase = ref("idle");
|
|
16
|
+
const transportStatus = ref("disconnected");
|
|
17
|
+
const conversationId = ref(options.conversationId);
|
|
18
|
+
function findOrCreateCluster(clusterId, role) {
|
|
19
|
+
const existing = clusters.value.find((c) => c.id === clusterId);
|
|
20
|
+
if (existing) return existing;
|
|
21
|
+
const cluster = {
|
|
22
|
+
id: clusterId,
|
|
23
|
+
role,
|
|
24
|
+
messages: [],
|
|
25
|
+
timestamp: Date.now()
|
|
26
|
+
};
|
|
27
|
+
clusters.value.push(cluster);
|
|
28
|
+
return cluster;
|
|
29
|
+
}
|
|
30
|
+
function handleFragment(data) {
|
|
31
|
+
const cluster = findOrCreateCluster(data.clusterId, data.role);
|
|
32
|
+
const existing = cluster.messages.find((m) => m.id === data.messageId);
|
|
33
|
+
if (existing) {
|
|
34
|
+
if (data.status === "streaming") existing.content += data.content;
|
|
35
|
+
else existing.content = data.content;
|
|
36
|
+
existing.status = data.status;
|
|
37
|
+
} else cluster.messages.push({
|
|
38
|
+
id: data.messageId,
|
|
39
|
+
role: data.role,
|
|
40
|
+
content: data.content,
|
|
41
|
+
status: data.status,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
clusterId: data.clusterId
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function handlePhaseChange(data) {
|
|
47
|
+
phase.value = data.phase;
|
|
48
|
+
}
|
|
49
|
+
function handleEvent(event) {
|
|
50
|
+
if (conversationId.value && event.conversationId !== conversationId.value) return;
|
|
51
|
+
switch (event.type) {
|
|
52
|
+
case "fragment":
|
|
53
|
+
handleFragment(event.data);
|
|
54
|
+
break;
|
|
55
|
+
case "fragment_end":
|
|
56
|
+
handleFragment({
|
|
57
|
+
...event.data,
|
|
58
|
+
status: "complete"
|
|
59
|
+
});
|
|
60
|
+
break;
|
|
61
|
+
case "phase_change":
|
|
62
|
+
handlePhaseChange(event.data);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
transport.onEvent(handleEvent);
|
|
67
|
+
transport.onStatusChange((status) => {
|
|
68
|
+
transportStatus.value = status;
|
|
69
|
+
});
|
|
70
|
+
function send(content) {
|
|
71
|
+
if (!conversationId.value) return;
|
|
72
|
+
const clusterId = `user-${Date.now()}`;
|
|
73
|
+
const messageId = `msg-${Date.now()}`;
|
|
74
|
+
findOrCreateCluster(clusterId, "user").messages.push({
|
|
75
|
+
id: messageId,
|
|
76
|
+
role: "user",
|
|
77
|
+
content,
|
|
78
|
+
status: "complete",
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
clusterId
|
|
81
|
+
});
|
|
82
|
+
transport.send({
|
|
83
|
+
conversationId: conversationId.value,
|
|
84
|
+
content,
|
|
85
|
+
timestamp: Date.now()
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function switchConversation(id) {
|
|
89
|
+
conversationId.value = id;
|
|
90
|
+
clusters.value = [];
|
|
91
|
+
phase.value = "idle";
|
|
92
|
+
}
|
|
93
|
+
function clear() {
|
|
94
|
+
clusters.value = [];
|
|
95
|
+
phase.value = "idle";
|
|
96
|
+
}
|
|
97
|
+
onUnmounted(() => {
|
|
98
|
+
transport.disconnect();
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
clusters,
|
|
102
|
+
phase,
|
|
103
|
+
transportStatus,
|
|
104
|
+
conversationId,
|
|
105
|
+
send,
|
|
106
|
+
switchConversation,
|
|
107
|
+
clear
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region composables/useChatHistory.ts
|
|
112
|
+
/**
|
|
113
|
+
* @description Chat history composable — multi-conversation management
|
|
114
|
+
* @author 阿怪
|
|
115
|
+
* @date 2026/5/7
|
|
116
|
+
* @version v0.0.1
|
|
117
|
+
*
|
|
118
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
119
|
+
*/
|
|
120
|
+
function generateId() {
|
|
121
|
+
return `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
122
|
+
}
|
|
123
|
+
function useChatHistory(options = {}) {
|
|
124
|
+
const storageKey = options.storageKey ?? "kine-ai-chat-history";
|
|
125
|
+
const conversations = ref([]);
|
|
126
|
+
const current = ref();
|
|
127
|
+
function load() {
|
|
128
|
+
try {
|
|
129
|
+
const raw = localStorage.getItem(storageKey);
|
|
130
|
+
if (raw) conversations.value = JSON.parse(raw);
|
|
131
|
+
} catch {
|
|
132
|
+
conversations.value = [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function persist() {
|
|
136
|
+
try {
|
|
137
|
+
localStorage.setItem(storageKey, JSON.stringify(conversations.value));
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
load();
|
|
141
|
+
watch(conversations, persist, { deep: true });
|
|
142
|
+
function create(title) {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
const conversation = {
|
|
145
|
+
id: generateId(),
|
|
146
|
+
title: title ?? `对话 ${conversations.value.length + 1}`,
|
|
147
|
+
clusters: [],
|
|
148
|
+
createdAt: now,
|
|
149
|
+
updatedAt: now
|
|
150
|
+
};
|
|
151
|
+
conversations.value.unshift(conversation);
|
|
152
|
+
current.value = conversation;
|
|
153
|
+
return conversation;
|
|
154
|
+
}
|
|
155
|
+
function remove(id) {
|
|
156
|
+
const idx = conversations.value.findIndex((c) => c.id === id);
|
|
157
|
+
if (idx === -1) return;
|
|
158
|
+
conversations.value.splice(idx, 1);
|
|
159
|
+
if (current.value?.id === id) current.value = conversations.value[0];
|
|
160
|
+
}
|
|
161
|
+
function rename(id, title) {
|
|
162
|
+
const conv = conversations.value.find((c) => c.id === id);
|
|
163
|
+
if (conv) {
|
|
164
|
+
conv.title = title;
|
|
165
|
+
conv.updatedAt = Date.now();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function select(id) {
|
|
169
|
+
current.value = conversations.value.find((c) => c.id === id);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
conversations,
|
|
173
|
+
current,
|
|
174
|
+
create,
|
|
175
|
+
remove,
|
|
176
|
+
rename,
|
|
177
|
+
select
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region composables/createWebSocketTransport.ts
|
|
182
|
+
function createWebSocketTransport() {
|
|
183
|
+
let ws = null;
|
|
184
|
+
let url = "";
|
|
185
|
+
let options = {};
|
|
186
|
+
let reconnectAttempts = 0;
|
|
187
|
+
let reconnectTimer = null;
|
|
188
|
+
const eventHandlers = [];
|
|
189
|
+
const statusHandlers = [];
|
|
190
|
+
function emitStatus(status) {
|
|
191
|
+
statusHandlers.forEach((h) => h(status));
|
|
192
|
+
}
|
|
193
|
+
function handleMessage(raw) {
|
|
194
|
+
try {
|
|
195
|
+
const event = JSON.parse(raw.data);
|
|
196
|
+
eventHandlers.forEach((h) => h(event));
|
|
197
|
+
} catch {}
|
|
198
|
+
}
|
|
199
|
+
function tryReconnect() {
|
|
200
|
+
if (!options.reconnect) return;
|
|
201
|
+
const max = options.maxReconnectAttempts ?? 5;
|
|
202
|
+
if (reconnectAttempts >= max) {
|
|
203
|
+
emitStatus("disconnected");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
reconnectAttempts++;
|
|
207
|
+
emitStatus("reconnecting");
|
|
208
|
+
const interval = options.reconnectInterval ?? 3e3;
|
|
209
|
+
reconnectTimer = setTimeout(() => connect(url, options), interval);
|
|
210
|
+
}
|
|
211
|
+
function connect(targetUrl, opts = {}) {
|
|
212
|
+
url = targetUrl;
|
|
213
|
+
options = opts;
|
|
214
|
+
emitStatus("connecting");
|
|
215
|
+
ws = new WebSocket(targetUrl, opts.protocols);
|
|
216
|
+
ws.onopen = () => {
|
|
217
|
+
reconnectAttempts = 0;
|
|
218
|
+
emitStatus("connected");
|
|
219
|
+
};
|
|
220
|
+
ws.onmessage = handleMessage;
|
|
221
|
+
ws.onclose = () => {
|
|
222
|
+
ws = null;
|
|
223
|
+
tryReconnect();
|
|
224
|
+
};
|
|
225
|
+
ws.onerror = () => {
|
|
226
|
+
ws?.close();
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function disconnect() {
|
|
230
|
+
if (reconnectTimer) {
|
|
231
|
+
clearTimeout(reconnectTimer);
|
|
232
|
+
reconnectTimer = null;
|
|
233
|
+
}
|
|
234
|
+
options.reconnect = false;
|
|
235
|
+
ws?.close();
|
|
236
|
+
ws = null;
|
|
237
|
+
emitStatus("disconnected");
|
|
238
|
+
}
|
|
239
|
+
function send(message) {
|
|
240
|
+
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message));
|
|
241
|
+
}
|
|
242
|
+
function onEvent(handler) {
|
|
243
|
+
eventHandlers.push(handler);
|
|
244
|
+
}
|
|
245
|
+
function onStatusChange(handler) {
|
|
246
|
+
statusHandlers.push(handler);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
connect,
|
|
250
|
+
disconnect,
|
|
251
|
+
send,
|
|
252
|
+
onEvent,
|
|
253
|
+
onStatusChange
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region components/KMessageBubble.tsx
|
|
258
|
+
/**
|
|
259
|
+
* @description Single message bubble — supports streaming content
|
|
260
|
+
* @author 阿怪
|
|
261
|
+
* @date 2026/5/7
|
|
262
|
+
* @version v0.0.1
|
|
263
|
+
*
|
|
264
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
265
|
+
*/
|
|
266
|
+
var KMessageBubble = /* @__PURE__ */ defineComponent({
|
|
267
|
+
name: "KMessageBubble",
|
|
268
|
+
props: { message: {
|
|
269
|
+
type: Object,
|
|
270
|
+
required: true
|
|
271
|
+
} },
|
|
272
|
+
setup(props, { slots }) {
|
|
273
|
+
return () => {
|
|
274
|
+
const msg = props.message;
|
|
275
|
+
return createVNode("div", { "class": [
|
|
276
|
+
"k-message-bubble",
|
|
277
|
+
`k-message-bubble--${msg.role}`,
|
|
278
|
+
`k-message-bubble--${msg.status}`
|
|
279
|
+
] }, [createVNode("div", { "class": "k-message-bubble__content" }, [slots.default ? slots.default({ message: msg }) : msg.content])]);
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region components/KMessageCluster.tsx
|
|
285
|
+
/**
|
|
286
|
+
* @description Message cluster — groups fragmented messages from the same sender
|
|
287
|
+
* @author 阿怪
|
|
288
|
+
* @date 2026/5/7
|
|
289
|
+
* @version v0.0.1
|
|
290
|
+
*
|
|
291
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
292
|
+
*/
|
|
293
|
+
var KMessageCluster = /* @__PURE__ */ defineComponent({
|
|
294
|
+
name: "KMessageCluster",
|
|
295
|
+
props: { cluster: {
|
|
296
|
+
type: Object,
|
|
297
|
+
required: true
|
|
298
|
+
} },
|
|
299
|
+
setup(props) {
|
|
300
|
+
return () => createVNode("div", { "class": ["k-message-cluster", `k-message-cluster--${props.cluster.role}`] }, [props.cluster.messages.map((msg) => createVNode(KMessageBubble, {
|
|
301
|
+
"key": msg.id,
|
|
302
|
+
"message": msg
|
|
303
|
+
}, null))]);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region components/KPhaseIndicator.tsx
|
|
308
|
+
/**
|
|
309
|
+
* @description Phase indicator — shows current chat phase with KLoading
|
|
310
|
+
* @author 阿怪
|
|
311
|
+
* @date 2026/5/7
|
|
312
|
+
* @version v0.0.1
|
|
313
|
+
*
|
|
314
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
315
|
+
*/
|
|
316
|
+
var PHASE_LABELS = {
|
|
317
|
+
idle: "",
|
|
318
|
+
aggregating: "正在输入…",
|
|
319
|
+
thinking: "思考中…",
|
|
320
|
+
streaming: "回复中…",
|
|
321
|
+
sending: "发送中…"
|
|
322
|
+
};
|
|
323
|
+
var KPhaseIndicator = /* @__PURE__ */ defineComponent({
|
|
324
|
+
name: "KPhaseIndicator",
|
|
325
|
+
props: { phase: {
|
|
326
|
+
type: String,
|
|
327
|
+
required: true
|
|
328
|
+
} },
|
|
329
|
+
setup(props) {
|
|
330
|
+
const label = computed(() => PHASE_LABELS[props.phase]);
|
|
331
|
+
const visible = computed(() => props.phase !== "idle");
|
|
332
|
+
return () => visible.value ? createVNode("div", { "class": ["k-phase-indicator", `k-phase-indicator--${props.phase}`] }, [createVNode(KLoading, { "size": "small" }, null), createVNode("span", null, [label.value])]) : null;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region components/KChatPanel.tsx
|
|
337
|
+
/**
|
|
338
|
+
* @description Main chat panel — renders message clusters with phase indicator
|
|
339
|
+
* @author 阿怪
|
|
340
|
+
* @date 2026/5/7
|
|
341
|
+
* @version v0.0.1
|
|
342
|
+
*
|
|
343
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
344
|
+
*/
|
|
345
|
+
var KChatPanel = /* @__PURE__ */ defineComponent({
|
|
346
|
+
name: "KChatPanel",
|
|
347
|
+
props: {
|
|
348
|
+
clusters: {
|
|
349
|
+
type: Array,
|
|
350
|
+
required: true
|
|
351
|
+
},
|
|
352
|
+
phase: {
|
|
353
|
+
type: String,
|
|
354
|
+
default: "idle"
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
setup(props, { slots }) {
|
|
358
|
+
const scrollRef = ref();
|
|
359
|
+
function scrollToBottom() {
|
|
360
|
+
nextTick(() => {
|
|
361
|
+
const el = scrollRef.value;
|
|
362
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
onMounted(scrollToBottom);
|
|
366
|
+
watch(() => props.clusters.length, scrollToBottom);
|
|
367
|
+
watch(() => {
|
|
368
|
+
const last = props.clusters[props.clusters.length - 1];
|
|
369
|
+
if (!last) return 0;
|
|
370
|
+
return last.messages[last.messages.length - 1]?.content.length ?? 0;
|
|
371
|
+
}, scrollToBottom);
|
|
372
|
+
return () => createVNode("div", { "class": "k-chat-panel" }, [createVNode("div", {
|
|
373
|
+
"class": "k-chat-panel__messages",
|
|
374
|
+
"ref": scrollRef
|
|
375
|
+
}, [props.clusters.map((cluster) => createVNode(KMessageCluster, {
|
|
376
|
+
"key": cluster.id,
|
|
377
|
+
"cluster": cluster
|
|
378
|
+
}, null)), slots.default?.()]), createVNode(KPhaseIndicator, { "phase": props.phase }, null)]);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region components/KChatInput.tsx
|
|
383
|
+
/**
|
|
384
|
+
* @description Chat input — supports multi-line and Enter to send
|
|
385
|
+
* @author 阿怪
|
|
386
|
+
* @date 2026/5/7
|
|
387
|
+
* @version v0.0.1
|
|
388
|
+
*
|
|
389
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
390
|
+
*/
|
|
391
|
+
var KChatInput = /* @__PURE__ */ defineComponent({
|
|
392
|
+
name: "KChatInput",
|
|
393
|
+
props: {
|
|
394
|
+
placeholder: {
|
|
395
|
+
type: String,
|
|
396
|
+
default: "输入消息…"
|
|
397
|
+
},
|
|
398
|
+
disabled: {
|
|
399
|
+
type: Boolean,
|
|
400
|
+
default: false
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
emits: ["send"],
|
|
404
|
+
setup(props, { emit }) {
|
|
405
|
+
const text = ref("");
|
|
406
|
+
function handleSend() {
|
|
407
|
+
const trimmed = text.value.trim();
|
|
408
|
+
if (!trimmed || props.disabled) return;
|
|
409
|
+
emit("send", trimmed);
|
|
410
|
+
text.value = "";
|
|
411
|
+
}
|
|
412
|
+
function handleKeydown(e) {
|
|
413
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
handleSend();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return () => createVNode("div", { "class": "k-chat-input" }, [createVNode("textarea", {
|
|
419
|
+
"class": "k-chat-input__textarea",
|
|
420
|
+
"value": text.value,
|
|
421
|
+
"onInput": (e) => {
|
|
422
|
+
text.value = e.target.value;
|
|
423
|
+
},
|
|
424
|
+
"onKeydown": handleKeydown,
|
|
425
|
+
"placeholder": props.placeholder,
|
|
426
|
+
"disabled": props.disabled,
|
|
427
|
+
"rows": 1
|
|
428
|
+
}, null), createVNode("button", {
|
|
429
|
+
"class": "k-chat-input__send",
|
|
430
|
+
"onClick": handleSend,
|
|
431
|
+
"disabled": props.disabled || !text.value.trim()
|
|
432
|
+
}, [createTextVNode("发送")])]);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
//#endregion
|
|
436
|
+
export { KChatInput, KChatPanel, KMessageBubble, KMessageCluster, KPhaseIndicator, createWebSocketTransport, useChat, useChatHistory };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const KChatInput: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
|
|
2
|
+
placeholder: {
|
|
3
|
+
type: StringConstructor;
|
|
4
|
+
default: string;
|
|
5
|
+
};
|
|
6
|
+
disabled: {
|
|
7
|
+
type: BooleanConstructor;
|
|
8
|
+
default: boolean;
|
|
9
|
+
};
|
|
10
|
+
}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, "send"[], "send", import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
|
|
11
|
+
placeholder: {
|
|
12
|
+
type: StringConstructor;
|
|
13
|
+
default: string;
|
|
14
|
+
};
|
|
15
|
+
disabled: {
|
|
16
|
+
type: BooleanConstructor;
|
|
17
|
+
default: boolean;
|
|
18
|
+
};
|
|
19
|
+
}>> & Readonly<{
|
|
20
|
+
onSend?: ((...args: any[]) => any) | undefined;
|
|
21
|
+
}>, {
|
|
22
|
+
placeholder: string;
|
|
23
|
+
disabled: boolean;
|
|
24
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PropType } from 'vue';
|
|
2
|
+
import { MessageCluster, ChatPhase } from '../composables/types';
|
|
3
|
+
export declare const KChatPanel: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
|
|
4
|
+
clusters: {
|
|
5
|
+
type: PropType<MessageCluster[]>;
|
|
6
|
+
required: true;
|
|
7
|
+
};
|
|
8
|
+
phase: {
|
|
9
|
+
type: PropType<ChatPhase>;
|
|
10
|
+
default: string;
|
|
11
|
+
};
|
|
12
|
+
}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
|
|
13
|
+
clusters: {
|
|
14
|
+
type: PropType<MessageCluster[]>;
|
|
15
|
+
required: true;
|
|
16
|
+
};
|
|
17
|
+
phase: {
|
|
18
|
+
type: PropType<ChatPhase>;
|
|
19
|
+
default: string;
|
|
20
|
+
};
|
|
21
|
+
}>> & Readonly<{}>, {
|
|
22
|
+
phase: ChatPhase;
|
|
23
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PropType } from 'vue';
|
|
2
|
+
import { ChatMessage } from '../composables/types';
|
|
3
|
+
export declare const KMessageBubble: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
|
|
4
|
+
message: {
|
|
5
|
+
type: PropType<ChatMessage>;
|
|
6
|
+
required: true;
|
|
7
|
+
};
|
|
8
|
+
}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
|
|
9
|
+
message: {
|
|
10
|
+
type: PropType<ChatMessage>;
|
|
11
|
+
required: true;
|
|
12
|
+
};
|
|
13
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PropType } from 'vue';
|
|
2
|
+
import { MessageCluster } from '../composables/types';
|
|
3
|
+
export declare const KMessageCluster: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
|
|
4
|
+
cluster: {
|
|
5
|
+
type: PropType<MessageCluster>;
|
|
6
|
+
required: true;
|
|
7
|
+
};
|
|
8
|
+
}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
|
|
9
|
+
cluster: {
|
|
10
|
+
type: PropType<MessageCluster>;
|
|
11
|
+
required: true;
|
|
12
|
+
};
|
|
13
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PropType } from 'vue';
|
|
2
|
+
import { ChatPhase } from '../composables/types';
|
|
3
|
+
export declare const KPhaseIndicator: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
|
|
4
|
+
phase: {
|
|
5
|
+
type: PropType<ChatPhase>;
|
|
6
|
+
required: true;
|
|
7
|
+
};
|
|
8
|
+
}>, () => import("vue/jsx-runtime").JSX.Element | null, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
|
|
9
|
+
phase: {
|
|
10
|
+
type: PropType<ChatPhase>;
|
|
11
|
+
required: true;
|
|
12
|
+
};
|
|
13
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description AI Chat type definitions — fragmented streaming dialogue model
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/5/7
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
export type MessageRole = 'user' | 'assistant' | 'system';
|
|
10
|
+
export type MessageStatus = 'pending' | 'streaming' | 'complete' | 'interrupted';
|
|
11
|
+
export interface ChatMessage {
|
|
12
|
+
id: string;
|
|
13
|
+
role: MessageRole;
|
|
14
|
+
content: string;
|
|
15
|
+
status: MessageStatus;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
clusterId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface MessageCluster {
|
|
20
|
+
id: string;
|
|
21
|
+
role: MessageRole;
|
|
22
|
+
messages: ChatMessage[];
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
export type ChatPhase = 'idle' | 'aggregating' | 'thinking' | 'streaming' | 'sending';
|
|
26
|
+
export interface Conversation {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
clusters: MessageCluster[];
|
|
30
|
+
createdAt: number;
|
|
31
|
+
updatedAt: number;
|
|
32
|
+
}
|
|
33
|
+
export interface ServerPushEvent {
|
|
34
|
+
type: 'fragment' | 'fragment_end' | 'phase_change' | 'conversation_meta';
|
|
35
|
+
conversationId: string;
|
|
36
|
+
data: FragmentEvent | PhaseChangeEvent | ConversationMetaEvent;
|
|
37
|
+
}
|
|
38
|
+
export interface FragmentEvent {
|
|
39
|
+
messageId: string;
|
|
40
|
+
clusterId: string;
|
|
41
|
+
role: MessageRole;
|
|
42
|
+
content: string;
|
|
43
|
+
status: MessageStatus;
|
|
44
|
+
}
|
|
45
|
+
export interface PhaseChangeEvent {
|
|
46
|
+
phase: ChatPhase;
|
|
47
|
+
}
|
|
48
|
+
export interface ConversationMetaEvent {
|
|
49
|
+
title?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ChatTransport {
|
|
52
|
+
connect(url: string, options?: TransportOptions): void;
|
|
53
|
+
disconnect(): void;
|
|
54
|
+
send(message: OutgoingMessage): void;
|
|
55
|
+
onEvent(handler: (event: ServerPushEvent) => void): void;
|
|
56
|
+
onStatusChange(handler: (status: TransportStatus) => void): void;
|
|
57
|
+
}
|
|
58
|
+
export type TransportStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
|
|
59
|
+
export interface TransportOptions {
|
|
60
|
+
protocols?: string[];
|
|
61
|
+
headers?: Record<string, string>;
|
|
62
|
+
reconnect?: boolean;
|
|
63
|
+
reconnectInterval?: number;
|
|
64
|
+
maxReconnectAttempts?: number;
|
|
65
|
+
}
|
|
66
|
+
export interface OutgoingMessage {
|
|
67
|
+
conversationId: string;
|
|
68
|
+
content: string;
|
|
69
|
+
timestamp: number;
|
|
70
|
+
}
|
|
71
|
+
export interface UseChatOptions {
|
|
72
|
+
transport: ChatTransport;
|
|
73
|
+
conversationId?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface UseChatReturn {
|
|
76
|
+
clusters: import('vue').Ref<MessageCluster[]>;
|
|
77
|
+
phase: import('vue').Ref<ChatPhase>;
|
|
78
|
+
transportStatus: import('vue').Ref<TransportStatus>;
|
|
79
|
+
conversationId: import('vue').Ref<string | undefined>;
|
|
80
|
+
send: (content: string) => void;
|
|
81
|
+
switchConversation: (id: string) => void;
|
|
82
|
+
clear: () => void;
|
|
83
|
+
}
|
|
84
|
+
export interface UseChatHistoryOptions {
|
|
85
|
+
storageKey?: string;
|
|
86
|
+
}
|
|
87
|
+
export interface UseChatHistoryReturn {
|
|
88
|
+
conversations: import('vue').Ref<Conversation[]>;
|
|
89
|
+
current: import('vue').Ref<Conversation | undefined>;
|
|
90
|
+
create: (title?: string) => Conversation;
|
|
91
|
+
remove: (id: string) => void;
|
|
92
|
+
rename: (id: string, title: string) => void;
|
|
93
|
+
select: (id: string) => void;
|
|
94
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description @kine-design/ai-chat entry
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/5/7
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
export { useChat } from './composables/useChat';
|
|
10
|
+
export { useChatHistory } from './composables/useChatHistory';
|
|
11
|
+
export { createWebSocketTransport } from './composables/createWebSocketTransport';
|
|
12
|
+
export type { MessageRole, MessageStatus, ChatMessage, MessageCluster, ChatPhase, Conversation, ServerPushEvent, FragmentEvent, PhaseChangeEvent, ConversationMetaEvent, ChatTransport, TransportStatus, TransportOptions, OutgoingMessage, UseChatOptions, UseChatReturn, UseChatHistoryOptions, UseChatHistoryReturn, } from './composables/types';
|
|
13
|
+
export { KChatPanel } from './components/KChatPanel';
|
|
14
|
+
export { KMessageCluster } from './components/KMessageCluster';
|
|
15
|
+
export { KMessageBubble } from './components/KMessageBubble';
|
|
16
|
+
export { KPhaseIndicator } from './components/KPhaseIndicator';
|
|
17
|
+
export { KChatInput } from './components/KChatInput';
|
package/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description @kine-design/ai-chat entry
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/5/7
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { useChat } from './composables/useChat';
|
|
11
|
+
export { useChatHistory } from './composables/useChatHistory';
|
|
12
|
+
export { createWebSocketTransport } from './composables/createWebSocketTransport';
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
MessageRole,
|
|
16
|
+
MessageStatus,
|
|
17
|
+
ChatMessage,
|
|
18
|
+
MessageCluster,
|
|
19
|
+
ChatPhase,
|
|
20
|
+
Conversation,
|
|
21
|
+
ServerPushEvent,
|
|
22
|
+
FragmentEvent,
|
|
23
|
+
PhaseChangeEvent,
|
|
24
|
+
ConversationMetaEvent,
|
|
25
|
+
ChatTransport,
|
|
26
|
+
TransportStatus,
|
|
27
|
+
TransportOptions,
|
|
28
|
+
OutgoingMessage,
|
|
29
|
+
UseChatOptions,
|
|
30
|
+
UseChatReturn,
|
|
31
|
+
UseChatHistoryOptions,
|
|
32
|
+
UseChatHistoryReturn,
|
|
33
|
+
} from './composables/types';
|
|
34
|
+
|
|
35
|
+
export { KChatPanel } from './components/KChatPanel';
|
|
36
|
+
export { KMessageCluster } from './components/KMessageCluster';
|
|
37
|
+
export { KMessageBubble } from './components/KMessageBubble';
|
|
38
|
+
export { KPhaseIndicator } from './components/KPhaseIndicator';
|
|
39
|
+
export { KChatInput } from './components/KChatInput';
|