mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
feat(webui): link model picker to settings
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
|||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type PointerEvent,
|
type PointerEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Check, CircleHelp, Sparkles } from "lucide-react";
|
import { Check, CircleHelp, SlidersHorizontal, Sparkles } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -85,6 +85,7 @@ interface ModelPresetBadgeProps {
|
|||||||
modelPreset?: string | null;
|
modelPreset?: string | null;
|
||||||
modelPresets?: ModelPresetOption[];
|
modelPresets?: ModelPresetOption[];
|
||||||
onPresetChange?: (name: string) => void;
|
onPresetChange?: (name: string) => void;
|
||||||
|
onManageModels?: () => void;
|
||||||
onRequestComposerFocus?: () => void;
|
onRequestComposerFocus?: () => void;
|
||||||
provider?: string | null;
|
provider?: string | null;
|
||||||
providerLabel?: string | null;
|
providerLabel?: string | null;
|
||||||
@@ -100,6 +101,7 @@ export function ModelPresetBadge({
|
|||||||
modelPreset,
|
modelPreset,
|
||||||
modelPresets = [],
|
modelPresets = [],
|
||||||
onPresetChange,
|
onPresetChange,
|
||||||
|
onManageModels,
|
||||||
onRequestComposerFocus,
|
onRequestComposerFocus,
|
||||||
provider,
|
provider,
|
||||||
providerLabel,
|
providerLabel,
|
||||||
@@ -156,6 +158,11 @@ export function ModelPresetBadge({
|
|||||||
requestAnimationFrame(() => onRequestComposerFocus?.());
|
requestAnimationFrame(() => onRequestComposerFocus?.());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openModelSettings = () => {
|
||||||
|
setOpen(false);
|
||||||
|
onManageModels?.();
|
||||||
|
};
|
||||||
|
|
||||||
const clearGesture = () => {
|
const clearGesture = () => {
|
||||||
const gesture = gestureRef.current;
|
const gesture = gestureRef.current;
|
||||||
if (gesture?.timer) clearTimeout(gesture.timer);
|
if (gesture?.timer) clearTimeout(gesture.timer);
|
||||||
@@ -418,6 +425,22 @@ export function ModelPresetBadge({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{onManageModels ? (
|
||||||
|
<div className="mt-1 border-t border-border/55 pt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={openModelSettings}
|
||||||
|
className={cn(
|
||||||
|
floatingItemClassName,
|
||||||
|
floatingItemFocusClassName,
|
||||||
|
"flex min-h-9 w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="size-4 shrink-0" strokeWidth={1.75} />
|
||||||
|
<span>{t("thread.composer.manageModels")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ interface ThreadComposerProps {
|
|||||||
modelNeedsSetup?: boolean;
|
modelNeedsSetup?: boolean;
|
||||||
fallbackModelName?: string | null;
|
fallbackModelName?: string | null;
|
||||||
onModelBadgeClick?: () => void;
|
onModelBadgeClick?: () => void;
|
||||||
|
onManageModels?: () => void;
|
||||||
contextUsage?: ComposerContextUsage | null;
|
contextUsage?: ComposerContextUsage | null;
|
||||||
variant?: "thread" | "hero";
|
variant?: "thread" | "hero";
|
||||||
slashCommands?: SlashCommand[];
|
slashCommands?: SlashCommand[];
|
||||||
@@ -998,6 +999,7 @@ export function ThreadComposer({
|
|||||||
modelNeedsSetup = false,
|
modelNeedsSetup = false,
|
||||||
fallbackModelName = null,
|
fallbackModelName = null,
|
||||||
onModelBadgeClick,
|
onModelBadgeClick,
|
||||||
|
onManageModels,
|
||||||
contextUsage = null,
|
contextUsage = null,
|
||||||
variant = "thread",
|
variant = "thread",
|
||||||
slashCommands = [],
|
slashCommands = [],
|
||||||
@@ -2539,6 +2541,7 @@ export function ThreadComposer({
|
|||||||
modelPreset={modelPreset}
|
modelPreset={modelPreset}
|
||||||
modelPresets={modelPresets}
|
modelPresets={modelPresets}
|
||||||
onPresetChange={onModelPresetChange}
|
onPresetChange={onModelPresetChange}
|
||||||
|
onManageModels={onManageModels}
|
||||||
onRequestComposerFocus={() => textareaRef.current?.focus()}
|
onRequestComposerFocus={() => textareaRef.current?.focus()}
|
||||||
provider={modelProvider}
|
provider={modelProvider}
|
||||||
providerLabel={modelProviderLabel}
|
providerLabel={modelProviderLabel}
|
||||||
|
|||||||
@@ -1519,6 +1519,7 @@ export function ThreadShell({
|
|||||||
modelNeedsSetup={modelBadge.needsSetup}
|
modelNeedsSetup={modelBadge.needsSetup}
|
||||||
fallbackModelName={fallbackModelName}
|
fallbackModelName={fallbackModelName}
|
||||||
onModelBadgeClick={modelBadge.needsSetup ? onOpenModelSettings : undefined}
|
onModelBadgeClick={modelBadge.needsSetup ? onOpenModelSettings : undefined}
|
||||||
|
onManageModels={onOpenModelSettings}
|
||||||
contextUsage={composerContextUsage}
|
contextUsage={composerContextUsage}
|
||||||
variant={composerVariant}
|
variant={composerVariant}
|
||||||
slashCommands={availableSlashCommands}
|
slashCommands={availableSlashCommands}
|
||||||
@@ -1567,6 +1568,7 @@ export function ThreadShell({
|
|||||||
modelNeedsSetup={modelBadge.needsSetup}
|
modelNeedsSetup={modelBadge.needsSetup}
|
||||||
fallbackModelName={fallbackModelName}
|
fallbackModelName={fallbackModelName}
|
||||||
onModelBadgeClick={modelBadge.needsSetup ? onOpenModelSettings : undefined}
|
onModelBadgeClick={modelBadge.needsSetup ? onOpenModelSettings : undefined}
|
||||||
|
onManageModels={onOpenModelSettings}
|
||||||
contextUsage={composerContextUsage}
|
contextUsage={composerContextUsage}
|
||||||
variant="hero"
|
variant="hero"
|
||||||
slashCommands={availableSlashCommands}
|
slashCommands={availableSlashCommands}
|
||||||
|
|||||||
@@ -1196,6 +1196,7 @@
|
|||||||
"modelNotConfigured": "Model not configured",
|
"modelNotConfigured": "Model not configured",
|
||||||
"configureModel": "Configure model",
|
"configureModel": "Configure model",
|
||||||
"switchModel": "Switch model for this chat",
|
"switchModel": "Switch model for this chat",
|
||||||
|
"manageModels": "Manage models",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "Context · {{tokens}}{{capacity}}",
|
"tooltip": "Context · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}. {{percent}}% used."
|
"meterDescription": "{{context}}. {{percent}}% used."
|
||||||
|
|||||||
@@ -1183,6 +1183,7 @@
|
|||||||
"modelNotConfigured": "Modelo no configurado",
|
"modelNotConfigured": "Modelo no configurado",
|
||||||
"configureModel": "Configurar modelo",
|
"configureModel": "Configurar modelo",
|
||||||
"switchModel": "Cambiar el modelo de este chat",
|
"switchModel": "Cambiar el modelo de este chat",
|
||||||
|
"manageModels": "Gestionar modelos",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "Contexto · {{tokens}}{{capacity}}",
|
"tooltip": "Contexto · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}. {{percent}} % usado."
|
"meterDescription": "{{context}}. {{percent}} % usado."
|
||||||
|
|||||||
@@ -1182,6 +1182,7 @@
|
|||||||
"modelNotConfigured": "Modèle non configuré",
|
"modelNotConfigured": "Modèle non configuré",
|
||||||
"configureModel": "Configurer le modèle",
|
"configureModel": "Configurer le modèle",
|
||||||
"switchModel": "Changer le modèle de cette conversation",
|
"switchModel": "Changer le modèle de cette conversation",
|
||||||
|
"manageModels": "Gérer les modèles",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "Contexte · {{tokens}}{{capacity}}",
|
"tooltip": "Contexte · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}. {{percent}} % utilisé."
|
"meterDescription": "{{context}}. {{percent}} % utilisé."
|
||||||
|
|||||||
@@ -1182,6 +1182,7 @@
|
|||||||
"modelNotConfigured": "Model belum dikonfigurasi",
|
"modelNotConfigured": "Model belum dikonfigurasi",
|
||||||
"configureModel": "Konfigurasi model",
|
"configureModel": "Konfigurasi model",
|
||||||
"switchModel": "Ganti model untuk percakapan ini",
|
"switchModel": "Ganti model untuk percakapan ini",
|
||||||
|
"manageModels": "Kelola model",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "Konteks · {{tokens}}{{capacity}}",
|
"tooltip": "Konteks · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}. {{percent}}% digunakan."
|
"meterDescription": "{{context}}. {{percent}}% digunakan."
|
||||||
|
|||||||
@@ -1182,6 +1182,7 @@
|
|||||||
"modelNotConfigured": "モデルが未設定です",
|
"modelNotConfigured": "モデルが未設定です",
|
||||||
"configureModel": "モデルを設定",
|
"configureModel": "モデルを設定",
|
||||||
"switchModel": "この会話で使うモデルを切り替える",
|
"switchModel": "この会話で使うモデルを切り替える",
|
||||||
|
"manageModels": "モデルを管理",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "コンテキスト · {{tokens}}{{capacity}}",
|
"tooltip": "コンテキスト · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}。{{percent}}% 使用中"
|
"meterDescription": "{{context}}。{{percent}}% 使用中"
|
||||||
|
|||||||
@@ -1182,6 +1182,7 @@
|
|||||||
"modelNotConfigured": "모델이 설정되지 않음",
|
"modelNotConfigured": "모델이 설정되지 않음",
|
||||||
"configureModel": "모델 설정",
|
"configureModel": "모델 설정",
|
||||||
"switchModel": "이 대화에서 사용할 모델 전환",
|
"switchModel": "이 대화에서 사용할 모델 전환",
|
||||||
|
"manageModels": "모델 관리",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "컨텍스트 · {{tokens}}{{capacity}}",
|
"tooltip": "컨텍스트 · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}. {{percent}}% 사용 중."
|
"meterDescription": "{{context}}. {{percent}}% 사용 중."
|
||||||
|
|||||||
@@ -1196,6 +1196,7 @@
|
|||||||
"modelNotConfigured": "Modelo não configurado",
|
"modelNotConfigured": "Modelo não configurado",
|
||||||
"configureModel": "Configurar modelo",
|
"configureModel": "Configurar modelo",
|
||||||
"switchModel": "Alternar o modelo desta conversa",
|
"switchModel": "Alternar o modelo desta conversa",
|
||||||
|
"manageModels": "Gerenciar modelos",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "Contexto · {{tokens}}{{capacity}}",
|
"tooltip": "Contexto · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}. {{percent}}% usado."
|
"meterDescription": "{{context}}. {{percent}}% usado."
|
||||||
|
|||||||
@@ -1182,6 +1182,7 @@
|
|||||||
"modelNotConfigured": "Chưa cấu hình mô hình",
|
"modelNotConfigured": "Chưa cấu hình mô hình",
|
||||||
"configureModel": "Cấu hình mô hình",
|
"configureModel": "Cấu hình mô hình",
|
||||||
"switchModel": "Chuyển mô hình cho cuộc trò chuyện này",
|
"switchModel": "Chuyển mô hình cho cuộc trò chuyện này",
|
||||||
|
"manageModels": "Quản lý mô hình",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "Ngữ cảnh · {{tokens}}{{capacity}}",
|
"tooltip": "Ngữ cảnh · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}. Đã dùng {{percent}}%."
|
"meterDescription": "{{context}}. Đã dùng {{percent}}%."
|
||||||
|
|||||||
@@ -1195,6 +1195,7 @@
|
|||||||
"modelNotConfigured": "模型未配置",
|
"modelNotConfigured": "模型未配置",
|
||||||
"configureModel": "配置模型",
|
"configureModel": "配置模型",
|
||||||
"switchModel": "切换本次对话所用模型",
|
"switchModel": "切换本次对话所用模型",
|
||||||
|
"manageModels": "管理模型预设",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "上下文 · {{tokens}}{{capacity}}",
|
"tooltip": "上下文 · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}。已使用 {{percent}}%。"
|
"meterDescription": "{{context}}。已使用 {{percent}}%。"
|
||||||
|
|||||||
@@ -1182,6 +1182,7 @@
|
|||||||
"modelNotConfigured": "尚未設定模型",
|
"modelNotConfigured": "尚未設定模型",
|
||||||
"configureModel": "設定模型",
|
"configureModel": "設定模型",
|
||||||
"switchModel": "切換此對話使用的模型",
|
"switchModel": "切換此對話使用的模型",
|
||||||
|
"manageModels": "管理模型預設",
|
||||||
"context": {
|
"context": {
|
||||||
"tooltip": "上下文 · {{tokens}}{{capacity}}",
|
"tooltip": "上下文 · {{tokens}}{{capacity}}",
|
||||||
"meterDescription": "{{context}}。已使用 {{percent}}%。"
|
"meterDescription": "{{context}}。已使用 {{percent}}%。"
|
||||||
|
|||||||
@@ -322,7 +322,10 @@ const MODEL_PRESETS = [
|
|||||||
{ name: "dspro", model: "deepseek/deepseek-v4-pro", provider: "deepseek" },
|
{ name: "dspro", model: "deepseek/deepseek-v4-pro", provider: "deepseek" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function renderPresetComposer(variant: "thread" | "hero" = "thread") {
|
function renderPresetComposer(
|
||||||
|
variant: "thread" | "hero" = "thread",
|
||||||
|
onManageModels?: () => void,
|
||||||
|
) {
|
||||||
const onPresetChange = vi.fn();
|
const onPresetChange = vi.fn();
|
||||||
render(
|
render(
|
||||||
<ThreadComposer
|
<ThreadComposer
|
||||||
@@ -332,6 +335,7 @@ function renderPresetComposer(variant: "thread" | "hero" = "thread") {
|
|||||||
modelProvider="moonshot"
|
modelProvider="moonshot"
|
||||||
modelPresets={MODEL_PRESETS}
|
modelPresets={MODEL_PRESETS}
|
||||||
onModelPresetChange={onPresetChange}
|
onModelPresetChange={onPresetChange}
|
||||||
|
onManageModels={onManageModels}
|
||||||
placeholder={variant === "hero" ? "Ask anything..." : "Type your message..."}
|
placeholder={variant === "hero" ? "Ask anything..." : "Type your message..."}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
/>,
|
/>,
|
||||||
@@ -610,6 +614,18 @@ describe("ThreadComposer", () => {
|
|||||||
expect(badge).toHaveClass("w-fit");
|
expect(badge).toHaveClass("w-fit");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens model settings from the picker footer", async () => {
|
||||||
|
const onManageModels = vi.fn();
|
||||||
|
const { badge } = renderPresetComposer("thread", onManageModels);
|
||||||
|
|
||||||
|
fireEvent.click(badge);
|
||||||
|
const picker = screen.getByRole("dialog", { name: "Switch model for this chat" });
|
||||||
|
fireEvent.click(within(picker).getByRole("button", { name: "Manage models" }));
|
||||||
|
|
||||||
|
expect(onManageModels).toHaveBeenCalledTimes(1);
|
||||||
|
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps long-press drag switching alongside the click picker", () => {
|
it("keeps long-press drag switching alongside the click picker", () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const { badge, onPresetChange } = renderPresetComposer();
|
const { badge, onPresetChange } = renderPresetComposer();
|
||||||
|
|||||||
Reference in New Issue
Block a user