Skip to content

Commit 69f88e5

Browse files
Zhiheng-07claude
andauthored
chore(release): v0.2.5 (#1)
- feat: Toast 通知系统重构(id 去重 + 时长分层 + visibleToasts=1) - feat: 未配置 AI 交互统一(顶部横幅 + 卡片文案化) - fix: 多标签页待确认面板不同步(补广播 pendingRemoved) - fix: 背景视频惰性加载(非活跃主题不下载) - docs: 新增 PROJECT_LOG.md / PROGRESS.md 项目文档体系 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c1d9983 commit 69f88e5

19 files changed

Lines changed: 280 additions & 75 deletions

File tree

‎manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "MindTab",
4-
"version": "0.2.4",
4+
"version": "0.2.5",
55
"description": "不用整理,随时找得到",
66
"permissions": [
77
"storage",

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "mindtab-extension",
33
"private": true,
4-
"version": "0.2.4",
4+
"version": "0.2.5",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

‎src/features/bookmarks/components/AddUrlModal.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function AddUrlModal({ open, onClose }: Props) {
3737
const submit = async () => {
3838
const normalized = normalizeUrl(url.trim())
3939
if (!normalized) {
40-
pushToast('warning', '请输入有效的 URL')
40+
pushToast('warning', '请输入有效的 URL', 'add-url-invalid')
4141
return
4242
}
4343
setBusy(true)
@@ -47,19 +47,19 @@ export function AddUrlModal({ open, onClose }: Props) {
4747
payload: { url: normalized, title: title.trim() || undefined },
4848
})
4949
if (res?.ok === false) {
50-
pushToast('error', res.error ?? '添加失败')
50+
pushToast('error', res.error ?? '添加失败', 'add-url-error')
5151
setBusy(false)
5252
return
5353
}
5454
if (res?.duplicated) {
55-
pushToast('info', '该页面已在收藏库中')
55+
pushToast('info', '该页面已在收藏库中', 'add-url-dup')
5656
} else {
57-
pushToast('success', '已添加到收藏库')
57+
pushToast('success', '已添加到收藏库', 'add-url-ok')
5858
void useBookmarkStore.getState().hydrate()
5959
}
6060
onClose()
6161
} catch (e) {
62-
pushToast('error', `添加失败:${(e as Error).message}`)
62+
pushToast('error', `添加失败:${(e as Error).message}`, 'add-url-error')
6363
setBusy(false)
6464
}
6565
}

‎src/features/bookmarks/components/BookmarkCard.tsx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
22
import type { Bookmark } from '@/shared/db'
3+
import { useAiConfigured } from '@/shared/lib/aiStatus'
34
import { formatRelativeTime } from '@/shared/lib/timeFormat'
45
import { ContextMenu, type ContextMenuItem } from '@/shared/ui/ContextMenu'
56
import { Favicon } from '@/shared/ui/Favicon'
@@ -15,6 +16,7 @@ interface Props {
1516
}
1617

1718
export function BookmarkCard({ item, onOpen, onTogglePin, onDelete }: Props) {
19+
const aiConfigured = useAiConfigured()
1820
const [hovered, setHovered] = useState(false)
1921
const [showSummary, setShowSummary] = useState(false)
2022
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null)
@@ -209,13 +211,14 @@ export function BookmarkCard({ item, onOpen, onTogglePin, onDelete }: Props) {
209211
bottom: 10,
210212
right: 14,
211213
fontSize: 11,
212-
color:
213-
item.indexStatus === 'pending'
214+
color: aiConfigured
215+
? item.indexStatus === 'pending'
214216
? 'var(--mt-success)'
215-
: 'var(--mt-text-placeholder)',
217+
: 'var(--mt-text-placeholder)'
218+
: 'var(--mt-text-placeholder)',
216219
}}
217220
>
218-
AI整理中
221+
{aiConfigured ? 'AI整理中' : '未配置AI'}
219222
</span>
220223
)}
221224
</article>

‎src/features/onboarding/components/ImportGuideCard.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ export function ImportGuideCard() {
3636
const html = await file.text()
3737
const { written, reassigned, skipped } = await run(html)
3838
if (written > 0) {
39-
pushToast('success', `导入:新增 ${written} 条,重新归类 ${reassigned}`)
39+
pushToast('success', `导入:新增 ${written} 条,重新归类 ${reassigned}`, 'import-guide')
4040
} else if (skipped > 0) {
41-
pushToast('info', `全部跳过:${skipped} 条已存在`)
41+
pushToast('info', `全部跳过:${skipped} 条已存在`, 'import-guide')
4242
} else {
43-
pushToast('error', '未解析到可导入条目')
43+
pushToast('error', '未解析到可导入条目', 'import-no-items')
4444
}
4545
} catch (e) {
46-
pushToast('error', `导入失败:${(e as Error).message}`)
46+
pushToast('error', `导入失败:${(e as Error).message}`, 'import-guide-fail')
4747
} finally {
4848
void dismiss()
4949
}

‎src/features/pending/components/PendingCard.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState } from 'react'
22
import type { IndexStatus, PendingBookmark } from '@/shared/db'
3+
import { useAiConfigured } from '@/shared/lib/aiStatus'
34
import { Favicon } from '@/shared/ui/Favicon'
45
import { IconCheck, IconClose, IconEdit } from '@/shared/ui/icons'
56
const MAX_TITLE = 20
@@ -110,12 +111,21 @@ export function PendingCard({ item, onRename, onDelete, onConfirm }: Props) {
110111
}
111112

112113
function IndexBadge({ status }: { status: IndexStatus }) {
114+
const aiConfigured = useAiConfigured()
113115
if (status === 'done') return <span />
114116
const color =
115-
status === 'pending' ? 'var(--mt-success)' : 'var(--mt-text-placeholder)'
117+
status === 'pending' && !aiConfigured
118+
? 'var(--mt-text-placeholder)'
119+
: status === 'pending'
120+
? 'var(--mt-success)'
121+
: 'var(--mt-text-placeholder)'
116122
return (
117123
<span style={{ fontSize: 11, color }}>
118-
{status === 'pending' ? 'AI整理中' : '索引中'}
124+
{status === 'pending' && !aiConfigured
125+
? '未配置AI'
126+
: status === 'pending'
127+
? 'AI整理中'
128+
: '索引中'}
119129
</span>
120130
)
121131
}

‎src/features/pending/components/PendingPanel.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ export function PendingPanel() {
2626
await useBookmarkStore.getState().hydrate()
2727
// 广播给其它打开的 newtab
2828
broadcast({ type: MSG.bookmarkAdded, payload: { id: bookmark.id } })
29-
pushToast('success', '已归入收藏库')
29+
broadcast({ type: MSG.pendingRemoved, payload: { id } })
30+
pushToast('success', '已归入收藏库', 'pending-confirm')
3031
} catch (e) {
31-
pushToast('error', `归入失败:${(e as Error).message}`)
32+
pushToast('error', `归入失败:${(e as Error).message}`, 'pending-confirm-fail')
3233
}
3334
}
3435

‎src/features/pending/store.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function subscribeRuntimeMessages(): void {
5555
switch (msg.type) {
5656
case MSG.pendingAdded:
5757
debouncedHydratePending()
58-
pushToast('success', '已添加到待确认')
58+
pushToast('success', '已添加到待确认', 'pending-add')
5959
break
6060
case MSG.pendingRemoved:
6161
debouncedHydratePending()

‎src/features/settings/components/AiSettingsSection.tsx‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -131,21 +131,21 @@ export function AiSettingsSection() {
131131
const baseUrl = effectiveBaseUrl.trim()
132132
const mdl = model.trim()
133133
if (!key) {
134-
pushToast('error', '请先填写 API Key')
134+
pushToast('error', '请先填写 API Key', 'ai-key-empty')
135135
return null
136136
}
137137
if (provider === 'custom' && !baseUrl) {
138-
pushToast('error', '请填写 Base URL')
138+
pushToast('error', '请填写 Base URL', 'ai-baseurl-empty')
139139
return null
140140
}
141141
if (!mdl) {
142-
pushToast('error', '请填写模型名称')
142+
pushToast('error', '请填写模型名称', 'ai-model-empty')
143143
return null
144144
}
145145
try {
146146
void new URL(baseUrl)
147147
} catch {
148-
pushToast('error', 'Base URL 格式无效,请填写完整地址(含 https://)')
148+
pushToast('error', 'Base URL 格式无效,请填写完整地址(含 https://)', 'ai-baseurl-invalid')
149149
return null
150150
}
151151
return { provider, apiKey: key, baseUrl, model: mdl }
@@ -210,17 +210,17 @@ export function AiSettingsSection() {
210210
const key = apiKey.trim()
211211
const baseUrl = effectiveBaseUrl.trim()
212212
if (!key) {
213-
pushToast('error', '请先填写 API Key')
213+
pushToast('error', '请先填写 API Key', 'ai-key-empty')
214214
return null
215215
}
216216
if (!baseUrl) {
217-
pushToast('error', '请填写 Base URL')
217+
pushToast('error', '请填写 Base URL', 'ai-baseurl-empty')
218218
return null
219219
}
220220
try {
221221
void new URL(baseUrl)
222222
} catch {
223-
pushToast('error', 'Base URL 格式无效,请填写完整地址(含 https://)')
223+
pushToast('error', 'Base URL 格式无效,请填写完整地址(含 https://)', 'ai-baseurl-invalid')
224224
return null
225225
}
226226
return { provider, apiKey: key, baseUrl, model: model.trim() || '' }
@@ -234,18 +234,18 @@ export function AiSettingsSection() {
234234
ensureHostPermission(draft.baseUrl)
235235
.then(async (granted) => {
236236
if (!granted) {
237-
pushToast('error', `未授权访问 ${hostOf(draft.baseUrl)} 域名,无法拉取模型列表`)
237+
pushToast('error', `未授权访问 ${hostOf(draft.baseUrl)} 域名,无法拉取模型列表`, 'ai-fetch-perm')
238238
return
239239
}
240240
const models = await fetchModels(draft)
241241
setFetchedModels(models)
242242
// 清空过滤词,立即展示完整拉取结果
243243
setModelFilter('')
244244
setShowModelSuggestions(true)
245-
pushToast('success', `获取到 ${models.length} 个可用模型`)
245+
pushToast('success', `获取到 ${models.length} 个可用模型`, 'ai-fetch-models')
246246
})
247247
.catch((e) => {
248-
pushToast('error', `获取模型列表失败:${truncateError((e as Error).message)}`)
248+
pushToast('error', `获取模型列表失败:${truncateError((e as Error).message)}`, 'ai-fetch-fail')
249249
})
250250
.finally(() => setFetchingModels(false))
251251
}
@@ -258,15 +258,15 @@ export function AiSettingsSection() {
258258
ensureHostPermission(draft.baseUrl)
259259
.then(async (granted) => {
260260
if (!granted) {
261-
pushToast('error', `未授权访问 ${hostOf(draft.baseUrl)} 域名,无法调用 AI 服务`)
261+
pushToast('error', `未授权访问 ${hostOf(draft.baseUrl)} 域名,无法调用 AI 服务`, 'ai-save-perm')
262262
return
263263
}
264264
await setAiConfig(draft)
265265
setSaved(draft)
266-
pushToast('success', 'AI 配置已保存')
266+
pushToast('success', 'AI 配置已保存', 'ai-config-save')
267267
})
268268
.catch((e) => {
269-
pushToast('error', `保存失败:${truncateError((e as Error).message)}`)
269+
pushToast('error', `保存失败:${truncateError((e as Error).message)}`, 'ai-save-fail')
270270
})
271271
.finally(() => setSaving(false))
272272
}

‎src/features/settings/components/SettingsModal.tsx‎

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,21 @@ import { Button } from '@/shared/ui/button'
2222
import { Row, Section, Segmented } from './SettingsPrimitives'
2323
import { AiSettingsSection } from './AiSettingsSection'
2424

25-
const VERSION = '0.2.4'
25+
const VERSION = '0.2.5'
2626

2727
interface Props {
2828
open: boolean
2929
onClose: () => void
30+
/** 打开时自动滚动到 AI 服务区域并聚焦 API Key 输入框 */
31+
focusAi?: boolean
3032
}
3133

32-
export function SettingsModal({ open, onClose }: Props) {
34+
export function SettingsModal({ open, onClose, focusAi }: Props) {
3335
const mode = useThemeStore((s) => s.mode)
3436
const setMode = useThemeStore((s) => s.setMode)
3537
const pushToast = useToastStore((s) => s.pushToast)
3638
const confirm = useConfirm()
39+
const aiSectionRef = useRef<HTMLDivElement>(null)
3740

3841
const [improve, setImprove] = useState(true)
3942
const fileRef = useRef<HTMLInputElement>(null)
@@ -55,6 +58,24 @@ export function SettingsModal({ open, onClose }: Props) {
5558
return () => document.removeEventListener('keydown', esc)
5659
}, [open, onClose])
5760

61+
// focusAi:打开时自动滚动到 AI 服务区域并聚焦 API Key 输入框
62+
useEffect(() => {
63+
if (!focusAi || !open || !aiSectionRef.current) return
64+
const el = aiSectionRef.current
65+
const scrollTimer = setTimeout(() => {
66+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
67+
}, 100)
68+
// 等滚动完成后聚焦 API Key 输入框
69+
const focusTimer = setTimeout(() => {
70+
const input = el.querySelector('input[type="password"]')
71+
if (input instanceof HTMLElement) input.focus()
72+
}, 400)
73+
return () => {
74+
clearTimeout(scrollTimer)
75+
clearTimeout(focusTimer)
76+
}
77+
}, [focusAi, open])
78+
5879
const handleClearAll = async () => {
5980
const ok = await confirm({
6081
title: '清空所有数据?',
@@ -68,9 +89,9 @@ export function SettingsModal({ open, onClose }: Props) {
6889
await clearAllData()
6990
await useBookmarkStore.getState().hydrate()
7091
await usePendingStore.getState().hydratePending()
71-
pushToast('success', '已清空所有数据')
92+
pushToast('success', '已清空所有数据', 'clear-all')
7293
} catch (e) {
73-
pushToast('error', `清空失败:${(e as Error).message}`)
94+
pushToast('error', `清空失败:${(e as Error).message}`, 'clear-fail')
7495
}
7596
}
7697

@@ -84,13 +105,13 @@ export function SettingsModal({ open, onClose }: Props) {
84105
try {
85106
const items = await getPendingIndexBookmarks()
86107
if (items.length === 0) {
87-
pushToast('info', '所有书签均已索引')
108+
pushToast('info', '所有书签均已索引', 'reindex-none')
88109
return
89110
}
90111
await chrome.runtime.sendMessage({ type: MSG.retryIndex })
91-
pushToast('success', `已发起 ${items.length} 条重新索引`)
112+
pushToast('success', `已发起 ${items.length} 条重新索引`, 'reindex-start')
92113
} catch (e) {
93-
pushToast('error', `重新索引失败:${(e as Error).message}`)
114+
pushToast('error', `重新索引失败:${(e as Error).message}`, 'reindex-fail')
94115
} finally {
95116
setReindexBusy(false)
96117
}
@@ -108,14 +129,15 @@ export function SettingsModal({ open, onClose }: Props) {
108129
pushToast(
109130
'success',
110131
`导入:新增 ${result.written} 条,重新归类 ${result.reassigned}`,
132+
'import-html'
111133
)
112134
} else if (result.skipped > 0) {
113-
pushToast('info', `全部跳过:${result.skipped} 条已存在`)
135+
pushToast('info', `全部跳过:${result.skipped} 条已存在`, 'import-html')
114136
} else {
115-
pushToast('error', '未解析到可导入条目')
137+
pushToast('error', '未解析到可导入条目', 'import-no-items')
116138
}
117139
} catch (e) {
118-
pushToast('error', `导入失败:${(e as Error).message}`)
140+
pushToast('error', `导入失败:${(e as Error).message}`, 'import-fail')
119141
} finally {
120142
setImportBusy(false)
121143
}
@@ -126,12 +148,12 @@ export function SettingsModal({ open, onClose }: Props) {
126148
try {
127149
const count = await exportBookmarksHtml()
128150
if (count > 0) {
129-
pushToast('success', `已导出 ${count} 条书签`)
151+
pushToast('success', `已导出 ${count} 条书签`, 'export-ok')
130152
} else {
131-
pushToast('info', '暂无书签可导出')
153+
pushToast('info', '暂无书签可导出', 'export-none')
132154
}
133155
} catch (e) {
134-
pushToast('error', `导出失败:${(e as Error).message}`)
156+
pushToast('error', `导出失败:${(e as Error).message}`, 'export-fail')
135157
} finally {
136158
setExportBusy(false)
137159
}
@@ -223,7 +245,9 @@ export function SettingsModal({ open, onClose }: Props) {
223245
</Section>
224246

225247
{/* ── AI 服务 ── */}
226-
<AiSettingsSection />
248+
<div ref={aiSectionRef}>
249+
<AiSettingsSection />
250+
</div>
227251

228252
{/* ── 数据 ── */}
229253
<Section title="数据管理">

0 commit comments

Comments
 (0)