@@ -2,15 +2,14 @@
import { LogOut , Menu , Settings2 , Shield } from "lucide-react" ;
import Link from "next/link" ;
import { App , Button , Drawer , Form , Input , Modal } from "antd" ;
import { App , Button , Drawer , Form , Input , Modal , Segmented } from "antd" ;
import { useConfigDialogStore } from "@/stores/use-config-dialog-store" ;
import { ModelPicker } from "@/components/model-picker" ;
import { GitHubLink } from "@/components/github-link" ;
import { UserStatusActions } from "@/components/user-status-actions" ;
import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler" ;
import { VersionReleaseModal } from "@/components/version-release-modal" ;
import type { AiConfig } from "@/lib/ai -config" ;
import { useConfigStore , type AiConfig } from "@/stores/use -config-store " ;
import { navigationTools , type NavigationToolSlug } from "@/lib/navigation-tools" ;
import { fetchImageModels } from "@/services/api/image" ;
import { useThemeStore } from "@/stores/use-theme-store" ;
@@ -30,20 +29,33 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
const [ loadingModels , setLoadingModels ] = useState ( false ) ;
const [ mobileNavOpen , setMobileNavOpen ] = useState ( false ) ;
const appVersion = process . env . NEXT_PUBLIC_APP_VERSION || "dev" ;
const isConfigOpen = useConfigDialogStore ( ( state ) = > state . isOpen ) ;
const shouldPromptContinue = useConfigDialogStore ( ( state ) = > state . shouldPromptContinue ) ;
const openConfigDialog = useConfigDialogStore ( ( state ) = > state . openConfigDialog ) ;
const setConfigDialogOpen = useConfigDialogStore ( ( state ) = > state . setConfigDialogOpen ) ;
const clearPromptContinue = useConfigDialogStore ( ( state ) = > state . clearPromptContinue ) ;
const isConfigOpen = useConfigStore ( ( state ) = > state . isConfigOpen ) ;
const shouldPromptContinue = useConfigStore ( ( state ) = > state . shouldPromptContinue ) ;
const openConfigDialog = useConfigStore ( ( state ) = > state . openConfigDialog ) ;
const setConfigDialogOpen = useConfigStore ( ( state ) = > state . setConfigDialogOpen ) ;
const clearPromptContinue = useConfigStore ( ( state ) = > state . clearPromptContinue ) ;
const theme = useThemeStore ( ( state ) = > state . theme ) ;
const setTheme = useThemeStore ( ( state ) = > state . setTheme ) ;
const user = useUserStore ( ( state ) = > state . user ) ;
const isReady = useUserStore ( ( state ) = > state . isReady ) ;
const logout = useUserStore ( ( state ) = > state . clearSession ) ;
const publicSettings = useConfigStore ( ( state ) = > state . publicSettings ) ;
const modelChannel = publicSettings ? . modelChannel ;
const allowCustomChannel = modelChannel ? . allowCustomChannel === true ;
const effectiveMode = allowCustomChannel ? config . channelMode : "remote" ;
const modelConfig = effectiveMode === "remote" && modelChannel ? {
. . . config ,
models : modelChannel.availableModels ,
imageModel : modelChannel.availableModels.includes ( config . imageModel ) ? config.imageModel : modelChannel.defaultImageModel || modelChannel . defaultModel ,
textModel : modelChannel.availableModels.includes ( config . textModel ) ? config.textModel : modelChannel.defaultTextModel || modelChannel . defaultModel ,
systemPrompt : modelChannel.systemPrompt ,
} : config ;
const finishConfig = ( ) = > {
setConfigDialogOpen ( false ) ;
if ( ! config . baseUrl . trim ( ) || ! config . imageModel . trim ( ) || ! config . textModel . trim ( ) || ! config . apiKey . trim ( ) ) return ;
if ( effectiveMode === "local" && ( ! config . baseUrl . trim ( ) || ! config . apiKey . trim ( ) ) ) return ;
if ( ! modelConfig . imageModel . trim ( ) || ! modelConfig . textModel . trim ( ) ) return ;
if ( ! allowCustomChannel && config . channelMode !== "remote" ) onConfigChange ( "channelMode" , "remote" ) ;
if ( shouldPromptContinue ) {
message . success ( "配置已保存,请继续刚才的请求" ) ;
} else {
@@ -52,6 +64,7 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
clearPromptContinue ( ) ;
} ;
const refreshModels = async ( ) = > {
if ( effectiveMode === "remote" ) return ;
if ( ! config . baseUrl . trim ( ) || ! config . apiKey . trim ( ) ) {
message . error ( "请先填写 Base URL 和 API Key" ) ;
return ;
@@ -197,35 +210,60 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
< Modal
title = { < div > < div className = "text-lg font-semibold" > 配 置 < / div > < div className = "mt-1 text-xs font-normal text-stone-500" > 模 型 和 密 钥 < / div > < / div > }
open = { isConfigOpen }
width = { 560 }
width = { 760 }
centered
onCancel = { ( ) = > setConfigDialogOpen ( false ) }
footer = { < Button type = "primary" size = "large" onClick = { finishConfig } > 完 成 < / Button > }
>
< div className = "pt-1" >
< Form layout = "vertical" requiredMark = { false } size = "large" >
< Form.Item label = "Base URL" className = "mb-4" >
< Input value = { config . baseUrl } onChange = { ( event ) = > onConfigChange ( "baseUrl" , event . target . value ) } / >
< / Form.Item >
< Form.Item label = "API Key" className = "mb-4" >
< Input.Password value = { config . apiKey } onChange = { ( event ) = > onConfigChange ( "apiKey" , event . target . value ) } / >
< / Form.Item >
< div className = "mb-4 flex items-center justify-between gap-3 rounded-lg border border-stone-200 p-3 dark:border-stone-800" >
< div className = "min-w-0" >
< div className = "text-sm font-medium" > 模 型 列 表 < / div >
< div className = "mt-1 text-xs text-stone-500" > 当 前 已 保 存 { config . models . length } 个 模 型 < / div >
{ allowCustomChannel ? (
< Form.Item label = "渠道模式" className = "mb-4" >
< Segmented
block
value = { effectiveMode }
onChange = { ( value ) = > onConfigChange ( "channelMode" , value as AiConfig [ "channelMode" ] ) }
options = { [ { label : "云端渠道" , value : "remote" } , { label : "本地直连" , value : "local" } ] }
/ >
< / Form.Item >
) : null }
{ effectiveMode === "local" ? (
< >
< div className = "grid gap-4 md:grid-cols-2" >
< Form.Item label = "Base URL" className = "mb-4" >
< Input value = { config . baseUrl } onChange = { ( event ) = > onConfigChange ( "baseUrl" , event . target . value ) } / >
< / Form.Item >
< Form.Item label = "API Key" className = "mb-4" >
< Input.Password value = { config . apiKey } onChange = { ( event ) = > onConfigChange ( "apiKey" , event . target . value ) } / >
< / Form.Item >
< / div >
< div className = "mb-4 flex items-center justify-between gap-3 rounded-lg border border-stone-200 px-3 py-2 dark:border-stone-800" >
< div className = "min-w-0" >
< div className = "text-sm font-medium" > 模 型 列 表 < / div >
< div className = "mt-1 text-xs text-stone-500" > 当 前 已 保 存 { config . models . length } 个 模 型 < / div >
< / div >
< Button loading = { loadingModels } onClick = { ( ) = > void refreshModels ( ) } > 拉 取 模 型 列 表 < / Button >
< / div >
< / >
) : (
< div className = "mb-4 rounded-lg border border-stone-200 p-3 text-sm text-stone-500 dark:border-stone-800" >
< div className = "font-medium text-stone-900 dark:text-stone-100" > 云 端 渠 道 < / div >
< div className = "mt-1" > 由 系 统 后 台 渠 道 转 发 请 求 , 当 前 可 用 { modelChannel ? . availableModels . length || 0 } 个 模 型 。 < / div >
< / div >
< Button loading = { loadingModels } onClick = { ( ) = > void refreshModels ( ) } > 拉 取 模 型 列 表 < / Button >
) }
< div className = "grid gap-4 md:grid-cols-2" >
< Form.Item label = "默认生图模型" className = "mb-4" >
< ModelPicker config = { modelConfig } value = { modelConfig . imageModel } onChange = { ( model ) = > onConfigChange ( "imageModel" , model ) } fullWidth / >
< / Form.Item >
< Form.Item label = "默认文本模型" className = "mb-4" >
< ModelPicker config = { modelConfig } value = { modelConfig . textModel } onChange = { ( model ) = > onConfigChange ( "textModel" , model ) } fullWidth / >
< / Form.Item >
< / div >
< Form.Item label = "默认生图模型" className = "mb-4" >
< ModelPicker config = { config } value = { config . imageModel } onChange = { ( model ) = > onConfigChange ( "imageModel" , model ) } fullWidth / >
< / Form.Item >
< Form.Item label = "默认文本模型" className = "mb-0" >
< ModelPicker config = { config } value = { config . textModel } onChange = { ( model ) = > onConfigChange ( "textModel" , model ) } fullWidth / >
< / Form.Item >
< Form.Item label = "系统提示词" className = "mb-0" >
< Input.TextArea rows = { 4 } value = { config . systemPrompt } placeholder = "例如:你是一位擅长电影感写实摄影的视觉导演。" onChange = { ( event ) = > onConfigChange ( "systemPrompt" , event . target . value ) } / >
< / Form.Item >
{ effectiveMode === "local" ? (
< Form.Item label = "系统提示词" className = "mb-0" >
< Input.TextArea rows = { 3 } value = { config . systemPrompt } placeholder = "例如:你是一位擅长电影感写实摄影的视觉导演。" onChange = { ( event ) = > onConfigChange ( "systemPrompt" , event . target . value ) } / >
< / Form.Item >
) : null }
< / Form >
< / div >
< / Modal >