增加模型选择功能,支持不同 Agent 模型
Build Push and Deploy / docker-image (push) Successful in 1m3s
Build Push and Deploy / deploy-fallback-log (push) Has been skipped

This commit is contained in:
2026-05-13 18:12:22 +08:00
parent a4486e3d89
commit 8058b7b859
5 changed files with 192 additions and 42 deletions
+167 -40
View File
@@ -7,8 +7,11 @@ import {
Box, Box,
Chip, Chip,
Collapse, Collapse,
FormControl,
IconButton, IconButton,
MenuItem,
Paper, Paper,
Select,
Stack, Stack,
TextField, TextField,
Typography, Typography,
@@ -21,6 +24,9 @@ import MicRounded from "@mui/icons-material/MicRounded";
import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded";
import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded"; import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded";
import AttachFileRounded from "@mui/icons-material/AttachFileRounded"; import AttachFileRounded from "@mui/icons-material/AttachFileRounded";
import BoltRounded from "@mui/icons-material/BoltRounded";
import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded";
import type { AgentModel } from "@/lib/chatStream";
type AgentComposerProps = { type AgentComposerProps = {
input: string; input: string;
@@ -36,6 +42,8 @@ type AgentComposerProps = {
onStartListening: () => void; onStartListening: () => void;
onStopListening: () => void; onStopListening: () => void;
onPresetSelect: (prompt: string) => void; onPresetSelect: (prompt: string) => void;
selectedModel: AgentModel;
onModelChange: (model: AgentModel) => void;
}; };
export const AgentComposer = ({ export const AgentComposer = ({
@@ -52,6 +60,8 @@ export const AgentComposer = ({
onStartListening, onStartListening,
onStopListening, onStopListening,
onPresetSelect, onPresetSelect,
selectedModel,
onModelChange,
}: AgentComposerProps) => { }: AgentComposerProps) => {
const theme = useTheme(); const theme = useTheme();
const canSend = input.trim().length > 0 && !isStreaming && !isHydrating; const canSend = input.trim().length > 0 && !isStreaming && !isHydrating;
@@ -213,46 +223,163 @@ export const AgentComposer = ({
) : null} ) : null}
</Stack> </Stack>
<AnimatePresence mode="wait"> <Stack direction="row" spacing={1} alignItems="center">
{isStreaming ? ( <FormControl size="small" sx={{ minWidth: 80 }}>
<motion.div key="stop" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}> <Select
<IconButton value={selectedModel}
onClick={onAbort} onChange={(event) => onModelChange(event.target.value as AgentModel)}
aria-label="停止生成" disabled={isHydrating || isStreaming}
size="small" aria-label="模型选择"
sx={{ renderValue={(val) => (
bgcolor: "error.main", <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
color: "#fff", {val === "deepseek/deepseek-v4-flash" ? (
width: 40, <BoltRounded sx={{ fontSize: 18, color: "inherit", transition: "color 0.2s" }} />
height: 40, ) : (
boxShadow: `0 4px 12px ${alpha(theme.palette.error.main, 0.4)}`, <AutoAwesomeRounded sx={{ fontSize: 16, color: "inherit", transition: "color 0.2s" }} />
"&:hover": { bgcolor: "error.dark" }, )}
}} <Typography sx={{ fontSize: "0.8rem", fontWeight: 600, color: "inherit", transition: "color 0.2s" }}>
> {val === "deepseek/deepseek-v4-flash" ? "快速" : "专家"}
<StopRounded /> </Typography>
</IconButton> </Box>
</motion.div> )}
) : ( MenuProps={{
<motion.div key="send" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}> anchorOrigin: { vertical: "top", horizontal: "center" },
<IconButton transformOrigin: { vertical: "bottom", horizontal: "center" },
disabled={!canSend} sx: { zIndex: (theme) => theme.zIndex.modal + 110 },
onClick={onSend} PaperProps: {
aria-label="发送" sx: {
size="small" mb: 1.5,
sx={{ width: 230,
bgcolor: canSend ? "#00acc1" : alpha("#fff", 0.5), borderRadius: 4,
color: canSend ? "#fff" : "action.disabled", bgcolor: alpha("#fff", 0.85),
width: 40, backdropFilter: "blur(24px)",
height: 40, border: `1px solid ${alpha("#fff", 0.9)}`,
boxShadow: canSend ? `0 6px 16px ${alpha("#00acc1", 0.4)}` : "none", boxShadow: `0 -12px 40px ${alpha("#000", 0.08)}, 0 0 0 1px ${alpha("#00acc1", 0.05)} inset`,
"&:hover": { bgcolor: canSend ? "#00838f" : alpha("#fff", 0.5) }, "& .MuiList-root": {
}} p: 1,
> },
<SendRounded sx={{ ml: 0.35 }} /> "& .MuiMenuItem-root": {
</IconButton> px: 1.5,
</motion.div> py: 1.2,
)} mb: 0.5,
</AnimatePresence> "&:last-child": { mb: 0 },
borderRadius: 3,
alignItems: "flex-start",
transition: "all 0.2s ease",
"&:hover": {
bgcolor: alpha("#000", 0.03),
},
"&.Mui-selected": {
bgcolor: alpha("#00acc1", 0.08),
"&:hover": {
bgcolor: alpha("#00acc1", 0.12),
},
"& .title": { color: "#00838f" },
"& .icon": { color: "#00acc1" },
}
}
}
}
}}
sx={{
height: 36,
borderRadius: "18px",
bgcolor: "transparent",
color: "text.secondary",
transition: "all 0.2s ease",
".MuiOutlinedInput-notchedOutline": {
border: "none",
},
".MuiSelect-select": {
py: 0,
pl: 1,
pr: "28px !important",
display: "flex",
alignItems: "center",
},
"&:hover, &:has(.MuiSelect-select[aria-expanded=\"true\"])": {
bgcolor: alpha("#000", 0.06),
color: "text.primary",
".MuiSelect-icon": {
color: "text.primary",
}
},
".MuiSelect-icon": {
color: "text.secondary",
right: 4,
transition: "color 0.2s ease",
}
}}
>
<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 }}
/>
<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>
</Select>
</FormControl>
<AnimatePresence mode="wait">
{isStreaming ? (
<motion.div key="stop" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
<IconButton
onClick={onAbort}
aria-label="停止生成"
size="small"
sx={{
bgcolor: "error.main",
color: "#fff",
width: 40,
height: 40,
boxShadow: `0 4px 12px ${alpha(theme.palette.error.main, 0.4)}`,
"&:hover": { bgcolor: "error.dark" },
}}
>
<StopRounded />
</IconButton>
</motion.div>
) : (
<motion.div key="send" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
<IconButton
disabled={!canSend}
onClick={onSend}
aria-label="发送"
size="small"
sx={{
bgcolor: canSend ? "#00acc1" : alpha("#fff", 0.5),
color: canSend ? "#fff" : "action.disabled",
width: 40,
height: 40,
boxShadow: canSend ? `0 6px 16px ${alpha("#00acc1", 0.4)}` : "none",
"&:hover": { bgcolor: canSend ? "#00838f" : alpha("#fff", 0.5) },
}}
>
<SendRounded sx={{ ml: 0.35 }} />
</IconButton>
</motion.div>
)}
</AnimatePresence>
</Stack>
</Stack> </Stack>
</Paper> </Paper>
</motion.div> </motion.div>
+7
View File
@@ -3,6 +3,7 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { Box, Drawer, alpha, useTheme } from "@mui/material"; import { Box, Drawer, alpha, useTheme } from "@mui/material";
import type { AgentModel } from "@/lib/chatStream";
import { AgentComposer } from "./AgentComposer"; import { AgentComposer } from "./AgentComposer";
import { AgentHeader } from "./AgentHeader"; import { AgentHeader } from "./AgentHeader";
import { AgentHistoryPanel } from "./AgentHistoryPanel"; import { AgentHistoryPanel } from "./AgentHistoryPanel";
@@ -19,6 +20,9 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
const [width, setWidth] = useState(520); const [width, setWidth] = useState(520);
const [isResizing, setIsResizing] = useState(false); const [isResizing, setIsResizing] = useState(false);
const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState<AgentModel>(
"deepseek/deepseek-v4-pro",
);
const bottomRef = useRef<HTMLDivElement>(null); const bottomRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
@@ -65,6 +69,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
} = useAgentChatSession({ } = useAgentChatSession({
onToolCall: handleToolCall, onToolCall: handleToolCall,
onBeforeSend: stopListening, onBeforeSend: stopListening,
getModel: () => selectedModel,
}); });
useEffect(() => { useEffect(() => {
@@ -298,6 +303,8 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
onStartListening={startListening} onStartListening={startListening}
onStopListening={stopListening} onStopListening={stopListening}
onPresetSelect={handlePresetPromptSelect} onPresetSelect={handlePresetPromptSelect}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
/> />
</Box> </Box>
</Box> </Box>
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { abortAgentChat, forkAgentChat, streamAgentChat } from "@/lib/chatStream"; import { abortAgentChat, forkAgentChat, streamAgentChat } from "@/lib/chatStream";
import type { StreamEvent } from "@/lib/chatStream"; import type { AgentModel, StreamEvent } from "@/lib/chatStream";
import type { import type {
AgentArtifact, AgentArtifact,
BranchGroup, BranchGroup,
@@ -37,6 +37,7 @@ type UseAgentChatSessionOptions = {
}, },
) => void; ) => void;
onBeforeSend?: () => void; onBeforeSend?: () => void;
getModel?: () => AgentModel;
}; };
type PromptRunOptions = { type PromptRunOptions = {
@@ -137,6 +138,7 @@ const messagesEqual = (left: Message[], right: Message[]) =>
export const useAgentChatSession = ({ export const useAgentChatSession = ({
onToolCall, onToolCall,
onBeforeSend, onBeforeSend,
getModel,
}: UseAgentChatSessionOptions) => { }: UseAgentChatSessionOptions) => {
const storageSessionIdRef = useRef<string | undefined>(undefined); const storageSessionIdRef = useRef<string | undefined>(undefined);
const hydrationCompletedRef = useRef(false); const hydrationCompletedRef = useRef(false);
@@ -317,6 +319,7 @@ export const useAgentChatSession = ({
await streamAgentChat({ await streamAgentChat({
message: prompt, message: prompt,
sessionId: sessionIdOverride ?? sessionIdRef.current, sessionId: sessionIdOverride ?? sessionIdRef.current,
model: getModel?.(),
signal: controller.signal, signal: controller.signal,
onEvent: (event) => { onEvent: (event) => {
if ("sessionId" in event && event.sessionId && event.sessionId !== sessionIdRef.current) { if ("sessionId" in event && event.sessionId && event.sessionId !== sessionIdRef.current) {
@@ -448,7 +451,7 @@ export const useAgentChatSession = ({
setIsStreaming(false); setIsStreaming(false);
} }
}, },
[appendArtifact, isHydrating, isStreaming, messages, onBeforeSend, onToolCall], [appendArtifact, getModel, isHydrating, isStreaming, messages, onBeforeSend, onToolCall],
); );
const abort = useCallback(() => { const abort = useCallback(() => {
+6
View File
@@ -51,6 +51,7 @@ describe("streamAgentChat", () => {
await streamAgentChat({ await streamAgentChat({
message: "hi", message: "hi",
model: "deepseek/deepseek-v4-pro",
onEvent: (event) => events.push(event), onEvent: (event) => events.push(event),
}); });
@@ -60,6 +61,11 @@ describe("streamAgentChat", () => {
method: "POST", method: "POST",
projectHeaderMode: "include", projectHeaderMode: "include",
skipAuthRedirect: true, skipAuthRedirect: true,
body: JSON.stringify({
message: "hi",
session_id: undefined,
model: "deepseek/deepseek-v4-pro",
}),
}), }),
); );
+7
View File
@@ -1,6 +1,10 @@
import { apiFetch } from "@/lib/apiFetch"; import { apiFetch } from "@/lib/apiFetch";
import { config } from "@config/config"; import { config } from "@config/config";
export type AgentModel =
| "deepseek/deepseek-v4-flash"
| "deepseek/deepseek-v4-pro";
export type StreamEvent = export type StreamEvent =
| { type: "token"; sessionId: string; content: string } | { type: "token"; sessionId: string; content: string }
| { type: "done"; sessionId: string; totalDurationMs?: number } | { type: "done"; sessionId: string; totalDurationMs?: number }
@@ -35,6 +39,7 @@ export type StreamEvent =
type StreamOptions = { type StreamOptions = {
message: string; message: string;
sessionId?: string; sessionId?: string;
model?: AgentModel;
signal?: AbortSignal; signal?: AbortSignal;
onEvent: (event: StreamEvent) => void; onEvent: (event: StreamEvent) => void;
}; };
@@ -85,6 +90,7 @@ const resolveToolParams = (
export const streamAgentChat = async ({ export const streamAgentChat = async ({
message, message,
sessionId, sessionId,
model,
signal, signal,
onEvent, onEvent,
}: StreamOptions) => { }: StreamOptions) => {
@@ -102,6 +108,7 @@ export const streamAgentChat = async ({
body: JSON.stringify({ body: JSON.stringify({
message, message,
session_id: sessionId, session_id: sessionId,
model,
}), }),
projectHeaderMode: "include", projectHeaderMode: "include",
userHeaderMode: "include", userHeaderMode: "include",