@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.
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @description Core chat composable — manages message clusters and server events
3
+ * @author 阿怪
4
+ * @date 2026/5/7
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import { ref, onUnmounted } from 'vue';
11
+ import type {
12
+ ChatPhase,
13
+ FragmentEvent,
14
+ MessageCluster,
15
+ PhaseChangeEvent,
16
+ ServerPushEvent,
17
+ TransportStatus,
18
+ UseChatOptions,
19
+ UseChatReturn,
20
+ } from './types';
21
+
22
+ export function useChat(options: UseChatOptions): UseChatReturn {
23
+ const { transport } = options;
24
+
25
+ const clusters = ref<MessageCluster[]>([]);
26
+ const phase = ref<ChatPhase>('idle');
27
+ const transportStatus = ref<TransportStatus>('disconnected');
28
+ const conversationId = ref<string | undefined>(options.conversationId);
29
+
30
+ function findOrCreateCluster(clusterId: string, role: FragmentEvent['role']): MessageCluster {
31
+ const existing = clusters.value.find(c => c.id === clusterId);
32
+ if (existing) return existing;
33
+
34
+ const cluster: MessageCluster = {
35
+ id: clusterId,
36
+ role,
37
+ messages: [],
38
+ timestamp: Date.now(),
39
+ };
40
+ clusters.value.push(cluster);
41
+ return cluster;
42
+ }
43
+
44
+ function handleFragment(data: FragmentEvent) {
45
+ const cluster = findOrCreateCluster(data.clusterId, data.role);
46
+ const existing = cluster.messages.find(m => m.id === data.messageId);
47
+
48
+ if (existing) {
49
+ if (data.status === 'streaming') {
50
+ existing.content += data.content;
51
+ } else {
52
+ existing.content = data.content;
53
+ }
54
+ existing.status = data.status;
55
+ } else {
56
+ cluster.messages.push({
57
+ id: data.messageId,
58
+ role: data.role,
59
+ content: data.content,
60
+ status: data.status,
61
+ timestamp: Date.now(),
62
+ clusterId: data.clusterId,
63
+ });
64
+ }
65
+ }
66
+
67
+ function handlePhaseChange(data: PhaseChangeEvent) {
68
+ phase.value = data.phase;
69
+ }
70
+
71
+ function handleEvent(event: ServerPushEvent) {
72
+ if (conversationId.value && event.conversationId !== conversationId.value) return;
73
+
74
+ switch (event.type) {
75
+ case 'fragment':
76
+ handleFragment(event.data as FragmentEvent);
77
+ break;
78
+ case 'fragment_end':
79
+ handleFragment({ ...(event.data as FragmentEvent), status: 'complete' });
80
+ break;
81
+ case 'phase_change':
82
+ handlePhaseChange(event.data as PhaseChangeEvent);
83
+ break;
84
+ }
85
+ }
86
+
87
+ transport.onEvent(handleEvent);
88
+ transport.onStatusChange(status => {
89
+ transportStatus.value = status;
90
+ });
91
+
92
+ function send(content: string) {
93
+ if (!conversationId.value) return;
94
+
95
+ const clusterId = `user-${Date.now()}`;
96
+ const messageId = `msg-${Date.now()}`;
97
+
98
+ const cluster = findOrCreateCluster(clusterId, 'user');
99
+ cluster.messages.push({
100
+ id: messageId,
101
+ role: 'user',
102
+ content,
103
+ status: 'complete',
104
+ timestamp: Date.now(),
105
+ clusterId,
106
+ });
107
+
108
+ transport.send({
109
+ conversationId: conversationId.value,
110
+ content,
111
+ timestamp: Date.now(),
112
+ });
113
+ }
114
+
115
+ function switchConversation(id: string) {
116
+ conversationId.value = id;
117
+ clusters.value = [];
118
+ phase.value = 'idle';
119
+ }
120
+
121
+ function clear() {
122
+ clusters.value = [];
123
+ phase.value = 'idle';
124
+ }
125
+
126
+ onUnmounted(() => {
127
+ transport.disconnect();
128
+ });
129
+
130
+ return {
131
+ clusters,
132
+ phase,
133
+ transportStatus,
134
+ conversationId,
135
+ send,
136
+ switchConversation,
137
+ clear,
138
+ };
139
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @description Chat history composable — multi-conversation management
3
+ * @author 阿怪
4
+ * @date 2026/5/7
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import { ref, watch } from 'vue';
11
+ import type { Conversation, UseChatHistoryOptions, UseChatHistoryReturn } from './types';
12
+
13
+ function generateId(): string {
14
+ return `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
15
+ }
16
+
17
+ export function useChatHistory(options: UseChatHistoryOptions = {}): UseChatHistoryReturn {
18
+ const storageKey = options.storageKey ?? 'kine-ai-chat-history';
19
+
20
+ const conversations = ref<Conversation[]>([]);
21
+ const current = ref<Conversation | undefined>();
22
+
23
+ function load() {
24
+ try {
25
+ const raw = localStorage.getItem(storageKey);
26
+ if (raw) {
27
+ conversations.value = JSON.parse(raw);
28
+ }
29
+ } catch {
30
+ conversations.value = [];
31
+ }
32
+ }
33
+
34
+ function persist() {
35
+ try {
36
+ localStorage.setItem(storageKey, JSON.stringify(conversations.value));
37
+ } catch {
38
+ // storage full or unavailable
39
+ }
40
+ }
41
+
42
+ load();
43
+ watch(conversations, persist, { deep: true });
44
+
45
+ function create(title?: string): Conversation {
46
+ const now = Date.now();
47
+ const conversation: Conversation = {
48
+ id: generateId(),
49
+ title: title ?? `对话 ${conversations.value.length + 1}`,
50
+ clusters: [],
51
+ createdAt: now,
52
+ updatedAt: now,
53
+ };
54
+ conversations.value.unshift(conversation);
55
+ current.value = conversation;
56
+ return conversation;
57
+ }
58
+
59
+ function remove(id: string) {
60
+ const idx = conversations.value.findIndex(c => c.id === id);
61
+ if (idx === -1) return;
62
+ conversations.value.splice(idx, 1);
63
+ if (current.value?.id === id) {
64
+ current.value = conversations.value[0];
65
+ }
66
+ }
67
+
68
+ function rename(id: string, title: string) {
69
+ const conv = conversations.value.find(c => c.id === id);
70
+ if (conv) {
71
+ conv.title = title;
72
+ conv.updatedAt = Date.now();
73
+ }
74
+ }
75
+
76
+ function select(id: string) {
77
+ current.value = conversations.value.find(c => c.id === id);
78
+ }
79
+
80
+ return { conversations, current, create, remove, rename, select };
81
+ }
@@ -0,0 +1,136 @@
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
+
35
+ .k-message-cluster {
36
+ display: flex;
37
+ flex-direction: column;
38
+ margin-bottom: var(--kine-spacing-8);
39
+ }
40
+
41
+ .k-message-cluster--user {
42
+ align-items: flex-end;
43
+ }
44
+
45
+ .k-message-cluster--assistant {
46
+ align-items: flex-start;
47
+ }
48
+
49
+ .k-message-cluster--system {
50
+ align-items: center;
51
+ }
52
+ .k-phase-indicator {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: var(--kine-spacing-3);
56
+ padding: var(--kine-spacing-2) var(--kine-spacing-8);
57
+ font-size: var(--kine-font-size-md);
58
+ color: var(--kine-color-text-muted);
59
+ }
60
+ .k-chat-panel {
61
+ display: flex;
62
+ flex-direction: column;
63
+ height: 100%;
64
+ overflow: hidden;
65
+ font-family: var(--kine-font-family-mono);
66
+ color: var(--kine-color-text-primary);
67
+ background: var(--kine-color-bg-primary);
68
+ }
69
+
70
+ .k-chat-panel__messages {
71
+ flex: 1;
72
+ overflow-y: auto;
73
+ padding: var(--kine-spacing-8);
74
+ scroll-behavior: smooth;
75
+ }
76
+ .k-chat-input {
77
+ display: flex;
78
+ align-items: flex-end;
79
+ gap: var(--kine-spacing-4);
80
+ padding: var(--kine-spacing-6) var(--kine-spacing-8);
81
+ border-top: 1px solid var(--kine-color-border-default);
82
+ }
83
+
84
+ .k-chat-input__textarea {
85
+ flex: 1;
86
+ resize: none;
87
+ box-sizing: border-box;
88
+ border: 1px solid var(--kine-control-border-color);
89
+ border-radius: var(--kine-radius-sm);
90
+ padding: var(--kine-spacing-4) var(--kine-spacing-6);
91
+ font-family: var(--kine-font-family-mono);
92
+ font-size: var(--kine-control-font-size-md);
93
+ line-height: 1.5;
94
+ outline: none;
95
+ background: var(--kine-control-bg);
96
+ color: var(--kine-color-text-primary);
97
+ min-height: var(--kine-control-height-md);
98
+ max-height: 120px;
99
+ overflow-y: auto;
100
+ transition: border-color var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
101
+ }
102
+
103
+ .k-chat-input__textarea::placeholder {
104
+ color: var(--kine-color-text-muted);
105
+ }
106
+
107
+ .k-chat-input__textarea:focus {
108
+ border-color: var(--kine-color-accent-default);
109
+ }
110
+
111
+ .k-chat-input__send {
112
+ height: var(--kine-control-height-md);
113
+ padding: 0 var(--kine-control-padding-x-md);
114
+ border: 1px solid var(--kine-color-accent-default);
115
+ border-radius: var(--kine-control-radius);
116
+ background: color-mix(in srgb, var(--kine-color-accent-default) 10%, var(--kine-color-bg-tertiary));
117
+ color: var(--kine-color-accent-default);
118
+ font-family: var(--kine-font-family-mono);
119
+ font-size: var(--kine-control-font-size-md);
120
+ font-weight: var(--kine-font-weight-medium, 500);
121
+ cursor: pointer;
122
+ white-space: nowrap;
123
+ transition: all var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
124
+ }
125
+
126
+ .k-chat-input__send:hover:not(:disabled) {
127
+ background: color-mix(in srgb, var(--kine-color-accent-default) 18%, var(--kine-color-bg-primary));
128
+ border-color: var(--kine-color-accent-hover);
129
+ color: var(--kine-color-accent-hover);
130
+ }
131
+
132
+ .k-chat-input__send:disabled {
133
+ opacity: var(--kine-opacity-muted);
134
+ cursor: not-allowed;
135
+ }
136
+ /*$vite$:1*/