Просмотр исходного кода

fix: show clear model-required prompt when no model is configured

Mochocyang 3 месяцев назад
Родитель
Сommit
b3be3922f9

+ 31 - 12
src/components/sources/outline-chat-panel.tsx

@@ -214,6 +214,21 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
   const activeConv = conversations.find((c) => c.id === activeConversationId)
   const activeMessages = activeConv?.messages ?? []
 
+  const hasAvailableModels = useMemo(() => {
+    for (const key of Object.keys(providerConfigs)) {
+      const config = providerConfigs[key]
+      if (key.startsWith("custom-")) {
+        if (config.enabled === false) continue
+      } else {
+        if (config.enabled !== true) continue
+      }
+      if (config.savedModels && config.savedModels.length > 0) {
+        return true
+      }
+    }
+    return false
+  }, [providerConfigs])
+
   const [inputValue, setInputValue] = useState("")
   const [localModelId, setLocalModelId] = useState(activeConv?.modelId ?? "")
 
@@ -271,7 +286,7 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
     }
     if (!hasUsableLlm(effectiveLlmConfig, providerConfigs)) {
       const convId = activeConversationId ?? createConversation()
-      addMessage(convId, { id: crypto.randomUUID(), role: "assistant", content: "请先在设置中配置可用的AI模型(API Key 和模型名称),或在AI会话中选择一个模型。" })
+      addMessage(convId, { id: crypto.randomUUID(), role: "assistant", content: "请先在设置中配置并选择一个可用的 AI 模型,或在下方模型选择器中选择模型后再试。" })
       return
     }
 
@@ -433,7 +448,7 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
       effectiveLlmConfig = resolveModelConfig(activeConv.modelId, effectiveLlmConfig, providerConfigs)
     }
     if (!hasUsableLlm(effectiveLlmConfig, providerConfigs)) {
-      addMessage(activeConversationId, { id: crypto.randomUUID(), role: "assistant", content: "请先在设置中配置可用的AI模型(API Key 和模型名称),或在AI会话中选择一个模型。" })
+      addMessage(activeConversationId, { id: crypto.randomUUID(), role: "assistant", content: "请先在设置中配置并选择一个可用的 AI 模型,或在下方模型选择器中选择模型后再试。" })
       return
     }
 
@@ -647,16 +662,20 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
               <div className="flex items-center gap-2 flex-nowrap overflow-x-auto">
                 <ChatDockControls />
               </div>
-              <ChatModelSelector
-                value={localModelId}
-                onChange={(value) => {
-                  setLocalModelId(value)
-                  if (activeConversationId) {
-                    setConversationModel(activeConversationId, value)
-                  }
-                }}
-                disabled={isStreaming}
-              />
+              {hasAvailableModels ? (
+                <ChatModelSelector
+                  value={localModelId}
+                  onChange={(value) => {
+                    setLocalModelId(value)
+                    if (activeConversationId) {
+                      setConversationModel(activeConversationId, value)
+                    }
+                  }}
+                  disabled={isStreaming}
+                />
+              ) : (
+                <p className="text-xs text-destructive">请先在「设置 → 大语言模型」中添加并启用一个模型。</p>
+              )}
             </div>
           </TooltipProvider>
         }

+ 26 - 5
src/components/sources/outline-generator-dialog.tsx

@@ -102,6 +102,7 @@ export function OutlineGeneratorDialog({
   const { t } = useTranslation()
   const project = useWikiStore((s) => s.project)
   const llmConfig = useWikiStore((s) => s.llmConfig)
+  const providerConfigs = useWikiStore((s) => s.providerConfigs)
   const dataVersion = useWikiStore((s) => s.dataVersion)
   const selectedFile = useWikiStore((s) => s.selectedFile)
   const createTask = useOutlineGenerationStore((s: OutlineGenerationState) => s.createTask)
@@ -143,6 +144,21 @@ export function OutlineGeneratorDialog({
     [channel, mainGenre],
   )
 
+  const hasAvailableModels = useMemo(() => {
+    for (const key of Object.keys(providerConfigs)) {
+      const config = providerConfigs[key]
+      if (key.startsWith("custom-")) {
+        if (config.enabled === false) continue
+      } else {
+        if (config.enabled !== true) continue
+      }
+      if (config.savedModels && config.savedModels.length > 0) {
+        return true
+      }
+    }
+    return false
+  }, [providerConfigs])
+
   useEffect(() => {
     const genres = getMainGenresByChannel(channel)
     const firstGenre = genres[0]?.key ?? ""
@@ -420,6 +436,7 @@ export function OutlineGeneratorDialog({
         displayTitle: currentSection?.title ?? t("novel.outlineGenerator.refineTitle"),
         writeMode: refineWriteMode,
         targetPath: refineWriteMode === "appendCurrent" ? selectedFile : null,
+        modelId: modelId || undefined,
       })
       updateTask(taskId, {
         status: "generating",
@@ -861,11 +878,15 @@ export function OutlineGeneratorDialog({
 
         <DialogFooter className="items-center gap-2 sm:justify-between">
           <div className="flex items-center gap-2">
-            <ChatModelSelector
-              value={modelId}
-              onChange={setModelId}
-              disabled={generating || taskGenerating}
-            />
+            {hasAvailableModels ? (
+              <ChatModelSelector
+                value={modelId}
+                onChange={setModelId}
+                disabled={generating || taskGenerating}
+              />
+            ) : (
+              <p className="text-xs text-destructive">请先在「设置 → 大语言模型」中添加并启用一个模型。</p>
+            )}
           </div>
           <div className="flex items-center gap-2">
             <Button

+ 7 - 0
src/lib/novel/deep-outline-generation.ts

@@ -1,4 +1,6 @@
 import type { LlmConfig } from "@/stores/wiki-store"
+import { useWikiStore } from "@/stores/wiki-store"
+import { hasUsableLlm } from "@/lib/has-usable-llm"
 import { streamChat, type ChatMessage, type RequestOverrides, type StreamCallbacks } from "@/lib/llm-client"
 
 export interface DeepOutlineGenerationInput {
@@ -40,6 +42,11 @@ export async function runDeepOutlineGeneration(
   deps: DeepOutlineGenerationDeps = defaultDeps,
   signal?: AbortSignal,
 ): Promise<DeepOutlineGenerationResult> {
+  const { providerConfigs } = useWikiStore.getState()
+  if (!hasUsableLlm(input.llmConfig, providerConfigs)) {
+    throw new Error("请先在设置中配置并选择一个可用的 AI 模型,或在大纲对话中选择模型后再试。")
+  }
+
   const safeContext = ensureString(input.context)
   const safeUserRequest = ensureString(input.userRequest)
   const history = formatRecentHistory(input.historyMessages ?? [])

+ 16 - 2
src/lib/novel/outline-generation.ts

@@ -10,6 +10,7 @@ import { useOutlineGenerationStore } from "@/stores/outline-generation-store"
 import { useImportProgressStore } from "@/stores/import-progress-store"
 import type { LlmConfig } from "@/stores/wiki-store"
 import { useWikiStore } from "@/stores/wiki-store"
+import { hasUsableLlm } from "@/lib/has-usable-llm"
 import { ingestOutline } from "./chapter-ingest"
 import { buildContextPack, type ContextPack } from "./context-engine"
 import { resolveModelConfig } from "@/lib/novel/model-resolver"
@@ -453,6 +454,10 @@ export async function runOutlineGenerationTask(taskId: string, llmConfig: LlmCon
     ? resolveModelConfig(task.modelId, llmConfig, providerConfigs)
     : llmConfig
 
+  if (!hasUsableLlm(effectiveLlmConfig, providerConfigs)) {
+    throw new Error("请先在设置中配置并选择一个可用的 AI 模型,或在大纲弹窗中选择模型后再试。")
+  }
+
   const abortController = new AbortController()
   const progressTaskId = useImportProgressStore.getState().startTask({
     projectPath: task.projectPath,
@@ -498,6 +503,15 @@ export async function runOutlineRefinementTask(taskId: string, llmConfig: LlmCon
   const task = useOutlineGenerationStore.getState().tasks.find((item) => item.id === taskId)
   if (!task) return
 
+  const { providerConfigs } = useWikiStore.getState()
+  const effectiveLlmConfig = task.modelId
+    ? resolveModelConfig(task.modelId, llmConfig, providerConfigs)
+    : llmConfig
+
+  if (!hasUsableLlm(effectiveLlmConfig, providerConfigs)) {
+    throw new Error("请先在设置中配置并选择一个可用的 AI 模型,或在大纲弹窗中选择模型后再试。")
+  }
+
   const abortController = new AbortController()
   const progressTaskId = useImportProgressStore.getState().startTask({
     projectPath: task.projectPath,
@@ -513,7 +527,7 @@ export async function runOutlineRefinementTask(taskId: string, llmConfig: LlmCon
     if (task.selectedSectionKey) {
       outlinePath = await generateOutlineRefinementSectionFile(
         task.projectPath,
-        llmConfig,
+        effectiveLlmConfig,
         task.userRequest,
         task.selectedSectionKey as OutlineSectionGenerationKey,
         {
@@ -525,7 +539,7 @@ export async function runOutlineRefinementTask(taskId: string, llmConfig: LlmCon
     } else {
       const result = await generateOutlineRefinementFiles(
         task.projectPath,
-        llmConfig,
+        effectiveLlmConfig,
         task.userRequest,
         {
           mode: (task.writeMode as OutlineRefinementWriteMode | null) ?? undefined,