增加模型选择功能,支持不同 Agent 模型
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user