@aura-react-comp/x 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +80 -2
- package/esm/actions/index.d.ts +28 -0
- package/esm/actions/index.js +37 -0
- package/esm/actions/index.less +59 -0
- package/esm/attachments/index.d.ts +41 -0
- package/esm/attachments/index.js +48 -0
- package/esm/attachments/index.less +22 -0
- package/esm/bubble/index.d.ts +42 -0
- package/esm/bubble/index.js +80 -0
- package/esm/bubble/index.less +164 -0
- package/esm/code-highlighter/index.d.ts +39 -0
- package/esm/code-highlighter/index.js +165 -0
- package/esm/code-highlighter/index.less +76 -0
- package/esm/code-highlighter/prism-theme.d.ts +13 -0
- package/esm/code-highlighter/prism-theme.js +75 -0
- package/esm/conversations/index.d.ts +35 -0
- package/esm/conversations/index.js +89 -0
- package/esm/conversations/index.less +138 -0
- package/esm/file-card/index.d.ts +40 -0
- package/esm/file-card/index.js +126 -0
- package/esm/file-card/index.less +161 -0
- package/esm/folder/index.d.ts +66 -0
- package/esm/folder/index.js +345 -0
- package/esm/folder/index.less +158 -0
- package/esm/folder/tree-utils.d.ts +30 -0
- package/esm/folder/tree-utils.js +147 -0
- package/esm/folder/types.d.ts +51 -0
- package/esm/folder/types.js +1 -0
- package/esm/index.d.ts +40 -0
- package/esm/index.js +20 -0
- package/esm/markdown-content/index.d.ts +34 -0
- package/esm/markdown-content/index.js +117 -0
- package/esm/markdown-content/index.less +124 -0
- package/esm/mermaid/index.d.ts +47 -0
- package/esm/mermaid/index.js +235 -0
- package/esm/mermaid/index.less +155 -0
- package/esm/notification/index.d.ts +59 -0
- package/esm/notification/index.js +148 -0
- package/esm/prompts/index.d.ts +27 -0
- package/esm/prompts/index.js +50 -0
- package/esm/prompts/index.less +95 -0
- package/esm/sender/index.d.ts +48 -0
- package/esm/sender/index.js +88 -0
- package/esm/sender/index.less +75 -0
- package/esm/sources/index.d.ts +40 -0
- package/esm/sources/index.js +154 -0
- package/esm/sources/index.less +219 -0
- package/esm/style.css +1565 -0
- package/esm/suggestion/index.d.ts +21 -0
- package/esm/suggestion/index.js +36 -0
- package/esm/suggestion/index.less +44 -0
- package/esm/think/index.d.ts +20 -0
- package/esm/think/index.js +48 -0
- package/esm/think/index.less +92 -0
- package/esm/thought-chain/index.d.ts +34 -0
- package/esm/thought-chain/index.js +105 -0
- package/esm/thought-chain/index.less +174 -0
- package/esm/use-x-chat/index.d.ts +85 -0
- package/esm/use-x-chat/index.js +178 -0
- package/esm/use-x-stream/index.d.ts +43 -0
- package/esm/use-x-stream/index.js +112 -0
- package/esm/use-x-stream/parse-sse.d.ts +19 -0
- package/esm/use-x-stream/parse-sse.js +91 -0
- package/esm/utils/color.d.ts +9 -0
- package/esm/utils/color.js +45 -0
- package/esm/welcome/index.d.ts +22 -0
- package/esm/welcome/index.js +36 -0
- package/esm/welcome/index.less +57 -0
- package/esm/x-provider/index.d.ts +48 -0
- package/esm/x-provider/index.js +66 -0
- package/package.json +69 -3
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { isAbortError } from "../use-x-stream";
|
|
3
|
+
|
|
4
|
+
/** 一条对话消息 */
|
|
5
|
+
|
|
6
|
+
/** onRequest 收到的上下文 */
|
|
7
|
+
|
|
8
|
+
/** 默认消息的来源:定长数组,或按会话动态求值(可异步,用于拉取历史) */
|
|
9
|
+
|
|
10
|
+
const cloneMessages = messages => messages.map(message => ({
|
|
11
|
+
...message
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* useXChat — 对话消息编排 Hook(与传输层 useXStream 正交)。
|
|
16
|
+
*
|
|
17
|
+
* 职责:维护消息列表状态机(user / assistant 成对追加、loading 态、
|
|
18
|
+
* 增量更新、错误态、中止与清空),并按 `conversationKey` 管理多会话切换。
|
|
19
|
+
* **不做传输**——怎么请求由 `onRequest` 决定,典型组合是内部调用
|
|
20
|
+
* `useXStream().fetchData` 并在 onMessage 里 `update` 内容。
|
|
21
|
+
*/
|
|
22
|
+
export function useXChat({
|
|
23
|
+
conversationKey,
|
|
24
|
+
defaultMessages,
|
|
25
|
+
initialMessages = [],
|
|
26
|
+
onRequest,
|
|
27
|
+
onError
|
|
28
|
+
}) {
|
|
29
|
+
const source = defaultMessages ?? initialMessages;
|
|
30
|
+
|
|
31
|
+
// 用 ref 持有最新的来源:它只在 conversationKey 变化时被读取,
|
|
32
|
+
// 因此调用方传数组字面量也不会每次渲染都触发重置。
|
|
33
|
+
const sourceRef = useRef(source);
|
|
34
|
+
sourceRef.current = source;
|
|
35
|
+
const [messages, setMessagesState] = useState(() => Array.isArray(source) ? cloneMessages(source) : []);
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
|
+
const [isDefaultMessagesRequesting, setRequesting] = useState(false);
|
|
38
|
+
const counterRef = useRef(0);
|
|
39
|
+
const abortRef = useRef(null);
|
|
40
|
+
/** 最近一次解析出的默认消息,供 clear() 复位使用 */
|
|
41
|
+
const baselineRef = useRef(Array.isArray(source) ? cloneMessages(source) : []);
|
|
42
|
+
const mountedRef = useRef(true);
|
|
43
|
+
const keyRef = useRef(conversationKey);
|
|
44
|
+
keyRef.current = conversationKey;
|
|
45
|
+
const nextId = () => `x-msg-${++counterRef.current}`;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 按 id 定位 assistant 消息。
|
|
49
|
+
*
|
|
50
|
+
* 刻意**不**用「当前 assistant id」这样的 ref:切换会话会中止旧请求,
|
|
51
|
+
* 而旧请求的收尾是异步的;若收尾时去读共享 ref,就会把新会话里
|
|
52
|
+
* 刚创建的 assistant 占位误标为完成。把 id 捕获在闭包里才没有这个竞态。
|
|
53
|
+
*/
|
|
54
|
+
const patchById = useCallback((id, patch) => {
|
|
55
|
+
setMessagesState(prev => prev.map(message => message.id === id ? {
|
|
56
|
+
...message,
|
|
57
|
+
...patch
|
|
58
|
+
} : message));
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
/** 仅把「仍是 loading」的那条置为 success,不覆盖 error 态 */
|
|
62
|
+
const settleById = useCallback(id => {
|
|
63
|
+
setMessagesState(prev => prev.map(message => message.id === id && message.status === 'loading' ? {
|
|
64
|
+
...message,
|
|
65
|
+
status: 'success'
|
|
66
|
+
} : message));
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
/** 求值默认消息:数组直接克隆,函数则调用(可能返回 Promise) */
|
|
70
|
+
const resolveDefault = useCallback(async () => {
|
|
71
|
+
const current = sourceRef.current;
|
|
72
|
+
const resolved = typeof current === 'function' ? await current({
|
|
73
|
+
conversationKey: keyRef.current
|
|
74
|
+
}) : current;
|
|
75
|
+
return cloneMessages(resolved ?? []);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
// 挂载 + conversationKey 变化:重新初始化消息
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
let cancelled = false;
|
|
81
|
+
const first = mountedRef.current;
|
|
82
|
+
mountedRef.current = false;
|
|
83
|
+
|
|
84
|
+
// 挂载时若来源是数组,useState 初值已经处理过,无需再走一遍
|
|
85
|
+
if (first && !(typeof sourceRef.current === 'function')) return undefined;
|
|
86
|
+
|
|
87
|
+
// 切换会话:先中止旧请求,避免它的增量写进新会话
|
|
88
|
+
abortRef.current?.abort();
|
|
89
|
+
abortRef.current = null;
|
|
90
|
+
setLoading(false);
|
|
91
|
+
setRequesting(true);
|
|
92
|
+
void (async () => {
|
|
93
|
+
try {
|
|
94
|
+
const next = await resolveDefault();
|
|
95
|
+
if (cancelled) return;
|
|
96
|
+
baselineRef.current = next;
|
|
97
|
+
setMessagesState(next);
|
|
98
|
+
} finally {
|
|
99
|
+
if (!cancelled) setRequesting(false);
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
return () => {
|
|
103
|
+
cancelled = true;
|
|
104
|
+
};
|
|
105
|
+
}, [conversationKey, resolveDefault]);
|
|
106
|
+
const send = useCallback(content => {
|
|
107
|
+
if (loading) return; // 串行语义:进行中的请求未结束时忽略新发送
|
|
108
|
+
if (!content.trim()) return; // 空白内容忽略
|
|
109
|
+
|
|
110
|
+
const userMessage = {
|
|
111
|
+
id: nextId(),
|
|
112
|
+
role: 'user',
|
|
113
|
+
content
|
|
114
|
+
};
|
|
115
|
+
const assistantMessage = {
|
|
116
|
+
id: nextId(),
|
|
117
|
+
role: 'assistant',
|
|
118
|
+
content: '',
|
|
119
|
+
status: 'loading'
|
|
120
|
+
};
|
|
121
|
+
const assistantId = assistantMessage.id;
|
|
122
|
+
const history = cloneMessages([...messages, userMessage, assistantMessage]);
|
|
123
|
+
setMessagesState(history);
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
abortRef.current = controller;
|
|
126
|
+
setLoading(true);
|
|
127
|
+
void (async () => {
|
|
128
|
+
try {
|
|
129
|
+
await onRequest({
|
|
130
|
+
message: content,
|
|
131
|
+
messages: history,
|
|
132
|
+
signal: controller.signal,
|
|
133
|
+
update: patch => patchById(assistantId, patch)
|
|
134
|
+
});
|
|
135
|
+
settleById(assistantId);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// stop() / 切换会话触发的 abort 属正常结束:保留已生成的部分内容
|
|
138
|
+
if (isAbortError(error)) {
|
|
139
|
+
settleById(assistantId);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
patchById(assistantId, {
|
|
143
|
+
status: 'error'
|
|
144
|
+
});
|
|
145
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
146
|
+
} finally {
|
|
147
|
+
// 只有当这次请求仍是「当前请求」时才收尾:切换会话后旧的收尾
|
|
148
|
+
// 不能把新会话刚点亮的 loading 态按下去
|
|
149
|
+
if (abortRef.current === controller) {
|
|
150
|
+
abortRef.current = null;
|
|
151
|
+
setLoading(false);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
})();
|
|
155
|
+
}, [loading, messages, onRequest, onError, patchById, settleById]);
|
|
156
|
+
const stop = useCallback(() => {
|
|
157
|
+
abortRef.current?.abort();
|
|
158
|
+
}, []);
|
|
159
|
+
const clear = useCallback(() => {
|
|
160
|
+
abortRef.current?.abort();
|
|
161
|
+
abortRef.current = null;
|
|
162
|
+
setLoading(false);
|
|
163
|
+
setMessagesState(cloneMessages(baselineRef.current));
|
|
164
|
+
}, []);
|
|
165
|
+
const setMessages = useCallback(next => {
|
|
166
|
+
// 直接替换,不触发请求;进行中的请求若仍在跑,其 update 会因 id 失配而落空
|
|
167
|
+
setMessagesState(cloneMessages(next));
|
|
168
|
+
}, []);
|
|
169
|
+
return {
|
|
170
|
+
messages,
|
|
171
|
+
loading,
|
|
172
|
+
isDefaultMessagesRequesting,
|
|
173
|
+
send,
|
|
174
|
+
stop,
|
|
175
|
+
clear,
|
|
176
|
+
setMessages
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type XStreamChunk } from './parse-sse';
|
|
2
|
+
export type { XStreamChunk } from './parse-sse';
|
|
3
|
+
export interface FetchDataStreamOptions {
|
|
4
|
+
/** 流式接口地址 */
|
|
5
|
+
url: string;
|
|
6
|
+
/** 缺省:body 存在时为 POST,否则 GET */
|
|
7
|
+
method?: string;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* 请求体。传普通对象时自动 `JSON.stringify` 并设置 `Content-Type: application/json`;
|
|
11
|
+
* FormData / Blob 等特殊类型请自行序列化后以字符串传入。
|
|
12
|
+
*/
|
|
13
|
+
body?: unknown;
|
|
14
|
+
/** 每解析出一条消息回调一次 */
|
|
15
|
+
onMessage(chunk: XStreamChunk): void;
|
|
16
|
+
/** 流正常结束(含 abort 静默结束)后回调 */
|
|
17
|
+
onDone?(): void;
|
|
18
|
+
/** 请求或读流出错(abort 不算)后回调 */
|
|
19
|
+
onError?(error: Error): void;
|
|
20
|
+
}
|
|
21
|
+
export interface UseXStreamResult {
|
|
22
|
+
/** 是否有进行中的流式请求 */
|
|
23
|
+
streaming: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 发起一次流式请求并逐条消费。同一时刻只保留一条流:
|
|
26
|
+
* 再次 fetchData 会先 abort 上一次(静默结束,不触发 onError)。
|
|
27
|
+
*/
|
|
28
|
+
fetchData(options: FetchDataStreamOptions): Promise<void>;
|
|
29
|
+
/** 中止当前流(静默结束:不触发 onError) */
|
|
30
|
+
abort(): void;
|
|
31
|
+
}
|
|
32
|
+
/** 判定是否为 abort 导致的错误(fetch / 读流被 signal 中断时抛出) */
|
|
33
|
+
export declare function isAbortError(error: unknown): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* useXStream — 流式传输层 Hook。
|
|
36
|
+
*
|
|
37
|
+
* 负责:fetch + 读流 + SSE / 纯文本两种模式的解析 + abort 生命周期,
|
|
38
|
+
* **不做任何消息状态管理**(那是 useXChat 的职责),两者保持正交。
|
|
39
|
+
*
|
|
40
|
+
* - 响应 `Content-Type` 含 `text/event-stream` → 按 SSE 解析;
|
|
41
|
+
* - 其余类型按「纯文本增量流」处理,每个字节块原样作为一条消息产出。
|
|
42
|
+
*/
|
|
43
|
+
export declare function useXStream(): UseXStreamResult;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { parseSSEStream } from "./parse-sse";
|
|
3
|
+
/** 判定是否为 abort 导致的错误(fetch / 读流被 signal 中断时抛出) */
|
|
4
|
+
export function isAbortError(error) {
|
|
5
|
+
return error instanceof DOMException ? error.name === 'AbortError' : error instanceof Error && error.name === 'AbortError';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* useXStream — 流式传输层 Hook。
|
|
10
|
+
*
|
|
11
|
+
* 负责:fetch + 读流 + SSE / 纯文本两种模式的解析 + abort 生命周期,
|
|
12
|
+
* **不做任何消息状态管理**(那是 useXChat 的职责),两者保持正交。
|
|
13
|
+
*
|
|
14
|
+
* - 响应 `Content-Type` 含 `text/event-stream` → 按 SSE 解析;
|
|
15
|
+
* - 其余类型按「纯文本增量流」处理,每个字节块原样作为一条消息产出。
|
|
16
|
+
*/
|
|
17
|
+
export function useXStream() {
|
|
18
|
+
const [streaming, setStreaming] = useState(false);
|
|
19
|
+
const controllerRef = useRef(null);
|
|
20
|
+
const mountedRef = useRef(true);
|
|
21
|
+
|
|
22
|
+
// 卸载时中止在途流,避免对已卸载组件回调 setState
|
|
23
|
+
useEffect(() => () => {
|
|
24
|
+
mountedRef.current = false;
|
|
25
|
+
controllerRef.current?.abort();
|
|
26
|
+
}, []);
|
|
27
|
+
const abort = useCallback(() => {
|
|
28
|
+
controllerRef.current?.abort();
|
|
29
|
+
}, []);
|
|
30
|
+
const fetchData = useCallback(async options => {
|
|
31
|
+
// 串行语义:新请求先中止上一次(静默结束,不触发其 onError)
|
|
32
|
+
controllerRef.current?.abort();
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
controllerRef.current = controller;
|
|
35
|
+
setStreaming(true);
|
|
36
|
+
const finishStreaming = () => {
|
|
37
|
+
if (mountedRef.current) setStreaming(false);
|
|
38
|
+
if (controllerRef.current === controller) controllerRef.current = null;
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
const headers = {
|
|
42
|
+
Accept: 'text/event-stream',
|
|
43
|
+
...options.headers
|
|
44
|
+
};
|
|
45
|
+
let body;
|
|
46
|
+
const raw = options.body;
|
|
47
|
+
if (raw !== undefined && raw !== null) {
|
|
48
|
+
if (typeof raw === 'object') {
|
|
49
|
+
var _ContentType;
|
|
50
|
+
body = JSON.stringify(raw);
|
|
51
|
+
headers[_ContentType = 'Content-Type'] ?? (headers[_ContentType] = 'application/json');
|
|
52
|
+
} else {
|
|
53
|
+
body = raw;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const response = await fetch(options.url, {
|
|
57
|
+
method: options.method ?? (body !== undefined ? 'POST' : 'GET'),
|
|
58
|
+
headers,
|
|
59
|
+
body,
|
|
60
|
+
signal: controller.signal
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`[XStream] 请求失败:${response.status} ${response.statusText || ''}`.trim());
|
|
64
|
+
}
|
|
65
|
+
const contentType = response.headers.get('Content-Type') ?? '';
|
|
66
|
+
if (response.body && contentType.includes('text/event-stream')) {
|
|
67
|
+
// SSE 模式
|
|
68
|
+
for await (const chunk of parseSSEStream(response.body)) {
|
|
69
|
+
options.onMessage(chunk);
|
|
70
|
+
}
|
|
71
|
+
} else if (response.body) {
|
|
72
|
+
// 纯文本增量流模式:每个字节块原样产出
|
|
73
|
+
const reader = response.body.getReader();
|
|
74
|
+
const decoder = new TextDecoder('utf-8');
|
|
75
|
+
while (true) {
|
|
76
|
+
const {
|
|
77
|
+
done,
|
|
78
|
+
value
|
|
79
|
+
} = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
const text = decoder.decode(value, {
|
|
82
|
+
stream: true
|
|
83
|
+
});
|
|
84
|
+
if (text) options.onMessage({
|
|
85
|
+
data: text,
|
|
86
|
+
event: 'message'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
// 兜底:无读流的响应,整体作为一条消息
|
|
91
|
+
const text = await response.text();
|
|
92
|
+
if (text) options.onMessage({
|
|
93
|
+
data: text,
|
|
94
|
+
event: 'message'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
options.onDone?.();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// abort 是主动停止,属于「正常结束」,静默处理
|
|
100
|
+
if (!isAbortError(error)) {
|
|
101
|
+
options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
finishStreaming();
|
|
105
|
+
}
|
|
106
|
+
}, []);
|
|
107
|
+
return {
|
|
108
|
+
streaming,
|
|
109
|
+
fetchData,
|
|
110
|
+
abort
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** SSE 解析出的单条消息 */
|
|
2
|
+
export interface XStreamChunk {
|
|
3
|
+
/** data 字段内容;多行 data 以 \n 拼接 */
|
|
4
|
+
data: string;
|
|
5
|
+
/** event 字段,缺省为 "message" */
|
|
6
|
+
event: string;
|
|
7
|
+
/** id 字段(可选) */
|
|
8
|
+
id?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 把字节流解析为 SSE 消息的异步生成器。
|
|
12
|
+
*
|
|
13
|
+
* 兼容性处理:BOM、CRLF / LF 混用、多行 data(以 \n 拼接)、注释行与心跳帧
|
|
14
|
+
* (无 data,不产出消息)、流结束时未以空行收尾的最后一条帧(宽容产出)。
|
|
15
|
+
*
|
|
16
|
+
* `data: [DONE]` 等「结束哨兵」不做特殊处理,原样产出,由消费方过滤——
|
|
17
|
+
* 解析器保持协议无关。
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseSSEStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<XStreamChunk, void, unknown>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SSE(text/event-stream)字节流 → 消息对象的解析器。
|
|
2
|
+
//
|
|
3
|
+
// 纯函数、UI 无关:输入 ReadableStream,输出异步生成器,便于单测(可用
|
|
4
|
+
// ReadableStream 造任意帧序列)与复用(useXStream 之外的场景也能用)。
|
|
5
|
+
|
|
6
|
+
/** SSE 解析出的单条消息 */
|
|
7
|
+
|
|
8
|
+
/** 找到下一个帧分隔符(空行)的位置;分隔符兼容 \n\n 与 \r\n\r\n */
|
|
9
|
+
function findFrameEnd(buffer) {
|
|
10
|
+
const lf = buffer.indexOf('\n\n');
|
|
11
|
+
const crlf = buffer.indexOf('\r\n\r\n');
|
|
12
|
+
if (lf === -1) return crlf;
|
|
13
|
+
if (crlf === -1) return lf;
|
|
14
|
+
return Math.min(lf, crlf);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 分隔符长度取决于命中的是 LF 还是 CRLF 形式 */
|
|
18
|
+
function frameEndLength(buffer, index) {
|
|
19
|
+
return buffer.startsWith('\r\n\r\n', index) ? 4 : 2;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 解析单个 SSE 帧;无 data 内容的帧(注释 / 心跳)返回 null */
|
|
23
|
+
function parseFrame(frame) {
|
|
24
|
+
let event = 'message';
|
|
25
|
+
let id;
|
|
26
|
+
const dataLines = [];
|
|
27
|
+
for (const rawLine of frame.split(/\r?\n/)) {
|
|
28
|
+
// 空行与注释行(以冒号开头,如 ": ping")按规范忽略
|
|
29
|
+
if (!rawLine || rawLine.startsWith(':')) continue;
|
|
30
|
+
const colon = rawLine.indexOf(':');
|
|
31
|
+
const field = colon === -1 ? rawLine : rawLine.slice(0, colon);
|
|
32
|
+
let value = colon === -1 ? '' : rawLine.slice(colon + 1);
|
|
33
|
+
if (value.startsWith(' ')) value = value.slice(1);
|
|
34
|
+
if (field === 'data') dataLines.push(value);else if (field === 'event') event = value;else if (field === 'id') id = value;
|
|
35
|
+
// retry 等其余字段第一版不消费
|
|
36
|
+
}
|
|
37
|
+
const data = dataLines.join('\n');
|
|
38
|
+
if (!data) return null;
|
|
39
|
+
return id !== undefined ? {
|
|
40
|
+
data,
|
|
41
|
+
event,
|
|
42
|
+
id
|
|
43
|
+
} : {
|
|
44
|
+
data,
|
|
45
|
+
event
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 把字节流解析为 SSE 消息的异步生成器。
|
|
51
|
+
*
|
|
52
|
+
* 兼容性处理:BOM、CRLF / LF 混用、多行 data(以 \n 拼接)、注释行与心跳帧
|
|
53
|
+
* (无 data,不产出消息)、流结束时未以空行收尾的最后一条帧(宽容产出)。
|
|
54
|
+
*
|
|
55
|
+
* `data: [DONE]` 等「结束哨兵」不做特殊处理,原样产出,由消费方过滤——
|
|
56
|
+
* 解析器保持协议无关。
|
|
57
|
+
*/
|
|
58
|
+
export async function* parseSSEStream(stream) {
|
|
59
|
+
const decoder = new TextDecoder('utf-8');
|
|
60
|
+
const reader = stream.getReader();
|
|
61
|
+
let buffer = '';
|
|
62
|
+
try {
|
|
63
|
+
while (true) {
|
|
64
|
+
const {
|
|
65
|
+
done,
|
|
66
|
+
value
|
|
67
|
+
} = await reader.read();
|
|
68
|
+
if (done) break;
|
|
69
|
+
buffer += decoder.decode(value, {
|
|
70
|
+
stream: true
|
|
71
|
+
});
|
|
72
|
+
buffer = buffer.replace(/^\uFEFF/, '');
|
|
73
|
+
let index = findFrameEnd(buffer);
|
|
74
|
+
while (index !== -1) {
|
|
75
|
+
const frame = buffer.slice(0, index);
|
|
76
|
+
buffer = buffer.slice(index + frameEndLength(buffer, index));
|
|
77
|
+
const chunk = parseFrame(frame);
|
|
78
|
+
if (chunk) yield chunk;
|
|
79
|
+
index = findFrameEnd(buffer);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 流结束但缓冲区还有内容:宽容地把最后一条未以空行收尾的帧产出
|
|
84
|
+
if (buffer.trim()) {
|
|
85
|
+
const chunk = parseFrame(buffer);
|
|
86
|
+
if (chunk) yield chunk;
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
reader.releaseLock();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type Rgb = [number, number, number];
|
|
2
|
+
export declare function hexToRgb(hex: string): Rgb | null;
|
|
3
|
+
/** hex → rgba() 字符串 */
|
|
4
|
+
export declare function toRgba(hex: string, alpha: number): string | null;
|
|
5
|
+
/** 向白色混合(ratio = 0 原色,1 纯白)——用于渐变的亮端 */
|
|
6
|
+
export declare function lighten(hex: string, ratio: number): string | null;
|
|
7
|
+
/** 由主色派生的 CSS 变量集(对应 tokens.css 中 --aura-x-* 的品牌相关子集) */
|
|
8
|
+
export declare function deriveAccentVars(primary: string): Record<string, string> | null;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// 主题派生工具:从主色推导 --aura-x-* 动态变量(渐变 / 透明度 / 光晕)。
|
|
2
|
+
// 仅处理 hex 输入(#rgb / #rrggbb);无法解析时返回 null,由调用方兜底。
|
|
3
|
+
|
|
4
|
+
export function hexToRgb(hex) {
|
|
5
|
+
const normalized = hex.trim().replace(/^#/, '');
|
|
6
|
+
const full = normalized.length === 3 ? normalized.split('').map(c => c + c).join('') : normalized;
|
|
7
|
+
if (!/^[0-9a-fA-F]{6}$/.test(full)) return null;
|
|
8
|
+
const value = parseInt(full, 16);
|
|
9
|
+
return [value >> 16 & 255, value >> 8 & 255, value & 255];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** hex → rgba() 字符串 */
|
|
13
|
+
export function toRgba(hex, alpha) {
|
|
14
|
+
const rgb = hexToRgb(hex);
|
|
15
|
+
if (!rgb) return null;
|
|
16
|
+
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 向白色混合(ratio = 0 原色,1 纯白)——用于渐变的亮端 */
|
|
20
|
+
export function lighten(hex, ratio) {
|
|
21
|
+
const rgb = hexToRgb(hex);
|
|
22
|
+
if (!rgb) return null;
|
|
23
|
+
const mixed = rgb.map(channel => Math.round(channel + (255 - channel) * ratio));
|
|
24
|
+
return `#${mixed.map(c => c.toString(16).padStart(2, '0')).join('')}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 由主色派生的 CSS 变量集(对应 tokens.css 中 --aura-x-* 的品牌相关子集) */
|
|
28
|
+
export function deriveAccentVars(primary) {
|
|
29
|
+
const rgb = hexToRgb(primary);
|
|
30
|
+
if (!rgb) return null;
|
|
31
|
+
const lightEnd = lighten(primary, 0.22) ?? primary;
|
|
32
|
+
const glowColor = toRgba(primary, 0.28) ?? `rgba(124, 58, 237, 0.28)`;
|
|
33
|
+
const ringColor = toRgba(primary, 0.16) ?? `rgba(124, 58, 237, 0.16)`;
|
|
34
|
+
const softColor = toRgba(primary, 0.08) ?? `rgba(124, 58, 237, 0.08)`;
|
|
35
|
+
const borderColor = toRgba(primary, 0.22) ?? `rgba(124, 58, 237, 0.22)`;
|
|
36
|
+
return {
|
|
37
|
+
'--aura-x-accent': primary,
|
|
38
|
+
'--aura-x-accent-gradient': `linear-gradient(135deg, ${primary} 0%, ${lightEnd} 100%)`,
|
|
39
|
+
'--aura-x-accent-soft': softColor,
|
|
40
|
+
'--aura-x-bubble-user-bg': `linear-gradient(135deg, ${primary} 0%, ${lightEnd} 100%)`,
|
|
41
|
+
'--aura-x-bubble-border': borderColor,
|
|
42
|
+
'--aura-x-glow': `0 0 0 1px ${toRgba(primary, 0.16)}, 0 12px 32px -8px ${glowColor}`,
|
|
43
|
+
'--aura-x-focus-ring': `0 0 0 3px ${ringColor}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface WelcomeProps {
|
|
3
|
+
/** 顶部图标 / 头像(如 Logo、渐变球) */
|
|
4
|
+
icon?: React.ReactNode;
|
|
5
|
+
/** 欢迎标题(如「早上好,Landy」) */
|
|
6
|
+
title?: React.ReactNode;
|
|
7
|
+
/** 副标题 / 描述 */
|
|
8
|
+
description?: React.ReactNode;
|
|
9
|
+
/** 附加内容插槽(常放 Prompts 提示词集) */
|
|
10
|
+
extra?: React.ReactNode;
|
|
11
|
+
/** board:带边框面板;simple:纯内容 */
|
|
12
|
+
variant?: 'board' | 'simple';
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: React.CSSProperties;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Welcome — 对话开始前的欢迎区。
|
|
18
|
+
*
|
|
19
|
+
* 典型用法:icon + title + description 居中展示,
|
|
20
|
+
* `extra` 里放 [`Prompts`](/x-components/prompts) 引导用户开口。
|
|
21
|
+
*/
|
|
22
|
+
export declare const Welcome: React.FC<WelcomeProps>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { classNames, prefixCls } from '@aura-react-comp/shared';
|
|
3
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
4
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
5
|
+
/**
|
|
6
|
+
* Welcome — 对话开始前的欢迎区。
|
|
7
|
+
*
|
|
8
|
+
* 典型用法:icon + title + description 居中展示,
|
|
9
|
+
* `extra` 里放 [`Prompts`](/x-components/prompts) 引导用户开口。
|
|
10
|
+
*/
|
|
11
|
+
export const Welcome = ({
|
|
12
|
+
icon,
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
extra,
|
|
16
|
+
variant = 'board',
|
|
17
|
+
className,
|
|
18
|
+
style
|
|
19
|
+
}) => /*#__PURE__*/_jsxs("div", {
|
|
20
|
+
className: classNames(prefixCls('x-welcome'), prefixCls(`x-welcome--${variant}`), className),
|
|
21
|
+
style: style,
|
|
22
|
+
children: [icon ? /*#__PURE__*/_jsx("div", {
|
|
23
|
+
className: prefixCls('x-welcome-icon'),
|
|
24
|
+
children: icon
|
|
25
|
+
}) : null, title ? /*#__PURE__*/_jsx("div", {
|
|
26
|
+
className: prefixCls('x-welcome-title'),
|
|
27
|
+
children: title
|
|
28
|
+
}) : null, description ? /*#__PURE__*/_jsx("div", {
|
|
29
|
+
className: prefixCls('x-welcome-description'),
|
|
30
|
+
children: description
|
|
31
|
+
}) : null, extra ? /*#__PURE__*/_jsx("div", {
|
|
32
|
+
className: prefixCls('x-welcome-extra'),
|
|
33
|
+
children: extra
|
|
34
|
+
}) : null]
|
|
35
|
+
});
|
|
36
|
+
Welcome.displayName = 'Welcome';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Welcome — 欢迎区(顶部光晕 + 渐变图标底座) */
|
|
2
|
+
|
|
3
|
+
.aura-x-welcome {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: var(--aura-spacing-3);
|
|
8
|
+
padding: var(--aura-spacing-6) var(--aura-spacing-4);
|
|
9
|
+
text-align: center;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.aura-x-welcome--board {
|
|
14
|
+
border: 1px solid var(--aura-x-bubble-border);
|
|
15
|
+
border-radius: var(--aura-radius-lg);
|
|
16
|
+
background:
|
|
17
|
+
radial-gradient(
|
|
18
|
+
560px 180px at 50% -60px,
|
|
19
|
+
var(--aura-x-accent-soft),
|
|
20
|
+
transparent 70%
|
|
21
|
+
),
|
|
22
|
+
var(--aura-x-bubble-ai-bg);
|
|
23
|
+
backdrop-filter: var(--aura-x-glass-blur);
|
|
24
|
+
-webkit-backdrop-filter: var(--aura-x-glass-blur);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* 图标底座:渐变圆角方块 + 光晕 */
|
|
28
|
+
.aura-x-welcome-icon {
|
|
29
|
+
display: grid;
|
|
30
|
+
place-items: center;
|
|
31
|
+
width: 56px;
|
|
32
|
+
height: 56px;
|
|
33
|
+
border-radius: var(--aura-radius-lg);
|
|
34
|
+
background: var(--aura-x-accent-gradient);
|
|
35
|
+
color: #fff;
|
|
36
|
+
font-size: 28px;
|
|
37
|
+
line-height: 1;
|
|
38
|
+
box-shadow: var(--aura-x-glow);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.aura-x-welcome-title {
|
|
42
|
+
font-size: var(--aura-font-size-2xl);
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
color: var(--aura-text);
|
|
45
|
+
letter-spacing: 0.2px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.aura-x-welcome-description {
|
|
49
|
+
font-size: var(--aura-font-size-sm);
|
|
50
|
+
color: var(--aura-text-secondary);
|
|
51
|
+
max-width: 480px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.aura-x-welcome-extra {
|
|
55
|
+
margin-top: var(--aura-spacing-2);
|
|
56
|
+
width: 100%;
|
|
57
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Locale } from 'antd/es/locale';
|
|
3
|
+
import type { ThemeConfig } from 'antd';
|
|
4
|
+
export interface XProviderProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
/** 暗色模式:切换 antd 暗色算法,并在子树作用域启用暗色令牌(data-theme="dark") */
|
|
7
|
+
dark?: boolean;
|
|
8
|
+
/** 是否启用紧凑模式 */
|
|
9
|
+
compact?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* 主题主色。组件视觉(渐变 / 光晕 / 软底)由它**实时派生**:
|
|
12
|
+
* 换主色,所有 --aura-x-* 品牌变量即时跟随。
|
|
13
|
+
* @default auraTokens.colors.primary[700] (#7c3aed)
|
|
14
|
+
*/
|
|
15
|
+
colorPrimary?: string;
|
|
16
|
+
/**
|
|
17
|
+
* 语言包
|
|
18
|
+
* @default zhCN
|
|
19
|
+
*/
|
|
20
|
+
locale?: Locale;
|
|
21
|
+
}
|
|
22
|
+
export interface XThemeConfigOptions {
|
|
23
|
+
dark?: boolean;
|
|
24
|
+
compact?: boolean;
|
|
25
|
+
colorPrimary?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 组装 XProvider 的 antd 主题配置。
|
|
29
|
+
*
|
|
30
|
+
* 令牌覆盖集来自 @aura-react-comp/shared 的单一数据源(与 BusinessProvider 共用);
|
|
31
|
+
* 这里只负责 algorithm 的组装:暗色 / 默认 + 可选紧凑。
|
|
32
|
+
* 导出以便单测直接断言配置内容。
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildXThemeConfig({ dark, compact, colorPrimary }: XThemeConfigOptions): ThemeConfig;
|
|
35
|
+
/**
|
|
36
|
+
* XProvider — AI 组件主题桥接
|
|
37
|
+
*
|
|
38
|
+
* 将 Aura 设计令牌映射到 antd 的主题系统,为 AI 对话组件(Bubble / Sender 等)
|
|
39
|
+
* 提供与 @aura-react-comp/ui、@aura-react-comp/business 一致的紫罗兰视觉语言。
|
|
40
|
+
*
|
|
41
|
+
* antd v6 默认使用 CSS Variables,因此可与 Aura 的 CSS 变量主题体系共存。
|
|
42
|
+
*
|
|
43
|
+
* 主题派生:`colorPrimary` 会实时派生整套 `--aura-x-*` 品牌变量(渐变 / 光晕 /
|
|
44
|
+
* 软底 / 边框),注入到子树作用域——Bubble / Sender / MarkdownContent 等
|
|
45
|
+
* 组件消费这些变量,因此**换主色即时全量生效**;`dark` 同时在子树启用
|
|
46
|
+
* `data-theme="dark"` 暗色令牌。
|
|
47
|
+
*/
|
|
48
|
+
export declare const XProvider: React.FC<XProviderProps>;
|