refine model and thinking selectors
This commit is contained in:
+314
-28
@@ -1,20 +1,66 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import WorkspaceFiles from './WorkspaceFiles.svelte';
|
import WorkspaceFiles from './WorkspaceFiles.svelte';
|
||||||
|
|
||||||
export let selectedModels = [''];
|
export let selectedModels = [''];
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let showSetDefault = true;
|
export let showSetDefault = true;
|
||||||
let showWorkspace = false;
|
|
||||||
|
|
||||||
const modelOptions = [
|
const modelOptions = [
|
||||||
{ id: 'luna', label: 'Luna', thinking: '开启 · 不分档' },
|
{
|
||||||
{ id: 'terra', label: 'Terra', thinking: '开启 · 不分档' },
|
id: 'luna',
|
||||||
{ id: 'sol', label: 'Sol', thinking: '开启 · 不分档' },
|
label: 'Luna',
|
||||||
{ id: 'deepseek-v4-pro', label: 'DeepSeek', thinking: '极高' }
|
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));
|
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';
|
$: selected = allowed.has(selectedModels?.[0]) ? selectedModels[0] : 'terra';
|
||||||
$: selectedOption = modelOptions.find((item) => item.id === selected) ?? modelOptions[1];
|
$: selectedOption = modelOptions.find((item) => item.id === selected) ?? modelOptions[1];
|
||||||
$: availableIds = new Set($models.map((model) => model.id));
|
$: availableIds = new Set($models.map((model) => model.id));
|
||||||
@@ -26,50 +72,290 @@
|
|||||||
if (fallback) selectedModels = [fallback];
|
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) => {
|
const chooseModel = (modelId: string) => {
|
||||||
if (disabled || !availableIds.has(modelId)) return;
|
if (disabled || ($models.length > 0 && !availableIds.has(modelId))) return;
|
||||||
selectedModels = [modelId];
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="flex max-w-full items-center gap-2 select-none">
|
<svelte:window
|
||||||
<div
|
on:pointerdown={closePopovers}
|
||||||
class="flex items-center overflow-x-auto rounded-xl border border-gray-200/80 p-1 text-xs dark:border-gray-700/80"
|
on:keydown={handleKeydown}
|
||||||
aria-label="模型"
|
on:resize={updatePositions}
|
||||||
>
|
on:scroll={updatePositions}
|
||||||
{#each modelOptions as item}
|
/>
|
||||||
|
|
||||||
|
<div class="flex max-w-full select-none items-center gap-0.5">
|
||||||
<button
|
<button
|
||||||
|
bind:this={modelTrigger}
|
||||||
type="button"
|
type="button"
|
||||||
class="whitespace-nowrap rounded-lg px-2.5 py-1.5 font-medium transition {selected === item.id
|
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]"
|
||||||
? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900'
|
on:click={toggleModels}
|
||||||
: 'text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'}"
|
disabled={disabled}
|
||||||
on:click={() => chooseModel(item.id)}
|
aria-label="选择模型,当前为 {selectedOption.label}"
|
||||||
disabled={disabled || ($models.length > 0 && !availableIds.has(item.id))}
|
aria-haspopup="listbox"
|
||||||
title={item.id === 'deepseek-v4-pro' ? 'DeepSeek V4 Pro' : item.label}
|
aria-expanded={showModels}
|
||||||
>
|
>
|
||||||
{item.label}
|
<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>
|
</button>
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div class="mx-0.5 h-4 w-px bg-gray-200 dark:bg-gray-700" aria-hidden="true" />
|
||||||
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="模型与思考能力是两个独立概念"
|
<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}
|
||||||
>
|
>
|
||||||
思考 <span class="font-medium text-gray-800 dark:text-gray-200">{selectedOption.thinking}</span>
|
<svg
|
||||||
</div>
|
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
|
<button
|
||||||
type="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)}
|
on:click={() => (showWorkspace = true)}
|
||||||
title="工作区文件"
|
title="工作区文件"
|
||||||
aria-label="工作区文件"
|
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" />
|
<path d="M3.5 6.5h6l2 2h9v10h-17z" stroke-linejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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} />
|
<WorkspaceFiles bind:show={showWorkspace} />
|
||||||
|
|||||||
Reference in New Issue
Block a user