ui: 支持模型选择并透传到聊天接口;
All checks were successful
CI / build (push) Successful in 2m39s

Made-with: Cursor
This commit is contained in:
2026-04-17 02:25:45 +08:00
parent 056c3e648f
commit 6579578a62
2 changed files with 43 additions and 7 deletions

View File

@@ -9,6 +9,8 @@ export interface ChatMessagePayload {
interface StreamOptions {
messages: ChatMessagePayload[];
/** 与后端 OpenAI 兼容字段一致,不传则由服务端默认模型处理 */
model?: string;
onToken: (token: string) => void;
signal?: AbortSignal;
timeoutMs?: number;
@@ -84,12 +86,15 @@ export async function streamQwenChat(options: StreamOptions): Promise<void> {
const timeoutSignal = createTimeoutAbortSignal(options.timeoutMs ?? 90000);
const mergedSignal = mergeAbortSignals([options.signal, timeoutSignal]);
await fetchEventSource(`${API_BASE_URL}/api/qwen/chat/stream`, {
await fetchEventSource(`${API_BASE_URL}/api/client/v1/chat/completions/stream`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ messages: options.messages }),
body: JSON.stringify({
messages: options.messages,
...(options.model ? { model: options.model } : {}),
}),
signal: mergedSignal,
openWhenHidden: true,
async onopen(response) {

View File

@@ -11,7 +11,7 @@ import {
ThunderboltOutlined,
UserOutlined,
} from "@ant-design/icons";
import { Button, Collapse, Drawer, Input, Layout, Typography } from "antd";
import { Button, Collapse, Drawer, Input, Layout, Select, Typography } from "antd";
import { isAbortLikeError, streamQwenChat, type ChatMessagePayload } from "../api/qwenChat";
import StreamMessage from "../components/StreamMessage";
@@ -37,6 +37,14 @@ const STREAM_TIMEOUT_MS = 90000;
const MAX_STREAM_RETRY = 1;
const CONTEXT_WINDOW_SIZE = 8;
/** 展示名与请求 model 字段;需与后端实际支持的模型 id 一致 */
const DEFAULT_QWEN_MODEL = "qwen3.5-flash";
const QWEN_MODEL_OPTIONS: { value: string; label: string }[] = [
{ value: "qwen3-max", label: "Qwen3-Max" },
{ value: "qwen3.6-plus", label: "Qwen3.6-Plus" },
{ value: DEFAULT_QWEN_MODEL, label: "Qwen3.5-Flash" },
];
export default function HomePage() {
const [collapsed, setCollapsed] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
@@ -47,6 +55,7 @@ export default function HomePage() {
const [isSending, setIsSending] = useState(false);
const [deepThink, setDeepThink] = useState(false);
const [smartSearch, setSmartSearch] = useState(false);
const [selectedModel, setSelectedModel] = useState(DEFAULT_QWEN_MODEL);
const messageListRef = useRef<HTMLDivElement | null>(null);
const abortRef = useRef<AbortController | null>(null);
@@ -136,6 +145,7 @@ export default function HomePage() {
try {
await streamQwenChat({
messages: retryMessages,
model: selectedModel,
signal: controller.signal,
timeoutMs: STREAM_TIMEOUT_MS,
onToken: (token) => {
@@ -318,9 +328,16 @@ export default function HomePage() {
className="h-6 w-6 shrink-0 object-contain md:hidden"
decoding="async"
/>
<Typography.Text className="text-[15px] font-medium text-neutral-800">
CI
</Typography.Text>
<Select
className="min-w-[190px]"
variant="borderless"
popupMatchSelectWidth={false}
options={QWEN_MODEL_OPTIONS}
value={selectedModel}
onChange={setSelectedModel}
disabled={isSending}
aria-label="选择对话模型"
/>
</div>
<span className="rounded-full border border-[var(--ds-border)] bg-neutral-50 px-3 py-1 text-xs text-neutral-500">
@@ -377,7 +394,21 @@ export default function HomePage() {
]}
/>
)}
{item.role === "assistant" && <StreamMessage content={item.content} />}
{item.role === "assistant" &&
(item.content ? (
<StreamMessage content={item.content} />
) : (
<div className="rounded-2xl border border-[var(--ds-border)] bg-neutral-50/80 px-4 py-3 text-[14px] text-neutral-500">
<div className="flex items-center gap-2">
<span></span>
<span className="inline-flex items-center gap-1">
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.3s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.15s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400" />
</span>
</div>
</div>
))}
{item.role === "user" && item.content}
</div>
</div>