Skip to content

Commit f8e26f7

Browse files
authored
feat: show ai toolbox hint (#1105)
1 parent 0c26896 commit f8e26f7

File tree

2 files changed

+196
-2
lines changed

2 files changed

+196
-2
lines changed

apps/web/src/components/ai/SidebarAIToolbar.vue

Lines changed: 191 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,20 @@ const { aiDialogVisible, aiImageDialogVisible } = storeToRefs(displayStore)
1515
const { toggleAIDialog, toggleAIImageDialog } = displayStore
1616
1717
const store = useStore()
18-
const { editor } = storeToRefs(store)
18+
const { editor, hasShownAIToolboxHint } = storeToRefs(store)
1919
2020
// 工具栏状态:false=默认(只显示贴边栏), true=展开(显示AI图标)
2121
const isExpanded = ref(false) // 默认收起状态
2222
2323
// AI 工具箱相关状态
2424
const toolBoxVisible = ref(false)
2525
26+
// 是否显示选中文本提示动画
27+
const showSelectionHint = ref(false)
28+
let selectionHintTimer: NodeJS.Timeout | null = null
29+
let selectionCheckInterval: NodeJS.Timeout | null = null
30+
let lastSelectedText = ``
31+
2632
// 检查选中文本的函数
2733
function getSelectedText() {
2834
try {
@@ -36,6 +42,43 @@ function getSelectedText() {
3642
}
3743
}
3844
45+
// 检查并更新选中文本提示
46+
function checkSelectionAndUpdateHint() {
47+
// 如果已经显示过提示,就不再显示
48+
if (hasShownAIToolboxHint.value) {
49+
return
50+
}
51+
52+
const selected = getSelectedText()
53+
54+
// 如果选中状态发生变化
55+
if (selected !== lastSelectedText) {
56+
lastSelectedText = selected
57+
58+
// 清除之前的定时器
59+
if (selectionHintTimer) {
60+
clearTimeout(selectionHintTimer)
61+
selectionHintTimer = null
62+
}
63+
64+
// 如果有选中文本且工具栏未展开
65+
if (selected && !isExpanded.value) {
66+
showSelectionHint.value = true
67+
68+
// 标记已经显示过提示
69+
hasShownAIToolboxHint.value = true
70+
71+
// 3秒后自动隐藏提示
72+
selectionHintTimer = setTimeout(() => {
73+
showSelectionHint.value = false
74+
}, 3000)
75+
}
76+
else {
77+
showSelectionHint.value = false
78+
}
79+
}
80+
}
81+
3982
// 动态计算是否有选中文本
4083
const hasSelectedText = computed(() => {
4184
if (!editor.value || !isExpanded.value)
@@ -51,6 +94,15 @@ const currentSelectedText = computed(() => {
5194
// 切换展开/收起状态
5295
function toggleExpanded() {
5396
isExpanded.value = !isExpanded.value
97+
98+
// 展开后隐藏提示
99+
if (isExpanded.value) {
100+
showSelectionHint.value = false
101+
if (selectionHintTimer) {
102+
clearTimeout(selectionHintTimer)
103+
selectionHintTimer = null
104+
}
105+
}
54106
}
55107
56108
// 打开AI助手
@@ -70,6 +122,11 @@ function openAIToolBox() {
70122
71123
// 监听编辑区点击,自动收起工具栏
72124
onMounted(() => {
125+
// 启动定时检查选中文本
126+
selectionCheckInterval = setInterval(() => {
127+
checkSelectionAndUpdateHint()
128+
}, 300) // 每300ms检查一次
129+
73130
const handleInteraction = (e: Event) => {
74131
// 只有在展开状态才需要处理
75132
if (!isExpanded.value)
@@ -103,7 +160,6 @@ onMounted(() => {
103160
const shouldNotCollapse = excludeSelectors.some(selector => target.closest(selector))
104161
105162
if (!shouldNotCollapse) {
106-
console.log(`Interaction outside excluded areas, collapsing toolbar. Event type: ${e.type}`)
107163
isExpanded.value = false
108164
}
109165
}
@@ -115,6 +171,18 @@ onMounted(() => {
115171
onUnmounted(() => {
116172
document.removeEventListener(`click`, handleInteraction, true)
117173
document.removeEventListener(`touchstart`, handleInteraction, true)
174+
175+
// 清理定时器
176+
if (selectionHintTimer) {
177+
clearTimeout(selectionHintTimer)
178+
selectionHintTimer = null
179+
}
180+
181+
// 清理轮询
182+
if (selectionCheckInterval) {
183+
clearInterval(selectionCheckInterval)
184+
selectionCheckInterval = null
185+
}
118186
})
119187
})
120188
</script>
@@ -129,10 +197,27 @@ onMounted(() => {
129197
<div
130198
v-if="!isExpanded"
131199
class="w-5 h-16 bg-gradient-to-b from-blue-500/90 to-purple-500/90 hover:from-blue-600/95 hover:to-purple-600/95 dark:from-blue-400/90 dark:to-purple-400/90 dark:hover:from-blue-500/95 dark:hover:to-purple-500/95 backdrop-blur-lg border-l border-y border-blue-300/50 dark:border-blue-600/50 cursor-pointer transition-all duration-200 flex items-center justify-center rounded-l-lg shadow-lg group utools-sidebar-edge"
200+
:class="{ 'animate-pulse-hint': showSelectionHint }"
132201
title="展开AI工具栏"
133202
@click="toggleExpanded"
134203
>
135204
<Settings2 class="h-4 w-4 text-white drop-shadow-sm group-hover:scale-110 transition-transform duration-200" />
205+
206+
<!-- 选中文本提示气泡 -->
207+
<Transition name="hint-fade">
208+
<div
209+
v-if="showSelectionHint"
210+
class="hint-bubble absolute right-full mr-3 px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white text-sm font-medium rounded-lg shadow-xl whitespace-nowrap pointer-events-none animate-bounce-gentle z-50"
211+
style="top: 50%; transform: translateY(-50%);"
212+
>
213+
<div class="relative flex items-center gap-2">
214+
<Wand2 class="h-4 w-4" />
215+
<span>点击打开 AI 工具箱</span>
216+
<!-- 箭头 -->
217+
<div class="hint-arrow absolute top-1/2 -right-2 -translate-y-1/2 w-0 h-0 border-t-[6px] border-b-[6px] border-l-[6px] border-transparent border-l-purple-500" />
218+
</div>
219+
</div>
220+
</Transition>
136221
</div>
137222

138223
<!-- 展开状态:显示AI图标 -->
@@ -236,6 +321,60 @@ onMounted(() => {
236321
overflow: visible;
237322
}
238323
324+
/* 选中文本时的脉冲动画 */
325+
@keyframes pulse-hint {
326+
0%,
327+
100% {
328+
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
329+
}
330+
50% {
331+
box-shadow: 0 0 0 8px rgba(59, 130, 246, 0);
332+
}
333+
}
334+
335+
.animate-pulse-hint {
336+
animation: pulse-hint 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
337+
}
338+
339+
/* 提示气泡的轻微弹跳动画 */
340+
@keyframes bounce-gentle {
341+
0%,
342+
100% {
343+
transform: translateX(0);
344+
}
345+
50% {
346+
transform: translateX(-4px);
347+
}
348+
}
349+
350+
.animate-bounce-gentle {
351+
animation: bounce-gentle 1s ease-in-out infinite;
352+
}
353+
354+
/* 提示气泡淡入淡出过渡 */
355+
.hint-fade-enter-active,
356+
.hint-fade-leave-active {
357+
transition:
358+
opacity 0.3s ease,
359+
transform 0.3s ease;
360+
}
361+
362+
.hint-fade-enter-from {
363+
opacity: 0;
364+
transform: translateX(8px);
365+
}
366+
367+
.hint-fade-leave-to {
368+
opacity: 0;
369+
transform: translateX(8px);
370+
}
371+
372+
.hint-fade-enter-to,
373+
.hint-fade-leave-from {
374+
opacity: 1;
375+
transform: translateX(0);
376+
}
377+
239378
/* 响应式调整 */
240379
@media (max-width: 768px) {
241380
.editor-ai-toolbar {
@@ -330,6 +469,56 @@ onMounted(() => {
330469
color: rgb(0 0 0) !important;
331470
}
332471
472+
/* uTools 模式下提示气泡使用黑白风格 */
473+
.is-utools .hint-bubble {
474+
background: rgb(0 0 0 / 0.9) !important;
475+
background-image: none !important;
476+
color: rgb(255 255 255) !important;
477+
}
478+
479+
.is-utools.dark .hint-bubble {
480+
background: rgb(255 255 255 / 0.9) !important;
481+
background-image: none !important;
482+
color: rgb(0 0 0) !important;
483+
}
484+
485+
.is-utools .hint-arrow {
486+
border-left-color: rgb(0 0 0 / 0.9) !important;
487+
}
488+
489+
.is-utools.dark .hint-arrow {
490+
border-left-color: rgb(255 255 255 / 0.9) !important;
491+
}
492+
493+
/* uTools 模式下脉冲动画使用黑白风格 */
494+
@keyframes pulse-hint-utools {
495+
0%,
496+
100% {
497+
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7);
498+
}
499+
50% {
500+
box-shadow: 0 0 0 8px rgba(0, 0, 0, 0);
501+
}
502+
}
503+
504+
@keyframes pulse-hint-utools-dark {
505+
0%,
506+
100% {
507+
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
508+
}
509+
50% {
510+
box-shadow: 0 0 0 8px rgba(255, 255, 255, 0);
511+
}
512+
}
513+
514+
.is-utools .animate-pulse-hint {
515+
animation: pulse-hint-utools 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
516+
}
517+
518+
.is-utools.dark .animate-pulse-hint {
519+
animation: pulse-hint-utools-dark 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
520+
}
521+
333522
/* uTools 模式下 AI 按钮使用黑白风格 */
334523
.is-utools .utools-ai-button {
335524
background: rgb(0 0 0 / 0.85) !important;

apps/web/src/stores/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export const useStore = defineStore(`store`, () => {
7979
const isCountStatus = useStorage(`isCountStatus`, defaultStyleConfig.isCountStatus)
8080
const toggleCountStatus = useToggle(isCountStatus)
8181

82+
// 是否已经显示过AI工具箱选中文本提示
83+
const hasShownAIToolboxHint = useStorage(`hasShownAIToolboxHint`, false)
84+
8285
// 是否开启段落首行缩进
8386
const isUseIndent = useStorage(addPrefix(`use_indent`), false)
8487
const toggleUseIndent = useToggle(isUseIndent)
@@ -748,6 +751,8 @@ export const useStore = defineStore(`store`, () => {
748751
isCountStatus,
749752
countStatusChanged,
750753

754+
hasShownAIToolboxHint,
755+
751756
output,
752757
editor,
753758
cssEditor,

0 commit comments

Comments
 (0)