ai-helper-core 1.0.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/src/config.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { AiHelperConfig, AiHelperOptions } from './types'
2
+
3
+ const DEFAULT_ICON = `data:image/svg+xml,<svg viewBox=%220 0 1051 1024%22 xmlns=%22http://www.w3.org/2000/svg%22><path d=%22M451.99 1005.12a369.77 369.77 0 0 1-369.81-369.81 369.81 369.81 0 0 1 369.81-369.81h147.93a369.81 369.81 0 0 1 369.81 369.81 369.77 369.77 0 0 1-369.81 369.81zM208.96 709.25a197.25 197.25 0 0 0 197.25 197.25h239.48a197.25 197.25 0 0 0 197.25-197.25 197.22 197.22 0 0 0-197.25-197.22h-239.48a197.22 197.22 0 0 0-197.25 197.18z m475.86 98.65a31.99 31.99 0 0 1-31.99-31.99v-133.28a31.99 31.99 0 0 1 31.99-31.99h6.46a31.99 31.99 0 0 1 31.99 31.99v133.28a31.99 31.99 0 0 1-31.99 31.99z m-324.07 0a31.99 31.99 0 0 1-31.99-31.99v-133.28a31.99 31.99 0 0 1 31.99-31.99h6.5a31.99 31.99 0 0 1 31.99 31.99v133.28a31.99 31.99 0 0 1-31.99 31.99z m91.92-624.2a20.73 20.73 0 0 1-13.37-26.17 88 88 0 0 1 87.39-49.59 91.85 91.85 0 0 1 85.09 43.7 20.77 20.77 0 0 1-10.12 27.57 20.81 20.81 0 0 1-27.65-10.16 53.48 53.48 0 0 0-47.32-19.56 51.32 51.32 0 0 0-47.85 20.77 20.77 20.77 0 0 1-19.79 14.43 20.54 20.54 0 0 1-6.42-0.98z m-69.41-78.78a20.77 20.77 0 0 1-2.34-29.27 194.08 194.08 0 0 1 145.74-56.65 193.52 193.52 0 0 1 144.12 54.91 20.81 20.81 0 0 1-1.78 29.38 20.85 20.85 0 0 1-29.38-1.78 154.39 154.39 0 0 0-112.96-40.94 154.5 154.5 0 0 0-114.09 42.11 20.58 20.58 0 0 1-15.82 7.29 20.88 20.88 0 0 1-13.52-5.02z%22 fill=%22%23fff%22 p-id=%223539%22/><path d=%22M0.04 643.54a115.04 115.04 0 0 0 115.04 115.04v-230.11A115.04 115.04 0 0 0 0.04 643.54z%22 fill=%22%23fff%22 opacity=%22.74%22 p-id=%223540%22/><path d=%22M936.87 528.5v230.11a115.04 115.04 0 0 0 115.04-115.04 115.04 115.04 0 0 0-115.04-115.07z%22 fill=%22%23fff%22 opacity=%22.74%22 p-id=%223541%22/></svg>`
4
+
5
+ const defaultConfig: AiHelperConfig = {
6
+ apiBase: '/api/ai-chat',
7
+ branding: {
8
+ name: '坤土智脑',
9
+ icon: DEFAULT_ICON,
10
+ welcomeTitle: '你好,我是坤土智脑',
11
+ welcomeDesc: '我可以帮你查询数据、分析趋势、生成图表'
12
+ },
13
+ theme: 'auto',
14
+ showQuickQuestions: true,
15
+ endpoints: {
16
+ chat: '/chat',
17
+ history: '/history',
18
+ prompts: '/prompts'
19
+ },
20
+ closeOnRouteChange: true
21
+ }
22
+
23
+ export class AiHelperConfigManager {
24
+ private config: AiHelperConfig
25
+
26
+ constructor(options?: AiHelperOptions) {
27
+ this.config = this.buildConfig(options)
28
+ }
29
+
30
+ set(options: AiHelperOptions): void {
31
+ this.config = this.buildConfig(options)
32
+ }
33
+
34
+ merge(options: AiHelperOptions): void {
35
+ this.config = this.buildConfig(options)
36
+ }
37
+
38
+ get(): AiHelperConfig {
39
+ return { ...this.config }
40
+ }
41
+
42
+ getApiUrl(path: string): string {
43
+ const base = this.config.apiBase.replace(/\/$/, '')
44
+ const p = path.startsWith('/') ? path : `/${path}`
45
+ return `${base}${p}`
46
+ }
47
+
48
+ private buildConfig(options?: AiHelperOptions): AiHelperConfig {
49
+ if (!options) return { ...defaultConfig }
50
+ return {
51
+ apiBase: options.apiBase ?? defaultConfig.apiBase,
52
+ branding: {
53
+ name: options.branding?.name ?? defaultConfig.branding.name,
54
+ icon: options.branding?.icon ?? defaultConfig.branding.icon,
55
+ welcomeTitle: options.branding?.welcomeTitle ?? defaultConfig.branding.welcomeTitle,
56
+ welcomeDesc: options.branding?.welcomeDesc ?? defaultConfig.branding.welcomeDesc
57
+ },
58
+ theme: options.theme ?? defaultConfig.theme,
59
+ getHeaders: options.getHeaders,
60
+ showQuickQuestions: options.showQuickQuestions ?? defaultConfig.showQuickQuestions,
61
+ endpoints: {
62
+ chat: options.endpoints?.chat ?? defaultConfig.endpoints.chat,
63
+ history: options.endpoints?.history ?? defaultConfig.endpoints.history,
64
+ prompts: options.endpoints?.prompts ?? defaultConfig.endpoints.prompts
65
+ },
66
+ closeOnRouteChange: options.closeOnRouteChange ?? defaultConfig.closeOnRouteChange,
67
+ buildChatBody: options.buildChatBody,
68
+ parseChatChunk: options.parseChatChunk,
69
+ parseHistoryResponse: options.parseHistoryResponse,
70
+ parsePromptResponse: options.parsePromptResponse
71
+ }
72
+ }
73
+ }
74
+
75
+ export const configManager = new AiHelperConfigManager()
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ export { AiHelperConfigManager, configManager } from './config'
2
+ export { AiHelperApiClient } from './api/client'
3
+ export { AiHelperStateManager } from './state/manager'
4
+ export { detectTheme, onThemeChange } from './theme/detector'
5
+ export { renderMarkdown } from './markdown/renderer'
6
+ export type {
7
+ AiHelperOptions,
8
+ AiHelperConfig,
9
+ AiHelperBranding,
10
+ AiHelperEndpoints,
11
+ ChatParseResult,
12
+ ChatRequestParams,
13
+ Message,
14
+ AiHelperState,
15
+ PromptRow,
16
+ PromptListResponse,
17
+ PromptQueryParams,
18
+ HistoryMessage,
19
+ HistoryResponse,
20
+ HistoryParams
21
+ } from './types'
@@ -0,0 +1,5 @@
1
+ import { marked } from 'marked'
2
+
3
+ export function renderMarkdown(content: string, theme?: 'light' | 'dark'): string {
4
+ return marked.parse(content, { breaks: true, gfm: true }) as string
5
+ }
@@ -0,0 +1,115 @@
1
+ import type { AiHelperState, Message } from '../types'
2
+
3
+ type StateListener = (state: Readonly<AiHelperState>) => void
4
+
5
+ export class AiHelperStateManager {
6
+ private state: AiHelperState = {
7
+ messages: [],
8
+ isVisible: false,
9
+ isLoading: false,
10
+ isInitial: true
11
+ }
12
+
13
+ private listeners = new Map<string, Set<StateListener>>()
14
+
15
+ on(event: 'stateChange' | 'messageChange', listener: StateListener): () => void {
16
+ if (!this.listeners.has(event)) {
17
+ this.listeners.set(event, new Set())
18
+ }
19
+ this.listeners.get(event)!.add(listener)
20
+ return () => this.off(event, listener)
21
+ }
22
+
23
+ off(event: string, listener: StateListener): void {
24
+ this.listeners.get(event)?.delete(listener)
25
+ }
26
+
27
+ private emit(event: string): void {
28
+ const listeners = this.listeners.get(event)
29
+ if (listeners) {
30
+ for (const listener of listeners) {
31
+ listener(this.state)
32
+ }
33
+ }
34
+ // Also emit stateChange for all state mutations
35
+ if (event !== 'stateChange') {
36
+ const stateListeners = this.listeners.get('stateChange')
37
+ if (stateListeners) {
38
+ for (const listener of stateListeners) {
39
+ listener(this.state)
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ getState(): Readonly<AiHelperState> {
46
+ return this.state
47
+ }
48
+
49
+ show(): void {
50
+ this.state.isVisible = true
51
+ this.emit('stateChange')
52
+ }
53
+
54
+ hide(): void {
55
+ this.state.isVisible = false
56
+ this.emit('stateChange')
57
+ }
58
+
59
+ toggleVisibility(): void {
60
+ this.state.isVisible = !this.state.isVisible
61
+ this.emit('stateChange')
62
+ }
63
+
64
+ addUserMessage(content: string): Message {
65
+ const msg: Message = { id: Date.now(), role: 'user', content }
66
+ this.state.messages = [...this.state.messages, msg]
67
+ this.state.isInitial = false
68
+ this.emit('messageChange')
69
+ return msg
70
+ }
71
+
72
+ startAssistantMessage(): Message {
73
+ const msg: Message = { id: Date.now() + 1, role: 'assistant', content: '', isStreaming: true }
74
+ this.state.messages = [...this.state.messages, msg]
75
+ this.state.isLoading = true
76
+ this.emit('messageChange')
77
+ return msg
78
+ }
79
+
80
+ appendToAssistantMessage(chunk: string): void {
81
+ const lastMsg = this.state.messages[this.state.messages.length - 1]
82
+ if (lastMsg && lastMsg.role === 'assistant') {
83
+ const newContent = lastMsg.content + chunk
84
+ const updated: Message = { ...lastMsg, content: newContent }
85
+ this.state.messages = [...this.state.messages.slice(0, -1), updated]
86
+ this.emit('messageChange')
87
+ }
88
+ }
89
+
90
+ finishAssistantMessage(): void {
91
+ const lastMsg = this.state.messages[this.state.messages.length - 1]
92
+ if (lastMsg && lastMsg.role === 'assistant') {
93
+ lastMsg.isStreaming = false
94
+ }
95
+ this.state.isLoading = false
96
+ this.emit('messageChange')
97
+ }
98
+
99
+ setMessages(messages: Message[]): void {
100
+ this.state.messages = messages
101
+ this.state.isInitial = messages.length === 0
102
+ this.emit('messageChange')
103
+ }
104
+
105
+ clearMessages(): void {
106
+ this.state.messages = []
107
+ this.state.isInitial = true
108
+ this.emit('messageChange')
109
+ }
110
+
111
+ setLoading(loading: boolean): void {
112
+ this.state.isLoading = loading
113
+ this.emit('stateChange')
114
+ }
115
+ }
@@ -0,0 +1,25 @@
1
+ export function detectTheme(): 'light' | 'dark' {
2
+ if (typeof document === 'undefined') return 'light'
3
+ return document.documentElement.classList.contains('dark') ? 'dark' : 'light'
4
+ }
5
+
6
+ export function onThemeChange(callback: (theme: 'light' | 'dark') => void): () => void {
7
+ if (typeof document === 'undefined' || typeof window === 'undefined') {
8
+ return () => {}
9
+ }
10
+
11
+ const observer = new MutationObserver(() => {
12
+ callback(detectTheme())
13
+ })
14
+
15
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
16
+
17
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
18
+ const mediaHandler = () => callback(detectTheme())
19
+ mediaQuery.addEventListener('change', mediaHandler)
20
+
21
+ return () => {
22
+ observer.disconnect()
23
+ mediaQuery.removeEventListener('change', mediaHandler)
24
+ }
25
+ }
package/src/types.ts ADDED
@@ -0,0 +1,136 @@
1
+ // 品牌配置
2
+ export interface AiHelperBranding {
3
+ name?: string
4
+ icon?: string
5
+ welcomeTitle?: string
6
+ welcomeDesc?: string
7
+ }
8
+
9
+ // API 端点配置
10
+ export interface AiHelperEndpoints {
11
+ chat?: string
12
+ history?: string
13
+ prompts?: string
14
+ }
15
+
16
+ /**
17
+ * 流式聊天响应解析结果
18
+ * content: 从当前 chunk 中提取的文本(追加到 assistant 消息)
19
+ * done: 是否已结束(设为 true 时终止流,不再调用 onChunk)
20
+ */
21
+ export interface ChatParseResult {
22
+ content: string
23
+ done?: boolean
24
+ }
25
+
26
+ /**
27
+ * 聊天请求构建参数
28
+ */
29
+ export interface ChatRequestParams {
30
+ message: string
31
+ }
32
+
33
+ // 初始化选项
34
+ export interface AiHelperOptions {
35
+ apiBase?: string
36
+ branding?: AiHelperBranding
37
+ theme?: 'light' | 'dark' | 'auto'
38
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>
39
+ showQuickQuestions?: boolean
40
+ endpoints?: AiHelperEndpoints
41
+ closeOnRouteChange?: boolean
42
+ /**
43
+ * 自定义聊天请求体构建函数
44
+ * 返回将作为 POST body 发送的字符串
45
+ * 默认: JSON.stringify({ message })
46
+ */
47
+ buildChatBody?: (params: ChatRequestParams) => string
48
+ /**
49
+ * 自定义流式响应解析函数
50
+ * 每个 SSE chunk(或 NDJSON 行)调用一次
51
+ * 返回 { content, done }
52
+ * 默认: 解析为 { content: string } 格式
53
+ */
54
+ parseChatChunk?: (rawChunk: string) => ChatParseResult
55
+ /**
56
+ * 自定义历史响应解析函数
57
+ * 接收 fetch().json() 的原始结果,返回 HistoryResponse
58
+ * 默认: 期望原始结果为 { messages: [{role, content}], total }
59
+ */
60
+ parseHistoryResponse?: (raw: unknown) => HistoryResponse
61
+ /**
62
+ * 自定义提示词响应解析函数
63
+ * 接收 fetch().json() 的原始结果,返回 PromptListResponse
64
+ * 默认: 期望原始结果为 { items: [{id, name, content}], total }
65
+ */
66
+ parsePromptResponse?: (raw: unknown) => PromptListResponse
67
+ }
68
+
69
+ // 解析后的完整配置
70
+ export interface AiHelperConfig {
71
+ apiBase: string
72
+ branding: Required<AiHelperBranding>
73
+ theme: 'light' | 'dark' | 'auto'
74
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>
75
+ showQuickQuestions: boolean
76
+ endpoints: Required<AiHelperEndpoints>
77
+ closeOnRouteChange: boolean
78
+ buildChatBody?: (params: ChatRequestParams) => string
79
+ parseChatChunk?: (rawChunk: string) => ChatParseResult
80
+ parseHistoryResponse?: (raw: unknown) => HistoryResponse
81
+ parsePromptResponse?: (raw: unknown) => PromptListResponse
82
+ }
83
+
84
+ // 聊天消息
85
+ export interface Message {
86
+ id: number
87
+ role: 'user' | 'assistant'
88
+ content: string
89
+ isStreaming?: boolean
90
+ }
91
+
92
+ // 快捷问题
93
+ export interface PromptRow {
94
+ id: number
95
+ name: string
96
+ content: string
97
+ description?: string
98
+ created_at: string
99
+ updated_at: string
100
+ }
101
+
102
+ export interface PromptListResponse {
103
+ items: PromptRow[]
104
+ total: number
105
+ }
106
+
107
+ export interface PromptQueryParams {
108
+ keyword?: string
109
+ limit?: number
110
+ offset?: number
111
+ }
112
+
113
+ // 历史记录
114
+ export interface HistoryMessage {
115
+ role: 'user' | 'assistant'
116
+ content: string
117
+ timestamp: string
118
+ }
119
+
120
+ export interface HistoryResponse {
121
+ messages: HistoryMessage[]
122
+ total: number
123
+ }
124
+
125
+ export interface HistoryParams {
126
+ limit?: number
127
+ offset?: number
128
+ }
129
+
130
+ // 内部状态
131
+ export interface AiHelperState {
132
+ messages: Message[]
133
+ isVisible: boolean
134
+ isLoading: boolean
135
+ isInitial: boolean
136
+ }