refine model and thinking selectors
This commit is contained in:
+318
-32
@@ -1,20 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { models } from '$lib/stores';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import WorkspaceFiles from './WorkspaceFiles.svelte';
|
||||
|
||||
export let selectedModels = [''];
|
||||
export let disabled = false;
|
||||
export let showSetDefault = true;
|
||||
let showWorkspace = false;
|
||||
|
||||
const modelOptions = [
|
||||
{ id: 'luna', label: 'Luna', thinking: '开启 · 不分档' },
|
||||
{ id: 'terra', label: 'Terra', thinking: '开启 · 不分档' },
|
||||
{ id: 'sol', label: 'Sol', thinking: '开启 · 不分档' },
|
||||
{ id: 'deepseek-v4-pro', label: 'DeepSeek', thinking: '极高' }
|
||||
{
|
||||
id: 'luna',
|
||||
label: 'Luna',
|
||||
description: '快速、轻量的本地 Agent',
|
||||
location: '本地',
|
||||
accent: 'bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300',
|
||||
thinking: '开启',
|
||||
thinkingTitle: '思考模式',
|
||||
thinkingHelp: '当前固定开启。此模型只支持思考开关,不提供轻、中、高强度档位。'
|
||||
},
|
||||
{
|
||||
id: 'terra',
|
||||
label: 'Terra',
|
||||
description: '速度与任务能力均衡',
|
||||
location: '本地',
|
||||
accent: 'bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300',
|
||||
thinking: '开启',
|
||||
thinkingTitle: '思考模式',
|
||||
thinkingHelp: '当前固定开启。此模型只支持思考开关,不提供轻、中、高强度档位。'
|
||||
},
|
||||
{
|
||||
id: 'sol',
|
||||
label: 'Sol',
|
||||
description: '面向复杂任务的本地 Agent',
|
||||
location: '本地',
|
||||
accent: 'bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300',
|
||||
thinking: '开启',
|
||||
thinkingTitle: '思考模式',
|
||||
thinkingHelp: '当前固定开启。此模型只支持思考开关,不提供轻、中、高强度档位。'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-v4-pro',
|
||||
label: 'DeepSeek V4 Pro',
|
||||
description: '云端高能力 Agent',
|
||||
location: '云端',
|
||||
accent: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300',
|
||||
thinking: '极高',
|
||||
thinkingTitle: '推理强度',
|
||||
thinkingHelp: '当前固定为极高(max),由 Agent 服务端管理。'
|
||||
}
|
||||
];
|
||||
const allowed = new Set(modelOptions.map((item) => item.id));
|
||||
|
||||
let showWorkspace = false;
|
||||
let showModels = false;
|
||||
let showThinking = false;
|
||||
let modelTrigger: HTMLElement | null = null;
|
||||
let modelPanel: HTMLElement | null = null;
|
||||
let thinkingTrigger: HTMLElement | null = null;
|
||||
let thinkingPanel: HTMLElement | null = null;
|
||||
let modelPosition = { top: 0, left: 0, width: 320 };
|
||||
let thinkingPosition = { top: 0, left: 0, width: 288 };
|
||||
|
||||
$: selected = allowed.has(selectedModels?.[0]) ? selectedModels[0] : 'terra';
|
||||
$: selectedOption = modelOptions.find((item) => item.id === selected) ?? modelOptions[1];
|
||||
$: availableIds = new Set($models.map((model) => model.id));
|
||||
@@ -26,50 +72,290 @@
|
||||
if (fallback) selectedModels = [fallback];
|
||||
}
|
||||
|
||||
const portal = (node: HTMLElement) => {
|
||||
document.body.appendChild(node);
|
||||
return {
|
||||
destroy() {
|
||||
node.remove();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const positionFor = (trigger: HTMLElement | null, preferredWidth: number) => {
|
||||
if (!trigger || typeof window === 'undefined') {
|
||||
return { top: 0, left: 0, width: preferredWidth };
|
||||
}
|
||||
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
const width = Math.min(preferredWidth, window.innerWidth - 16);
|
||||
return {
|
||||
top: rect.bottom + 8,
|
||||
left: Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)),
|
||||
width
|
||||
};
|
||||
};
|
||||
|
||||
const updatePositions = () => {
|
||||
if (showModels) modelPosition = positionFor(modelTrigger, 320);
|
||||
if (showThinking) thinkingPosition = positionFor(thinkingTrigger, 288);
|
||||
};
|
||||
|
||||
const toggleModels = () => {
|
||||
if (disabled) return;
|
||||
showThinking = false;
|
||||
showModels = !showModels;
|
||||
if (showModels) modelPosition = positionFor(modelTrigger, 320);
|
||||
};
|
||||
|
||||
const toggleThinking = () => {
|
||||
showModels = false;
|
||||
showThinking = !showThinking;
|
||||
if (showThinking) thinkingPosition = positionFor(thinkingTrigger, 288);
|
||||
};
|
||||
|
||||
const chooseModel = (modelId: string) => {
|
||||
if (disabled || !availableIds.has(modelId)) return;
|
||||
if (disabled || ($models.length > 0 && !availableIds.has(modelId))) return;
|
||||
selectedModels = [modelId];
|
||||
showModels = false;
|
||||
};
|
||||
|
||||
const closePopovers = (event: PointerEvent) => {
|
||||
const target = event.target as Node;
|
||||
if (
|
||||
(modelTrigger?.contains(target) ?? false) ||
|
||||
(modelPanel?.contains(target) ?? false) ||
|
||||
(thinkingTrigger?.contains(target) ?? false) ||
|
||||
(thinkingPanel?.contains(target) ?? false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
showModels = false;
|
||||
showThinking = false;
|
||||
};
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape') return;
|
||||
const focusTarget = showThinking ? thinkingTrigger : modelTrigger;
|
||||
showModels = false;
|
||||
showThinking = false;
|
||||
focusTarget?.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex max-w-full items-center gap-2 select-none">
|
||||
<div
|
||||
class="flex items-center overflow-x-auto rounded-xl border border-gray-200/80 p-1 text-xs dark:border-gray-700/80"
|
||||
aria-label="模型"
|
||||
>
|
||||
{#each modelOptions as item}
|
||||
<button
|
||||
type="button"
|
||||
class="whitespace-nowrap rounded-lg px-2.5 py-1.5 font-medium transition {selected === item.id
|
||||
? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900'
|
||||
: 'text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'}"
|
||||
on:click={() => chooseModel(item.id)}
|
||||
disabled={disabled || ($models.length > 0 && !availableIds.has(item.id))}
|
||||
title={item.id === 'deepseek-v4-pro' ? 'DeepSeek V4 Pro' : item.label}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<svelte:window
|
||||
on:pointerdown={closePopovers}
|
||||
on:keydown={handleKeydown}
|
||||
on:resize={updatePositions}
|
||||
on:scroll={updatePositions}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="whitespace-nowrap rounded-xl bg-gray-100/90 px-2.5 py-2 text-xs text-gray-500 dark:bg-gray-800/90 dark:text-gray-400"
|
||||
title="模型与思考能力是两个独立概念"
|
||||
<div class="flex max-w-full select-none items-center gap-0.5">
|
||||
<button
|
||||
bind:this={modelTrigger}
|
||||
type="button"
|
||||
class="group flex min-w-0 max-w-[13rem] items-center gap-1.5 rounded-xl px-2.5 py-2 text-sm font-semibold text-gray-900 outline-hidden transition hover:bg-gray-100 focus-visible:ring-2 focus-visible:ring-gray-300 disabled:cursor-not-allowed disabled:opacity-50 dark:text-white dark:hover:bg-gray-800 dark:focus-visible:ring-gray-600 sm:max-w-[16rem]"
|
||||
on:click={toggleModels}
|
||||
disabled={disabled}
|
||||
aria-label="选择模型,当前为 {selectedOption.label}"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={showModels}
|
||||
>
|
||||
思考 <span class="font-medium text-gray-800 dark:text-gray-200">{selectedOption.thinking}</span>
|
||||
</div>
|
||||
<span class="truncate">{selectedOption.label}</span>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-3.5 shrink-0 text-gray-400 transition-transform duration-150 {showModels
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m6 8 4 4 4-4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="mx-0.5 h-4 w-px bg-gray-200 dark:bg-gray-700" aria-hidden="true" />
|
||||
|
||||
<button
|
||||
bind:this={thinkingTrigger}
|
||||
type="button"
|
||||
class="group flex shrink-0 items-center gap-1.5 rounded-xl px-2.5 py-2 text-xs font-medium text-gray-500 outline-hidden transition hover:bg-gray-100 hover:text-gray-900 focus-visible:ring-2 focus-visible:ring-gray-300 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:ring-gray-600"
|
||||
on:click={toggleThinking}
|
||||
aria-label="{selectedOption.thinkingTitle}:{selectedOption.thinking}"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={showThinking}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-3.5 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-200"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M10 2.5c.3 2.4 1.5 3.6 4 4-2.5.4-3.7 1.6-4 4-.4-2.4-1.6-3.6-4-4 2.4-.4 3.6-1.6 4-4Z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M4.5 11.5c.2 1.7 1 2.5 2.7 2.7-1.7.3-2.5 1.1-2.7 2.8-.3-1.7-1.1-2.5-2.8-2.8 1.7-.2 2.5-1 2.8-2.7Z" />
|
||||
<path d="M15 12.5c.2 1.2.8 1.8 2 2-1.2.2-1.8.8-2 2-.2-1.2-.8-1.8-2-2 1.2-.2 1.8-.8 2-2Z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">{selectedOption.thinkingTitle}</span>
|
||||
<span class="text-gray-800 dark:text-gray-200">{selectedOption.thinking}</span>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-3 shrink-0 text-gray-400 transition-transform duration-150 {showThinking
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m6 8 4 4 4-4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-xl border border-gray-200/80 p-2 text-gray-500 transition hover:bg-gray-100 hover:text-gray-900 dark:border-gray-700/80 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
class="ml-0.5 rounded-xl p-2 text-gray-500 outline-hidden transition hover:bg-gray-100 hover:text-gray-900 focus-visible:ring-2 focus-visible:ring-gray-300 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:ring-gray-600"
|
||||
on:click={() => (showWorkspace = true)}
|
||||
title="工作区文件"
|
||||
aria-label="工作区文件"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
class="size-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M3.5 6.5h6l2 2h9v10h-17z" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showModels}
|
||||
<div
|
||||
use:portal
|
||||
bind:this={modelPanel}
|
||||
class="fixed z-[9999]"
|
||||
style="top: {modelPosition.top}px; left: {modelPosition.left}px; width: {modelPosition.width}px;"
|
||||
transition:flyAndScale
|
||||
>
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-gray-200/70 bg-white p-1.5 text-gray-900 shadow-xl shadow-black/10 dark:border-gray-700/70 dark:bg-gray-850 dark:text-white dark:shadow-black/30"
|
||||
role="listbox"
|
||||
aria-label="可用模型"
|
||||
>
|
||||
<div class="px-2.5 pb-1.5 pt-1 text-xs font-medium text-gray-400 dark:text-gray-500">
|
||||
选择模型
|
||||
</div>
|
||||
{#each modelOptions as item}
|
||||
{@const unavailable = $models.length > 0 && !availableIds.has(item.id)}
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selected === item.id}
|
||||
class="group flex w-full items-center gap-3 rounded-xl px-2.5 py-2.5 text-left outline-hidden transition hover:bg-gray-100 focus-visible:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40 dark:hover:bg-gray-800 dark:focus-visible:bg-gray-800"
|
||||
on:click={() => chooseModel(item.id)}
|
||||
disabled={disabled || unavailable}
|
||||
>
|
||||
<span
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-bold {item.accent}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{item.label.slice(0, 1)}
|
||||
</span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="truncate text-sm font-medium">{item.label}</span>
|
||||
<span
|
||||
class="rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500 dark:bg-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{item.location}
|
||||
</span>
|
||||
</span>
|
||||
<span class="mt-0.5 block truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{unavailable ? '当前不可用' : item.description}
|
||||
</span>
|
||||
</span>
|
||||
{#if selected === item.id}
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-4 shrink-0 text-gray-900 dark:text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m5 10 3 3 7-7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showThinking}
|
||||
<div
|
||||
use:portal
|
||||
bind:this={thinkingPanel}
|
||||
class="fixed z-[9999]"
|
||||
style="top: {thinkingPosition.top}px; left: {thinkingPosition.left}px; width: {thinkingPosition.width}px;"
|
||||
transition:flyAndScale
|
||||
>
|
||||
<div
|
||||
class="rounded-2xl border border-gray-200/70 bg-white p-2 text-gray-900 shadow-xl shadow-black/10 dark:border-gray-700/70 dark:bg-gray-850 dark:text-white dark:shadow-black/30"
|
||||
role="dialog"
|
||||
aria-label="{selectedOption.thinkingTitle}说明"
|
||||
>
|
||||
<div class="flex items-center gap-3 rounded-xl bg-gray-50 px-3 py-2.5 dark:bg-gray-800/70">
|
||||
<span
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-full bg-gray-900 text-white dark:bg-white dark:text-gray-900"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
>
|
||||
<path
|
||||
d="M10 2.5c.3 2.4 1.5 3.6 4 4-2.5.4-3.7 1.6-4 4-.4-2.4-1.6-3.6-4-4 2.4-.4 3.6-1.6 4-4Z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M5 12c.2 1.7 1 2.5 2.7 2.7C6 15 5.2 15.8 5 17.5c-.3-1.7-1.1-2.5-2.8-2.8C3.9 14.5 4.7 13.7 5 12Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block text-xs text-gray-500 dark:text-gray-400">
|
||||
{selectedOption.thinkingTitle}
|
||||
</span>
|
||||
<span class="block text-sm font-semibold">{selectedOption.thinking}</span>
|
||||
</span>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-4 shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m5 10 3 3 7-7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="px-2 pb-1 pt-2.5 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{selectedOption.thinkingHelp}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<WorkspaceFiles bind:show={showWorkspace} />
|
||||
|
||||
Reference in New Issue
Block a user