Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions src/renderer/src/components/notebook/NotePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,19 +283,33 @@ export default function NotePanel(): ReactElement {
if (notebookId) {
try {
// 立即开始生成(异步)
window.api.mindmap.generate(notebookId).then((result) => {
if (result.success) {
// 生成完成后重新加载列表
loadItems(notebookId)
}
})
void window.api.mindmap
.generate(notebookId)
.then((result) => {
if (!result.success) throw new Error(result.error || t('ui:unknownError'))
return loadItems(notebookId)
})
.catch((error) => {
toast.error(
t('ui:generationFailed', {
error: error instanceof Error ? error.message : String(error)
}),
{ duration: 10000 }
)
})

// 等待一小段时间后刷新列表,以显示"正在生成"的 item
setTimeout(() => {
loadItems(notebookId)
}, 500)
} catch (error) {
console.error('[NotePanel] Failed to generate mind map:', error)
toast.error(
t('ui:generationFailed', {
error: error instanceof Error ? error.message : String(error)
}),
{ duration: 10000 }
)
}
}
}
Expand All @@ -322,10 +336,16 @@ export default function NotePanel(): ReactElement {
customPrompt: params.customPrompt
})
.then((result) => {
if (result.success) {
// 生成完成后重新加载列表
loadItems(notebookId)
}
if (!result.success) throw new Error(result.error || t('ui:unknownError'))
return loadItems(notebookId)
})
.catch((error) => {
toast.error(
t('ui:generationFailed', {
error: error instanceof Error ? error.message : String(error)
}),
{ duration: 10000 }
)
})

// 等待一小段时间后刷新列表,以显示"正在生成"的 item
Expand All @@ -334,6 +354,10 @@ export default function NotePanel(): ReactElement {
}, 500)
} catch (error) {
console.error('[NotePanel] Failed to start quiz:', error)
toast.error(
t('ui:generationFailed', { error: error instanceof Error ? error.message : String(error) }),
{ duration: 10000 }
)
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/renderer/src/components/notebook/ProcessPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { memo, useState, useEffect, useRef, ReactElement } from 'react'
import { toast } from 'sonner'
import {
Send,
StopCircle,
Expand Down Expand Up @@ -97,7 +98,12 @@ function ProcessPanel({
const handleSend = (): void => {
if (!canSend) return

sendMessage(currentSession.id, input.trim())
void sendMessage(currentSession.id, input.trim()).catch((error) => {
toast.error(
t('generationFailed', { error: error instanceof Error ? error.message : String(error) }),
{ duration: 10000 }
)
})
setInput('')
// The reader may have scrolled up meanwhile: their own message must always
// land in view, regardless of the follow state.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react'
import { toast } from 'sonner'
import { useTranslation } from 'react-i18next'
import { useAnkiStore } from '../../../store/ankiStore'
import {
Expand Down Expand Up @@ -63,6 +64,12 @@ export default function AnkiConfigDialog({
customPrompt: customPrompt || undefined
}).catch((error) => {
console.error('Failed to generate anki cards:', error)
toast.error(
t('generateFailed', {
error: error instanceof Error ? error.message : String(error)
}),
{ duration: 10000 }
)
})

// 等待一小段时间后触发回调,以显示"正在生成"的 item
Expand Down
33 changes: 25 additions & 8 deletions src/renderer/src/components/notebook/chat/MessageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,33 @@ function MessageItem({ message }: MessageItemProps): ReactElement {
const recoveryNotebookId = message.notebookId ?? currentNotebook?.id
const handleContinue = (): void => {
if (!recoveryNotebookId) return
void continueMessage(recoveryNotebookId, message.id).then((result) => {
// The main process refuses past the continuation bound (#179); say why the
// button did nothing instead of leaving it silent.
if (!result.started && result.reason === 'continuation-limit') {
toast.error(t('ui:continueLimitReached'))
}
})
void continueMessage(recoveryNotebookId, message.id)
.then((result) => {
// The main process refuses past the continuation bound (#179); say why the
// button did nothing instead of leaving it silent.
if (!result.started && result.reason === 'continuation-limit') {
toast.error(t('ui:continueLimitReached'))
}
})
.catch((error) => {
toast.error(
t('ui:generationFailed', {
error: error instanceof Error ? error.message : String(error)
}),
{ duration: 10000 }
)
})
}
const handleRetry = (): void => {
if (recoveryNotebookId) void retryMessage(recoveryNotebookId, message.id)
if (recoveryNotebookId)
void retryMessage(recoveryNotebookId, message.id).catch((error) => {
toast.error(
t('ui:generationFailed', {
error: error instanceof Error ? error.message : String(error)
}),
{ duration: 10000 }
)
})
}

const handleCopy = async () => {
Expand Down
15 changes: 11 additions & 4 deletions src/renderer/src/components/notebook/mindmap/NodeDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useMindMapStore } from '../../../store/mindmapStore'
import { useChatStore } from '../../../store/chatStore'
import ReactMarkdown from 'react-markdown'
import { toast } from 'sonner'

export default function NodeDetailPanel() {
const { t } = useTranslation('notebook')
Expand All @@ -23,10 +24,16 @@ export default function NodeDetailPanel() {
if (!currentSession) return

const question = `基于以下内容,请详细解释:\n\n${chunkContent}`
await sendMessage(currentSession.id, question)

// 关闭思维导图Dialog,聚焦到对话面板
setDialogOpen(false)
try {
await sendMessage(currentSession.id, question)
// 关闭思维导图Dialog,聚焦到对话面板
setDialogOpen(false)
} catch (error) {
toast.error(
t('ui:generationFailed', { error: error instanceof Error ? error.message : String(error) }),
{ duration: 10000 }
)
}
}

return (
Expand Down
134 changes: 107 additions & 27 deletions src/renderer/src/components/pages/OnboardingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import type { ConnectionMap } from '../../../../shared/types'
import ModelsSettings from '../settings/ModelsSettings'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useOnboardingStore } from '../../store/onboardingStore'
Expand All @@ -20,19 +22,58 @@ export default function OnboardingPage() {
const { language, changeLanguage } = useI18nStore()
const [selectedLanguage, setSelectedLanguage] = useState<Language>(language)
const [isCompleting, setIsCompleting] = useState(false)
const [step, setStep] = useState(0)
const [connections, setConnections] = useState<ConnectionMap | null>(null)
const [error, setError] = useState('')

const handleComplete = async () => {
useEffect(() => {
window.api.connections
.getAll()
.then(setConnections)
.catch((err) => {
setError(err instanceof Error ? err.message : String(err))
})
}, [])

const handleNext = async (skip = false) => {
setIsCompleting(true)
await changeLanguage(selectedLanguage)
await completeOnboarding()
navigate('/')
setError('')
try {
if (step === 0) {
await changeLanguage(selectedLanguage)
} else if (!skip && connections) {
const capability = step === 1 ? 'embedding' : 'chat'
const connection = connections[capability]
if (connection) {
if (!connection.baseUrl.trim() || !connection.modelId.trim()) {
setError(t('onboardingIncompleteConnection'))
return
}
await window.api.connections.save(capability, connection)
} else {
await window.api.connections.remove(capability)
}
}
if (step < 2) {
// Reload persisted connections so skipped edits do not leak into later steps.
setConnections(await window.api.connections.getAll())
setStep(step + 1)
} else {
await completeOnboarding()
navigate('/')
}
} catch (err) {
setError(err instanceof Error ? err.message : String(err))
} finally {
setIsCompleting(false)
}
}

const selectedLangName = languages.find((l) => l.code === selectedLanguage)?.name

return (
<div className="flex items-center justify-center min-h-screen bg-surface-sunken">
<div className="w-full max-w-md px-6 text-center">
<div className="w-full max-w-xl px-6 py-8 text-center">
{/* Logo */}
<div className="mb-8">
<img src={logo} alt="KnowNote" className="w-24 h-24 mx-auto mb-4" />
Expand All @@ -44,29 +85,68 @@ export default function OnboardingPage() {
<p className="text-sm text-muted-foreground">{t('onboardingTagline')}</p>
</div>

{/* 语言选择 */}
<div className="mb-6">
<Select
value={selectedLanguage}
onValueChange={(value) => setSelectedLanguage(value as Language)}
<p className="text-sm text-muted-foreground mb-4">
{t('onboardingStep', { step: step + 1, total: 3 })}
</p>
{step === 0 ? (
<div className="mb-6">
<Select
value={selectedLanguage}
onValueChange={(value) => setSelectedLanguage(value as Language)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('selectLanguage')}>{selectedLangName}</SelectValue>
</SelectTrigger>
<SelectContent>
{languages.map((lang) => (
<SelectItem key={lang.code} value={lang.code}>
{lang.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : (
<div className="mb-6 text-left">
<p className="text-sm text-muted-foreground mb-4">
{t(step === 1 ? 'onboardingEmbeddingHint' : 'onboardingChatHint')}
</p>
{connections && (
<ModelsSettings
key={step}
capability={step === 1 ? 'embedding' : 'chat'}
connections={connections}
onConnectionsChange={setConnections}
/>
)}
</div>
)}
{error && (
<p role="alert" className="text-sm text-destructive mb-4">
{error}
</p>
)}
<div className="flex gap-3">
{step > 0 && (
<Button
variant="outline"
onClick={() => void handleNext(true)}
disabled={isCompleting || !connections}
>
{t('onboardingSkip')}
</Button>
)}
<Button
size="lg"
onClick={() => void handleNext()}
disabled={isCompleting || !connections}
className="flex-1"
>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('selectLanguage')}>{selectedLangName}</SelectValue>
</SelectTrigger>
<SelectContent>
{languages.map((lang) => (
<SelectItem key={lang.code} value={lang.code}>
{lang.name}
</SelectItem>
))}
</SelectContent>
</Select>
{isCompleting
? t('onboardingStarting')
: t(step === 2 ? 'onboardingGetStarted' : 'onboardingNext')}
</Button>
</div>

{/* 确认按钮 */}
<Button size="lg" onClick={handleComplete} disabled={isCompleting} className="w-full">
{isCompleting ? t('onboardingStarting') : t('onboardingGetStarted')}
</Button>
</div>
</div>
)
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/components/settings/EmbeddingSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export default function EmbeddingSettings({
if (!result.success) {
setError(result.error || 'Download failed')
}
} catch (err) {
setError(err instanceof Error ? err.message : String(err))
} finally {
setBusy(false)
await refreshStatus()
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/locales/en-US/anki.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@
"validatingData": "Validating data",
"savingCards": "Saving cards",
"completed": "Completed"
}
},
"generateFailed": "Card generation failed: {{error}}"
}
9 changes: 8 additions & 1 deletion src/renderer/src/locales/en-US/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,12 @@
"searchMatchingText": "Matching text",
"searchRelatedPassages": "Related passages",
"searchPage": "p. {{page}}",
"continueLimitReached": "This answer has reached its continuation limit."
"continueLimitReached": "This answer has reached its continuation limit.",
"onboardingStep": "Step {{step}} of {{total}}",
"onboardingIncompleteConnection": "Enter a base URL and model name, or choose Set up later.",
"onboardingEmbeddingHint": "Download the built-in embedding model to index and search documents without an API key. You can also configure a remote model or set it up later in Settings.",
"onboardingChatHint": "A chat model powers answers, cards and quizzes; the embedding model cannot do this. Configure a compatible service or local model now, or later in Settings.",
"onboardingSkip": "Set up later",
"onboardingNext": "Save and continue",
"generationFailed": "Generation failed: {{error}}"
}
3 changes: 2 additions & 1 deletion src/renderer/src/locales/zh-CN/anki.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@
"validatingData": "验证数据",
"savingCards": "保存卡片",
"completed": "完成"
}
},
"generateFailed": "卡片生成失败:{{error}}"
}
9 changes: 8 additions & 1 deletion src/renderer/src/locales/zh-CN/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,12 @@
"searchMatchingText": "匹配的文本",
"searchRelatedPassages": "相关段落",
"searchPage": "第 {{page}} 页",
"continueLimitReached": "这条回答已达到继续次数上限。"
"continueLimitReached": "这条回答已达到继续次数上限。",
"onboardingStep": "第 {{step}} / {{total}} 步",
"onboardingIncompleteConnection": "请填写服务地址和模型名称,或选择稍后配置。",
"onboardingEmbeddingHint": "下载内置嵌入模型后即可索引和检索文档,无需 API Key。也可以配置远程模型,或稍后在设置中完成。",
"onboardingChatHint": "对话模型用于问答、卡片和测验生成,不包含在嵌入模型中。可配置兼容服务或本地模型;稍后也可在设置中完成。",
"onboardingSkip": "稍后配置",
"onboardingNext": "保存并继续",
"generationFailed": "生成失败:{{error}}"
}
Loading
Loading