@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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Chat input — supports multi-line and Enter to send
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/5/7
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineComponent, ref } from 'vue';
|
|
11
|
+
import './chat-input.css';
|
|
12
|
+
|
|
13
|
+
export const KChatInput = defineComponent({
|
|
14
|
+
name: 'KChatInput',
|
|
15
|
+
props: {
|
|
16
|
+
placeholder: { type: String, default: '输入消息…' },
|
|
17
|
+
disabled: { type: Boolean, default: false },
|
|
18
|
+
},
|
|
19
|
+
emits: ['send'],
|
|
20
|
+
setup(props, { emit }) {
|
|
21
|
+
const text = ref('');
|
|
22
|
+
|
|
23
|
+
function handleSend() {
|
|
24
|
+
const trimmed = text.value.trim();
|
|
25
|
+
if (!trimmed || props.disabled) return;
|
|
26
|
+
emit('send', trimmed);
|
|
27
|
+
text.value = '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
31
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
handleSend();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return () => (
|
|
38
|
+
<div class="k-chat-input">
|
|
39
|
+
<textarea
|
|
40
|
+
class="k-chat-input__textarea"
|
|
41
|
+
value={text.value}
|
|
42
|
+
onInput={(e: Event) => { text.value = (e.target as HTMLTextAreaElement).value; }}
|
|
43
|
+
onKeydown={handleKeydown}
|
|
44
|
+
placeholder={props.placeholder}
|
|
45
|
+
disabled={props.disabled}
|
|
46
|
+
rows={1}
|
|
47
|
+
/>
|
|
48
|
+
<button
|
|
49
|
+
class="k-chat-input__send"
|
|
50
|
+
onClick={handleSend}
|
|
51
|
+
disabled={props.disabled || !text.value.trim()}
|
|
52
|
+
>
|
|
53
|
+
发送
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Main chat panel — renders message clusters with phase indicator
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/5/7
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineComponent, ref, nextTick, watch, onMounted, type PropType } from 'vue';
|
|
11
|
+
import type { MessageCluster, ChatPhase } from '../composables/types';
|
|
12
|
+
import { KMessageCluster } from './KMessageCluster';
|
|
13
|
+
import { KPhaseIndicator } from './KPhaseIndicator';
|
|
14
|
+
import './chat-panel.css';
|
|
15
|
+
|
|
16
|
+
export const KChatPanel = defineComponent({
|
|
17
|
+
name: 'KChatPanel',
|
|
18
|
+
props: {
|
|
19
|
+
clusters: { type: Array as PropType<MessageCluster[]>, required: true },
|
|
20
|
+
phase: { type: String as PropType<ChatPhase>, default: 'idle' },
|
|
21
|
+
},
|
|
22
|
+
setup(props, { slots }) {
|
|
23
|
+
const scrollRef = ref<HTMLElement>();
|
|
24
|
+
|
|
25
|
+
function scrollToBottom() {
|
|
26
|
+
nextTick(() => {
|
|
27
|
+
const el = scrollRef.value;
|
|
28
|
+
if (el) {
|
|
29
|
+
el.scrollTop = el.scrollHeight;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onMounted(scrollToBottom);
|
|
35
|
+
watch(() => props.clusters.length, scrollToBottom);
|
|
36
|
+
watch(() => {
|
|
37
|
+
const last = props.clusters[props.clusters.length - 1];
|
|
38
|
+
if (!last) return 0;
|
|
39
|
+
const lastMsg = last.messages[last.messages.length - 1];
|
|
40
|
+
return lastMsg?.content.length ?? 0;
|
|
41
|
+
}, scrollToBottom);
|
|
42
|
+
|
|
43
|
+
return () => (
|
|
44
|
+
<div class="k-chat-panel">
|
|
45
|
+
<div class="k-chat-panel__messages" ref={scrollRef}>
|
|
46
|
+
{props.clusters.map(cluster => (
|
|
47
|
+
<KMessageCluster key={cluster.id} cluster={cluster} />
|
|
48
|
+
))}
|
|
49
|
+
{slots.default?.()}
|
|
50
|
+
</div>
|
|
51
|
+
<KPhaseIndicator phase={props.phase} />
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Single message bubble — supports streaming content
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/5/7
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineComponent, type PropType } from 'vue';
|
|
11
|
+
import type { ChatMessage } from '../composables/types';
|
|
12
|
+
import './message-bubble.css';
|
|
13
|
+
|
|
14
|
+
export const KMessageBubble = defineComponent({
|
|
15
|
+
name: 'KMessageBubble',
|
|
16
|
+
props: {
|
|
17
|
+
message: { type: Object as PropType<ChatMessage>, required: true },
|
|
18
|
+
},
|
|
19
|
+
setup(props, { slots }) {
|
|
20
|
+
return () => {
|
|
21
|
+
const msg = props.message;
|
|
22
|
+
return (
|
|
23
|
+
<div class={[
|
|
24
|
+
'k-message-bubble',
|
|
25
|
+
`k-message-bubble--${msg.role}`,
|
|
26
|
+
`k-message-bubble--${msg.status}`,
|
|
27
|
+
]}>
|
|
28
|
+
<div class="k-message-bubble__content">
|
|
29
|
+
{slots.default ? slots.default({ message: msg }) : msg.content}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Message cluster — groups fragmented messages from the same sender
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/5/7
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineComponent, type PropType } from 'vue';
|
|
11
|
+
import type { MessageCluster } from '../composables/types';
|
|
12
|
+
import { KMessageBubble } from './KMessageBubble';
|
|
13
|
+
import './message-cluster.css';
|
|
14
|
+
|
|
15
|
+
export const KMessageCluster = defineComponent({
|
|
16
|
+
name: 'KMessageCluster',
|
|
17
|
+
props: {
|
|
18
|
+
cluster: { type: Object as PropType<MessageCluster>, required: true },
|
|
19
|
+
},
|
|
20
|
+
setup(props) {
|
|
21
|
+
return () => (
|
|
22
|
+
<div class={['k-message-cluster', `k-message-cluster--${props.cluster.role}`]}>
|
|
23
|
+
{props.cluster.messages.map(msg => (
|
|
24
|
+
<KMessageBubble key={msg.id} message={msg} />
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Phase indicator — shows current chat phase with KLoading
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/5/7
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineComponent, computed, type PropType } from 'vue';
|
|
11
|
+
import KLoading from 'kine-ui/components/loading/KLoading.tsx';
|
|
12
|
+
import type { ChatPhase } from '../composables/types';
|
|
13
|
+
import './phase-indicator.css';
|
|
14
|
+
|
|
15
|
+
const PHASE_LABELS: Record<ChatPhase, string> = {
|
|
16
|
+
idle: '',
|
|
17
|
+
aggregating: '正在输入…',
|
|
18
|
+
thinking: '思考中…',
|
|
19
|
+
streaming: '回复中…',
|
|
20
|
+
sending: '发送中…',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const KPhaseIndicator = defineComponent({
|
|
24
|
+
name: 'KPhaseIndicator',
|
|
25
|
+
props: {
|
|
26
|
+
phase: { type: String as PropType<ChatPhase>, required: true },
|
|
27
|
+
},
|
|
28
|
+
setup(props) {
|
|
29
|
+
const label = computed(() => PHASE_LABELS[props.phase]);
|
|
30
|
+
const visible = computed(() => props.phase !== 'idle');
|
|
31
|
+
|
|
32
|
+
return () => visible.value
|
|
33
|
+
? <div class={['k-phase-indicator', `k-phase-indicator--${props.phase}`]}>
|
|
34
|
+
<KLoading size="small" />
|
|
35
|
+
<span>{label.value}</span>
|
|
36
|
+
</div>
|
|
37
|
+
: null;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.k-chat-input {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: flex-end;
|
|
4
|
+
gap: var(--kine-spacing-4);
|
|
5
|
+
padding: var(--kine-spacing-6) var(--kine-spacing-8);
|
|
6
|
+
border-top: 1px solid var(--kine-color-border-default);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.k-chat-input__textarea {
|
|
10
|
+
flex: 1;
|
|
11
|
+
resize: none;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
border: 1px solid var(--kine-control-border-color);
|
|
14
|
+
border-radius: var(--kine-radius-sm);
|
|
15
|
+
padding: var(--kine-spacing-4) var(--kine-spacing-6);
|
|
16
|
+
font-family: var(--kine-font-family-mono);
|
|
17
|
+
font-size: var(--kine-control-font-size-md);
|
|
18
|
+
line-height: 1.5;
|
|
19
|
+
outline: none;
|
|
20
|
+
background: var(--kine-control-bg);
|
|
21
|
+
color: var(--kine-color-text-primary);
|
|
22
|
+
min-height: var(--kine-control-height-md);
|
|
23
|
+
max-height: 120px;
|
|
24
|
+
overflow-y: auto;
|
|
25
|
+
transition: border-color var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.k-chat-input__textarea::placeholder {
|
|
29
|
+
color: var(--kine-color-text-muted);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.k-chat-input__textarea:focus {
|
|
33
|
+
border-color: var(--kine-color-accent-default);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.k-chat-input__send {
|
|
37
|
+
height: var(--kine-control-height-md);
|
|
38
|
+
padding: 0 var(--kine-control-padding-x-md);
|
|
39
|
+
border: 1px solid var(--kine-color-accent-default);
|
|
40
|
+
border-radius: var(--kine-control-radius);
|
|
41
|
+
background: color-mix(in srgb, var(--kine-color-accent-default) 10%, var(--kine-color-bg-tertiary));
|
|
42
|
+
color: var(--kine-color-accent-default);
|
|
43
|
+
font-family: var(--kine-font-family-mono);
|
|
44
|
+
font-size: var(--kine-control-font-size-md);
|
|
45
|
+
font-weight: var(--kine-font-weight-medium, 500);
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
transition: all var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.k-chat-input__send:hover:not(:disabled) {
|
|
52
|
+
background: color-mix(in srgb, var(--kine-color-accent-default) 18%, var(--kine-color-bg-primary));
|
|
53
|
+
border-color: var(--kine-color-accent-hover);
|
|
54
|
+
color: var(--kine-color-accent-hover);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.k-chat-input__send:disabled {
|
|
58
|
+
opacity: var(--kine-opacity-muted);
|
|
59
|
+
cursor: not-allowed;
|
|
60
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.k-chat-panel {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
height: 100%;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
font-family: var(--kine-font-family-mono);
|
|
7
|
+
color: var(--kine-color-text-primary);
|
|
8
|
+
background: var(--kine-color-bg-primary);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.k-chat-panel__messages {
|
|
12
|
+
flex: 1;
|
|
13
|
+
overflow-y: auto;
|
|
14
|
+
padding: var(--kine-spacing-8);
|
|
15
|
+
scroll-behavior: smooth;
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.k-message-bubble {
|
|
2
|
+
max-width: 70%;
|
|
3
|
+
margin-bottom: var(--kine-spacing-2);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.k-message-bubble__content {
|
|
7
|
+
padding: var(--kine-spacing-4) var(--kine-spacing-6);
|
|
8
|
+
border-radius: var(--kine-radius-lg);
|
|
9
|
+
font-size: var(--kine-font-size-xl);
|
|
10
|
+
line-height: 1.5;
|
|
11
|
+
word-break: break-word;
|
|
12
|
+
white-space: pre-wrap;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.k-message-bubble--user .k-message-bubble__content {
|
|
16
|
+
background: color-mix(in srgb, var(--kine-color-accent-default) 15%, var(--kine-color-bg-tertiary));
|
|
17
|
+
color: var(--kine-color-text-primary);
|
|
18
|
+
border-bottom-right-radius: var(--kine-radius-xs);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.k-message-bubble--assistant .k-message-bubble__content {
|
|
22
|
+
background: var(--kine-color-bg-secondary);
|
|
23
|
+
color: var(--kine-color-text-primary);
|
|
24
|
+
border-bottom-left-radius: var(--kine-radius-xs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.k-message-bubble--streaming .k-message-bubble__content {
|
|
28
|
+
opacity: 0.95;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.k-message-bubble--interrupted .k-message-bubble__content {
|
|
32
|
+
opacity: var(--kine-opacity-muted);
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.k-message-cluster {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
margin-bottom: var(--kine-spacing-8);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.k-message-cluster--user {
|
|
8
|
+
align-items: flex-end;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.k-message-cluster--assistant {
|
|
12
|
+
align-items: flex-start;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.k-message-cluster--system {
|
|
16
|
+
align-items: center;
|
|
17
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description WebSocket transport implementation
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/5/7
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ChatTransport,
|
|
12
|
+
OutgoingMessage,
|
|
13
|
+
ServerPushEvent,
|
|
14
|
+
TransportOptions,
|
|
15
|
+
TransportStatus,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
export function createWebSocketTransport(): ChatTransport {
|
|
19
|
+
let ws: WebSocket | null = null;
|
|
20
|
+
let url = '';
|
|
21
|
+
let options: TransportOptions = {};
|
|
22
|
+
let reconnectAttempts = 0;
|
|
23
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
24
|
+
|
|
25
|
+
const eventHandlers: Array<(event: ServerPushEvent) => void> = [];
|
|
26
|
+
const statusHandlers: Array<(status: TransportStatus) => void> = [];
|
|
27
|
+
|
|
28
|
+
function emitStatus(status: TransportStatus) {
|
|
29
|
+
statusHandlers.forEach(h => h(status));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleMessage(raw: MessageEvent) {
|
|
33
|
+
try {
|
|
34
|
+
const event: ServerPushEvent = JSON.parse(raw.data);
|
|
35
|
+
eventHandlers.forEach(h => h(event));
|
|
36
|
+
} catch {
|
|
37
|
+
// non-JSON message, ignore
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tryReconnect() {
|
|
42
|
+
if (!options.reconnect) return;
|
|
43
|
+
const max = options.maxReconnectAttempts ?? 5;
|
|
44
|
+
if (reconnectAttempts >= max) {
|
|
45
|
+
emitStatus('disconnected');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
reconnectAttempts++;
|
|
49
|
+
emitStatus('reconnecting');
|
|
50
|
+
const interval = options.reconnectInterval ?? 3000;
|
|
51
|
+
reconnectTimer = setTimeout(() => connect(url, options), interval);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function connect(targetUrl: string, opts: TransportOptions = {}) {
|
|
55
|
+
url = targetUrl;
|
|
56
|
+
options = opts;
|
|
57
|
+
emitStatus('connecting');
|
|
58
|
+
|
|
59
|
+
ws = new WebSocket(targetUrl, opts.protocols);
|
|
60
|
+
|
|
61
|
+
ws.onopen = () => {
|
|
62
|
+
reconnectAttempts = 0;
|
|
63
|
+
emitStatus('connected');
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
ws.onmessage = handleMessage;
|
|
67
|
+
|
|
68
|
+
ws.onclose = () => {
|
|
69
|
+
ws = null;
|
|
70
|
+
tryReconnect();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
ws.onerror = () => {
|
|
74
|
+
ws?.close();
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function disconnect() {
|
|
79
|
+
if (reconnectTimer) {
|
|
80
|
+
clearTimeout(reconnectTimer);
|
|
81
|
+
reconnectTimer = null;
|
|
82
|
+
}
|
|
83
|
+
options.reconnect = false;
|
|
84
|
+
ws?.close();
|
|
85
|
+
ws = null;
|
|
86
|
+
emitStatus('disconnected');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function send(message: OutgoingMessage) {
|
|
90
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
91
|
+
ws.send(JSON.stringify(message));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function onEvent(handler: (event: ServerPushEvent) => void) {
|
|
96
|
+
eventHandlers.push(handler);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function onStatusChange(handler: (status: TransportStatus) => void) {
|
|
100
|
+
statusHandlers.push(handler);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { connect, disconnect, send, onEvent, onStatusChange };
|
|
104
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
|
|
10
|
+
export type MessageRole = 'user' | 'assistant' | 'system';
|
|
11
|
+
|
|
12
|
+
export type MessageStatus = 'pending' | 'streaming' | 'complete' | 'interrupted';
|
|
13
|
+
|
|
14
|
+
export interface ChatMessage {
|
|
15
|
+
id: string;
|
|
16
|
+
role: MessageRole;
|
|
17
|
+
content: string;
|
|
18
|
+
status: MessageStatus;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
clusterId?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MessageCluster {
|
|
24
|
+
id: string;
|
|
25
|
+
role: MessageRole;
|
|
26
|
+
messages: ChatMessage[];
|
|
27
|
+
timestamp: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ChatPhase =
|
|
31
|
+
| 'idle'
|
|
32
|
+
| 'aggregating'
|
|
33
|
+
| 'thinking'
|
|
34
|
+
| 'streaming'
|
|
35
|
+
| 'sending';
|
|
36
|
+
|
|
37
|
+
export interface Conversation {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
clusters: MessageCluster[];
|
|
41
|
+
createdAt: number;
|
|
42
|
+
updatedAt: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ServerPushEvent {
|
|
46
|
+
type: 'fragment' | 'fragment_end' | 'phase_change' | 'conversation_meta';
|
|
47
|
+
conversationId: string;
|
|
48
|
+
data: FragmentEvent | PhaseChangeEvent | ConversationMetaEvent;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface FragmentEvent {
|
|
52
|
+
messageId: string;
|
|
53
|
+
clusterId: string;
|
|
54
|
+
role: MessageRole;
|
|
55
|
+
content: string;
|
|
56
|
+
status: MessageStatus;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PhaseChangeEvent {
|
|
60
|
+
phase: ChatPhase;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ConversationMetaEvent {
|
|
64
|
+
title?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ChatTransport {
|
|
68
|
+
connect(url: string, options?: TransportOptions): void;
|
|
69
|
+
disconnect(): void;
|
|
70
|
+
send(message: OutgoingMessage): void;
|
|
71
|
+
onEvent(handler: (event: ServerPushEvent) => void): void;
|
|
72
|
+
onStatusChange(handler: (status: TransportStatus) => void): void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type TransportStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
|
|
76
|
+
|
|
77
|
+
export interface TransportOptions {
|
|
78
|
+
protocols?: string[];
|
|
79
|
+
headers?: Record<string, string>;
|
|
80
|
+
reconnect?: boolean;
|
|
81
|
+
reconnectInterval?: number;
|
|
82
|
+
maxReconnectAttempts?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface OutgoingMessage {
|
|
86
|
+
conversationId: string;
|
|
87
|
+
content: string;
|
|
88
|
+
timestamp: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface UseChatOptions {
|
|
92
|
+
transport: ChatTransport;
|
|
93
|
+
conversationId?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface UseChatReturn {
|
|
97
|
+
clusters: import('vue').Ref<MessageCluster[]>;
|
|
98
|
+
phase: import('vue').Ref<ChatPhase>;
|
|
99
|
+
transportStatus: import('vue').Ref<TransportStatus>;
|
|
100
|
+
conversationId: import('vue').Ref<string | undefined>;
|
|
101
|
+
send: (content: string) => void;
|
|
102
|
+
switchConversation: (id: string) => void;
|
|
103
|
+
clear: () => void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface UseChatHistoryOptions {
|
|
107
|
+
storageKey?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface UseChatHistoryReturn {
|
|
111
|
+
conversations: import('vue').Ref<Conversation[]>;
|
|
112
|
+
current: import('vue').Ref<Conversation | undefined>;
|
|
113
|
+
create: (title?: string) => Conversation;
|
|
114
|
+
remove: (id: string) => void;
|
|
115
|
+
rename: (id: string, title: string) => void;
|
|
116
|
+
select: (id: string) => void;
|
|
117
|
+
}
|