feat(chat): load model options from backend
Build Push and Deploy / docker-image (push) Successful in 1m6s
Build Push and Deploy / deploy-fallback-log (push) Has been skipped

This commit is contained in:
2026-06-10 19:50:43 +08:00
parent 1e872ca873
commit 7d2ae87e39
7 changed files with 220 additions and 41 deletions
+43 -32
View File
@@ -28,6 +28,7 @@ import BoltRounded from "@mui/icons-material/BoltRounded";
import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded";
import VerifiedUserRounded from "@mui/icons-material/VerifiedUserRounded";
import AdminPanelSettingsRounded from "@mui/icons-material/AdminPanelSettingsRounded";
import type { AgentModelOption } from "@/lib/chatModels";
import type { AgentApprovalMode, AgentModel } from "@/lib/chatStream";
export type AgentComposerHandle = {
@@ -48,12 +49,23 @@ type AgentComposerProps = {
onAbort: () => void;
onStartListening: () => void;
onStopListening: () => void;
selectedModel: AgentModel;
modelOptions: AgentModelOption[];
selectedModel?: AgentModel;
onModelChange: (model: AgentModel) => void;
approvalMode: AgentApprovalMode;
onApprovalModeChange: (mode: AgentApprovalMode) => void;
};
const renderModelIcon = (
icon: AgentModelOption["icon"] | undefined,
props?: React.ComponentProps<typeof BoltRounded>,
) =>
icon === "bolt" ? (
<BoltRounded {...props} />
) : (
<AutoAwesomeRounded {...props} />
);
export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposerProps>(function AgentComposer({
isHydrating = false,
isStreaming,
@@ -64,6 +76,7 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
onAbort,
onStartListening,
onStopListening,
modelOptions,
selectedModel,
onModelChange,
approvalMode,
@@ -74,6 +87,7 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
const [input, setInput] = React.useState("");
const [isPresetOpen, setIsPresetOpen] = React.useState(false);
const canSend = input.trim().length > 0 && !isStreaming && !isHydrating;
const selectedModelOption = modelOptions.find((model) => model.id === selectedModel);
React.useImperativeHandle(
ref,
@@ -347,19 +361,21 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
<Stack direction="row" spacing={1} alignItems="center">
<FormControl size="small" sx={{ minWidth: 80 }}>
<Select
value={selectedModel}
value={selectedModel ?? ""}
onChange={(event) => onModelChange(event.target.value as AgentModel)}
disabled={isHydrating || isStreaming}
disabled={isHydrating || isStreaming || modelOptions.length === 0}
aria-label="模型选择"
renderValue={(val) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{val === "deepseek/deepseek-v4-flash" ? (
<BoltRounded sx={{ fontSize: 18, color: "inherit", transition: "color 0.2s" }} />
) : (
<AutoAwesomeRounded sx={{ fontSize: 16, color: "inherit", transition: "color 0.2s" }} />
)}
renderValue={() => (
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
{renderModelIcon(selectedModelOption?.icon, {
sx: {
fontSize: selectedModelOption?.icon === "bolt" ? 18 : 16,
color: "inherit",
transition: "color 0.2s",
},
})}
<Typography sx={{ fontSize: "0.8rem", fontWeight: 600, color: "inherit", transition: "color 0.2s" }}>
{val === "deepseek/deepseek-v4-flash" ? "快速" : "专家"}
{selectedModelOption?.label ?? "模型"}
</Typography>
</Box>
)}
@@ -433,30 +449,25 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
}}
>
<Box sx={{ px: 2, py: 1.5, pb: 1, display: "flex", alignItems: "center", gap: 1, pointerEvents: "none" }}>
<Box
component="img"
src="/deepseek-logo.svg"
alt="DeepSeek"
sx={{ width: 16, height: 16, display: "block", flexShrink: 0 }}
/>
<AutoAwesomeRounded sx={{ width: 16, height: 16, color: "text.secondary", flexShrink: 0 }} />
<Typography sx={{ fontSize: "0.75rem", fontWeight: 700, color: "text.secondary", letterSpacing: 0.5 }}>
DEEPSEEK V4
</Typography>
</Box>
<MenuItem value="deepseek/deepseek-v4-flash">
<BoltRounded className="icon" sx={{ mr: 1.5, mt: 0.2, fontSize: 20, color: "text.secondary", transition: "color 0.2s" }} />
<Box>
<Typography className="title" sx={{ fontSize: "0.85rem", fontWeight: 700, color: "text.primary", mb: 0.2, transition: "color 0.2s" }}></Typography>
<Typography sx={{ fontSize: "0.7rem", fontWeight: 500, color: "text.secondary", lineHeight: 1.3 }}></Typography>
</Box>
</MenuItem>
<MenuItem value="deepseek/deepseek-v4-pro">
<AutoAwesomeRounded className="icon" sx={{ mr: 1.5, mt: 0.2, fontSize: 18, color: "text.secondary", transition: "color 0.2s" }} />
<Box>
<Typography className="title" sx={{ fontSize: "0.85rem", fontWeight: 700, color: "text.primary", mb: 0.2, transition: "color 0.2s" }}></Typography>
<Typography sx={{ fontSize: "0.7rem", fontWeight: 500, color: "text.secondary", lineHeight: 1.3 }}></Typography>
</Box>
</MenuItem>
{modelOptions.map((model) => (
<MenuItem key={model.id} value={model.id}>
{renderModelIcon(model.icon, {
className: "icon",
sx: { mr: 1.5, mt: 0.2, fontSize: model.icon === "bolt" ? 20 : 18, color: "text.secondary", transition: "color 0.2s" },
})}
<Box>
<Typography className="title" sx={{ fontSize: "0.85rem", fontWeight: 700, color: "text.primary", mb: 0.2, transition: "color 0.2s" }}>{model.label}</Typography>
{model.description ? (
<Typography sx={{ fontSize: "0.7rem", fontWeight: 500, color: "text.secondary", lineHeight: 1.3 }}>{model.description}</Typography>
) : null}
</Box>
</MenuItem>
))}
</Select>
</FormControl>
+34 -3
View File
@@ -10,6 +10,7 @@ import { Box, Drawer, alpha, useTheme } from "@mui/material";
import { useNotification } from "@refinedev/core";
import { getAccessToken } from "@/lib/authToken";
import { fetchAgentModels, type AgentModelOption } from "@/lib/chatModels";
import type { AgentApprovalMode, AgentModel } from "@/lib/chatStream";
import { useProjectStore } from "@/store/projectStore";
import { AgentComposer, type AgentComposerHandle } from "./AgentComposer";
@@ -28,9 +29,8 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
const [isResizing, setIsResizing] = useState(false);
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [isCheckingAuth, setIsCheckingAuth] = useState(false);
const [selectedModel, setSelectedModel] = useState<AgentModel>(
"deepseek/deepseek-v4-flash",
);
const [modelOptions, setModelOptions] = useState<AgentModelOption[]>([]);
const [selectedModel, setSelectedModel] = useState<AgentModel | undefined>(undefined);
const [approvalMode, setApprovalMode] =
useState<AgentApprovalMode>("request");
@@ -62,6 +62,36 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
isSupported: isSttSupported,
} = useSpeechRecognition(handleSpeechResult);
useEffect(() => {
let cancelled = false;
const loadModels = async () => {
try {
const modelConfig = await fetchAgentModels();
if (cancelled) return;
setModelOptions(modelConfig.models);
setSelectedModel((current) => {
if (current && modelConfig.models.some((model) => model.id === current)) {
return current;
}
return modelConfig.defaultModel;
});
} catch (error) {
console.error("[GlobalChatbox] Failed to load agent models:", error);
if (!cancelled) {
setModelOptions([]);
setSelectedModel(undefined);
}
}
};
void loadModels();
return () => {
cancelled = true;
};
}, []);
const handleToolCall = useAgentToolActions();
const {
messages,
@@ -372,6 +402,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
onAbort={abort}
onStartListening={startListening}
onStopListening={stopListening}
modelOptions={modelOptions}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
approvalMode={approvalMode}
@@ -11,7 +11,7 @@ export type UseAgentChatSessionOptions = {
},
) => void;
onBeforeSend?: () => void;
getModel?: () => AgentModel;
getModel?: () => AgentModel | undefined;
getApprovalMode?: () => AgentApprovalMode;
};