@amaster.ai/components-templates 1.5.0 → 1.6.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/README.md +12 -8
- package/components/ai-assistant/amaster.config.json +3 -0
- package/components/ai-assistant/package.json +3 -3
- package/components/ai-assistant/template/components/chat-assistant-message.tsx +1 -1
- package/components/ai-assistant/template/components/chat-banner.tsx +1 -1
- package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +6 -6
- package/components/ai-assistant/template/components/chat-floating-button.tsx +4 -5
- package/components/ai-assistant/template/components/chat-floating-card.tsx +3 -3
- package/components/ai-assistant/template/components/chat-header.tsx +5 -5
- package/components/ai-assistant/template/components/chat-input.tsx +21 -22
- package/components/ai-assistant/template/components/chat-messages.tsx +10 -2
- package/components/ai-assistant/template/components/chat-recommends.tsx +12 -14
- package/components/ai-assistant/template/components/chat-user-message.tsx +1 -1
- package/components/ai-assistant/template/components/ui-renderer.tsx +1 -1
- package/components/ai-assistant/template/components/voice-input.tsx +1 -1
- package/components/ai-assistant/template/hooks/useVoiceInput.ts +18 -20
- package/components/ai-assistant/template/inline-ai-assistant.tsx +1 -1
- package/components/ai-assistant-taro/amaster.config.json +3 -0
- package/components/ai-assistant-taro/package.json +94 -0
- package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +154 -0
- package/components/ai-assistant-taro/template/components/ChatHeader.tsx +27 -0
- package/components/ai-assistant-taro/template/components/ChatInput.tsx +204 -0
- package/components/ai-assistant-taro/template/components/ChatMessages.tsx +126 -0
- package/components/ai-assistant-taro/template/components/ChatUserMessage.tsx +25 -0
- package/components/ai-assistant-taro/template/components/VoiceInput.tsx +169 -0
- package/components/ai-assistant-taro/template/components/markdown.tsx +156 -0
- package/components/ai-assistant-taro/template/hooks/useConversation.ts +787 -0
- package/components/ai-assistant-taro/template/hooks/useSafeArea.ts +20 -0
- package/components/ai-assistant-taro/template/hooks/useVoiceInput.ts +204 -0
- package/components/ai-assistant-taro/template/i18n.ts +157 -0
- package/components/ai-assistant-taro/template/index.config.ts +10 -0
- package/components/ai-assistant-taro/template/index.tsx +83 -0
- package/components/ai-assistant-taro/template/types.ts +58 -0
- package/package.json +5 -2
- package/packages/cli/dist/index.js +14 -3
- package/packages/cli/dist/index.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/components/ai-assistant/example.md +0 -34
- package/components/ai-assistant/others.md +0 -16
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Text, View } from "@tarojs/components";
|
|
2
|
+
import Taro from "@tarojs/taro";
|
|
3
|
+
import type React from "react";
|
|
4
|
+
import { useCallback, useRef, useState } from "react";
|
|
5
|
+
import { useVoiceInput, type VoiceInputStatus } from "../hooks/useVoiceInput";
|
|
6
|
+
import { useAiAssistantI18n } from "../i18n";
|
|
7
|
+
|
|
8
|
+
interface VoiceInputProps {
|
|
9
|
+
onResult: (text: string) => void;
|
|
10
|
+
onError?: (error: string) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const getStatusIcon = (status: VoiceInputStatus): string => {
|
|
15
|
+
switch (status) {
|
|
16
|
+
case "recording":
|
|
17
|
+
return "i-lucide-mic";
|
|
18
|
+
case "recognizing":
|
|
19
|
+
return "i-lucide-brain";
|
|
20
|
+
default:
|
|
21
|
+
return "i-lucide-mic";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getStatusColor = (status: VoiceInputStatus): string => {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case "recording":
|
|
28
|
+
return "bg-red-500";
|
|
29
|
+
case "recognizing":
|
|
30
|
+
return "bg-blue-500";
|
|
31
|
+
default:
|
|
32
|
+
return "bg-gray-500";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const VoiceInput: React.FC<VoiceInputProps> = ({
|
|
37
|
+
onResult,
|
|
38
|
+
disabled = false,
|
|
39
|
+
}) => {
|
|
40
|
+
const [showOverlay, setShowOverlay] = useState(false);
|
|
41
|
+
const shouldStopRef = useRef(false);
|
|
42
|
+
const { t } = useAiAssistantI18n();
|
|
43
|
+
|
|
44
|
+
const getStatusText = (status: VoiceInputStatus): string => {
|
|
45
|
+
switch (status) {
|
|
46
|
+
case "recording":
|
|
47
|
+
return t.VoiceInputStatus.recording;
|
|
48
|
+
case "recognizing":
|
|
49
|
+
return t.VoiceInputStatus.recognizing;
|
|
50
|
+
default:
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleStatusChange = useCallback((newStatus: VoiceInputStatus) => {
|
|
56
|
+
if (newStatus === "idle") {
|
|
57
|
+
setShowOverlay(false);
|
|
58
|
+
// setTimeout(() => {
|
|
59
|
+
// }, 500);
|
|
60
|
+
}
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const handleResult = useCallback(
|
|
64
|
+
(text: string) => {
|
|
65
|
+
if (text) {
|
|
66
|
+
onResult(text);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[onResult],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const { status, startRecording, stopRecording } = useVoiceInput({
|
|
73
|
+
onResult: handleResult,
|
|
74
|
+
onError: (error) => {
|
|
75
|
+
let errorType = error;
|
|
76
|
+
if (error.includes("deny")) {
|
|
77
|
+
errorType = "MIC_PERMISSION_DENIED";
|
|
78
|
+
}
|
|
79
|
+
Taro.showToast({
|
|
80
|
+
title: t.voiceInputError[errorType] || error,
|
|
81
|
+
icon: "none",
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
onStatusChange: handleStatusChange,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const handleTouchStart = () => {
|
|
88
|
+
if (disabled) return;
|
|
89
|
+
shouldStopRef.current = false;
|
|
90
|
+
setShowOverlay(true);
|
|
91
|
+
startRecording();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleTouchEnd = () => {
|
|
95
|
+
shouldStopRef.current = true;
|
|
96
|
+
stopRecording();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{showOverlay && (
|
|
102
|
+
<View
|
|
103
|
+
className="fixed top-0 left-0 right-0 bottom-0 w-screen h-screen z-[9999] flex items-center justify-center"
|
|
104
|
+
style={{ backgroundColor: "rgba(0, 0, 0, 0.6)" }}
|
|
105
|
+
catchMove
|
|
106
|
+
>
|
|
107
|
+
<View className="flex flex-col items-center gap-6 px-20 py-10 bg-black/80 rounded-3xl shadow-2xl relative">
|
|
108
|
+
<View
|
|
109
|
+
className={`size-20 rounded-full ${getStatusColor(status)} flex items-center justify-center shadow-lg ${status === "recording" ? "animate-pulse" : ""}`}
|
|
110
|
+
>
|
|
111
|
+
<View
|
|
112
|
+
className={`text-white text-5xl ${getStatusIcon(status)}`}
|
|
113
|
+
/>
|
|
114
|
+
</View>
|
|
115
|
+
|
|
116
|
+
<Text className="text-white text-center text-xl font-medium whitespace-pre-line leading-relaxed">
|
|
117
|
+
{getStatusText(status)}
|
|
118
|
+
</Text>
|
|
119
|
+
|
|
120
|
+
{(status === "recording" || status === "idle") && (
|
|
121
|
+
<View className="flex items-center gap-2 mt-2">
|
|
122
|
+
<View className="w-2 h-2 bg-red-400 rounded-full animate-pulse" />
|
|
123
|
+
<View
|
|
124
|
+
className="w-2 h-2 bg-red-400 rounded-full animate-pulse"
|
|
125
|
+
style={{ animationDelay: "150ms" }}
|
|
126
|
+
/>
|
|
127
|
+
<View
|
|
128
|
+
className="w-2 h-2 bg-red-400 rounded-full animate-pulse"
|
|
129
|
+
style={{ animationDelay: "300ms" }}
|
|
130
|
+
/>
|
|
131
|
+
</View>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{/* 点击结束 */}
|
|
135
|
+
<View
|
|
136
|
+
className="w-6 h-6 i-lucide-x text-white rounded-full bg-gray-500 absolute top-2 right-2"
|
|
137
|
+
onClick={() => {
|
|
138
|
+
stopRecording();
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
</View>
|
|
142
|
+
</View>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<View
|
|
146
|
+
onTouchStart={handleTouchStart}
|
|
147
|
+
onTouchEnd={handleTouchEnd}
|
|
148
|
+
onTouchCancel={handleTouchEnd}
|
|
149
|
+
onLongPress={handleTouchStart}
|
|
150
|
+
className={`flex items-center justify-center gap-1.5 px-4 py-2 bg-danger rounded-xl active:opacity-80 transition-all duration-200 shadow-sm ${
|
|
151
|
+
disabled
|
|
152
|
+
? "bg-gray-200 opacity-50"
|
|
153
|
+
: status !== "idle"
|
|
154
|
+
? "bg-red-500 shadow-md"
|
|
155
|
+
: "bg-gradient-to-r from-primary to-primary/80 active:opacity-80 shadow-md"
|
|
156
|
+
}`}
|
|
157
|
+
hoverClass={disabled ? "" : "opacity-90"}
|
|
158
|
+
>
|
|
159
|
+
<View
|
|
160
|
+
className={`i-lucide-mic text-lg ${
|
|
161
|
+
disabled ? "text-gray-400" : "text-white"
|
|
162
|
+
}`}
|
|
163
|
+
/>
|
|
164
|
+
</View>
|
|
165
|
+
</>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default VoiceInput;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// components/MarkdownLite.tsx
|
|
2
|
+
|
|
3
|
+
import {Image, Text, View} from '@tarojs/components'
|
|
4
|
+
import Taro from '@tarojs/taro'
|
|
5
|
+
import MarkdownIt from 'markdown-it'
|
|
6
|
+
import {memo, useMemo} from 'react'
|
|
7
|
+
|
|
8
|
+
const md = new MarkdownIt({
|
|
9
|
+
html: true,
|
|
10
|
+
breaks: true,
|
|
11
|
+
linkify: true
|
|
12
|
+
// 如果需要代码高亮,可以在这里加 highlight 函数
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
function renderToken(token: any, key: string): JSX.Element | null {
|
|
16
|
+
const children = token.children
|
|
17
|
+
? token.children.map((t: any, i: number) => renderToken(t, `${key}-child-${i}`))
|
|
18
|
+
: null
|
|
19
|
+
|
|
20
|
+
switch (token.type) {
|
|
21
|
+
// 标题
|
|
22
|
+
case 'heading_open': {
|
|
23
|
+
const level = token.markup.length
|
|
24
|
+
const hClasses =
|
|
25
|
+
{
|
|
26
|
+
1: 'text-4xl font-bold mt-4 mb-4 border-b border-gray-200 dark:border-gray-700',
|
|
27
|
+
2: 'text-3xl font-bold mt-3 mb-3 border-l-4 border-blue-500',
|
|
28
|
+
3: 'text-2xl font-bold mt-2 mb-2',
|
|
29
|
+
4: 'text-xl font-bold mt-1 mb-1',
|
|
30
|
+
5: 'text-lg font-bold',
|
|
31
|
+
6: 'text-base font-bold'
|
|
32
|
+
}[level] || 'text-lg font-bold mt-2 mb-2'
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View key={key} className={hClasses}>
|
|
36
|
+
{children}
|
|
37
|
+
</View>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 段落
|
|
42
|
+
case 'paragraph_open':
|
|
43
|
+
return (
|
|
44
|
+
<View key={key} className="mb-1 leading-relaxed text-base">
|
|
45
|
+
{children}
|
|
46
|
+
</View>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// 文本节点
|
|
50
|
+
case 'text':
|
|
51
|
+
case 'softbreak':
|
|
52
|
+
case 'hardbreak':
|
|
53
|
+
return <Text key={key}>{token.content}</Text>
|
|
54
|
+
|
|
55
|
+
// 粗体
|
|
56
|
+
case 'strong_open':
|
|
57
|
+
return (
|
|
58
|
+
<Text key={key} className="font-bold text-gray-900 dark:text-gray-100">
|
|
59
|
+
{children}
|
|
60
|
+
</Text>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// 斜体
|
|
64
|
+
case 'em_open':
|
|
65
|
+
return (
|
|
66
|
+
<Text key={key} className="italic text-gray-700 dark:text-gray-300">
|
|
67
|
+
{children}
|
|
68
|
+
</Text>
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// 行内代码
|
|
72
|
+
case 'code_inline':
|
|
73
|
+
return (
|
|
74
|
+
<Text
|
|
75
|
+
key={key}
|
|
76
|
+
className="font-mono text-red-600 dark:text-red-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
|
|
77
|
+
{token.content}
|
|
78
|
+
</Text>
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// 代码块
|
|
82
|
+
case 'fence': {
|
|
83
|
+
const lang = token.info ? token.info.trim() : ''
|
|
84
|
+
return (
|
|
85
|
+
<View
|
|
86
|
+
key={key}
|
|
87
|
+
className="my-2 bg-gray-900 text-gray-200 p-6 rounded-xl overflow-x-auto shadow-lg font-mono text-sm leading-6">
|
|
88
|
+
<Text className="whitespace-pre">{token.content.trim()}</Text>
|
|
89
|
+
{lang && (
|
|
90
|
+
<View className="absolute top-2 right-3 text-xs text-gray-500 bg-gray-800 px-2 py-1 rounded">{lang}</View>
|
|
91
|
+
)}
|
|
92
|
+
</View>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 图片
|
|
97
|
+
case 'image': {
|
|
98
|
+
const src = token.attrs?.find((a: string[]) => a[0] === 'src')?.[1] || ''
|
|
99
|
+
return (
|
|
100
|
+
<Image
|
|
101
|
+
key={key}
|
|
102
|
+
src={src}
|
|
103
|
+
mode="widthFix"
|
|
104
|
+
lazyLoad
|
|
105
|
+
className="rounded-xl shadow-md my-2 max-w-full inline-block"
|
|
106
|
+
onClick={() => src && Taro.previewImage({urls: [src]})}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 链接
|
|
112
|
+
case 'link_open': {
|
|
113
|
+
const href = token.attrs?.find((a: string[]) => a[0] === 'href')?.[1] || ''
|
|
114
|
+
return (
|
|
115
|
+
<Text
|
|
116
|
+
key={key}
|
|
117
|
+
className="text-blue-600 dark:text-blue-400 no-underline hover:underline cursor-pointer"
|
|
118
|
+
onClick={() => {
|
|
119
|
+
if (href) {
|
|
120
|
+
Taro.setClipboardData({data: href}).then(() => Taro.showToast({title: '链接已复制', icon: 'none'}))
|
|
121
|
+
}
|
|
122
|
+
}}>
|
|
123
|
+
{children}
|
|
124
|
+
</Text>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 关闭标签(我们通常不渲染 closing token 的内容,因为 children 已处理)
|
|
129
|
+
case 'heading_close':
|
|
130
|
+
case 'paragraph_close':
|
|
131
|
+
case 'strong_close':
|
|
132
|
+
case 'em_close':
|
|
133
|
+
case 'link_close':
|
|
134
|
+
return null
|
|
135
|
+
|
|
136
|
+
// 其他未处理的 token,直接渲染 children(兜底)
|
|
137
|
+
default:
|
|
138
|
+
return children ? <View key={key}>{children}</View> : null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface MarkdownLiteProps {
|
|
143
|
+
content: string
|
|
144
|
+
className?: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const MarkdownLite = memo(({content, className}: MarkdownLiteProps) => {
|
|
148
|
+
const tokens = useMemo(() => {
|
|
149
|
+
if (!content) return []
|
|
150
|
+
return md.parse(content, {})
|
|
151
|
+
}, [content])
|
|
152
|
+
|
|
153
|
+
return <View className={`text-lg ${className}`}>{tokens.map((token, i) => renderToken(token, `root-${i}`))}</View>
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
export default MarkdownLite
|