mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 14:07:20 +08:00
refactor: improve models config and explore pages
This commit is contained in:
@@ -443,7 +443,7 @@ export function AgentBuilderRoute() {
|
||||
</div>
|
||||
|
||||
<footer class="drawer-footer">
|
||||
<Button variant="secondary" onClick={state.close}>
|
||||
<Button variant="ghost" onClick={state.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -512,7 +512,7 @@ export function AgentBuilderRoute() {
|
||||
</div>
|
||||
|
||||
<footer class="drawer-footer">
|
||||
<Button variant="secondary" onClick={state.close}>
|
||||
<Button variant="ghost" onClick={state.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" disabled={Boolean(state.ctx.saving()) || state.locked()} onClick={state.saveTools}>
|
||||
|
||||
@@ -5,24 +5,13 @@ import { Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import type { Model } from "@kilocode/sdk/v2/client"
|
||||
import { SearchField } from "../../components/SearchField"
|
||||
import { text } from "../../shared/utils"
|
||||
import { ConfigPage, ConfigToolbar, SourceBadge } from "./ConfigPage"
|
||||
import { ConfigPage, SourceBadge } from "./ConfigPage"
|
||||
import { type Capability, type ModelField, useModelSettings } from "./state/models"
|
||||
|
||||
function fmtPrice(n: number) {
|
||||
if (n === 0) return "Free"
|
||||
if (n < 0.01) return `$${n.toFixed(4)}/1M`
|
||||
return `$${n.toFixed(2)}/1M`
|
||||
}
|
||||
|
||||
function fmtCachedPrice(cost: Model["cost"]) {
|
||||
if (cost.cache.read > 0) return fmtPrice(cost.cache.read)
|
||||
if (cost.input === 0) return fmtPrice(0)
|
||||
return null
|
||||
}
|
||||
|
||||
function avgPrice(cost: Model["cost"]) {
|
||||
if (cost.cache.read > 0) return cost.cache.read * 0.7 + cost.input * 0.2 + cost.output * 0.1
|
||||
return cost.input * 0.9 + cost.output * 0.1
|
||||
function money(n: number) {
|
||||
if (n === 0) return "$0.00"
|
||||
if (n < 0.01) return `$${n.toFixed(4)}`
|
||||
return `$${n.toFixed(2)}`
|
||||
}
|
||||
|
||||
function fmtContext(n: number) {
|
||||
@@ -41,32 +30,19 @@ function title(input: string) {
|
||||
return input.charAt(0).toUpperCase() + input.slice(1)
|
||||
}
|
||||
|
||||
function mods(input: Record<string, boolean>) {
|
||||
const values = Object.entries(input)
|
||||
.filter(([key, value]) => value && key !== "text")
|
||||
.map(([key]) => title(key))
|
||||
return values.length > 0 ? values.join(", ") : "Text only"
|
||||
}
|
||||
|
||||
function desc(model: Model) {
|
||||
const value = model.options?.description
|
||||
if (typeof value === "string" && value.trim()) return value
|
||||
return null
|
||||
}
|
||||
|
||||
function capLabel(cap: Capability) {
|
||||
if (cap === "toolcall") return "Tools"
|
||||
if (cap === "attachment") return "Attachments"
|
||||
if (cap === "temperature") return "Temperature"
|
||||
if (cap === "interleaved") return "Interleaved"
|
||||
if (cap === "input:audio") return "Input audio"
|
||||
if (cap === "input:image") return "Input image"
|
||||
if (cap === "input:video") return "Input video"
|
||||
if (cap === "input:pdf") return "Input PDF"
|
||||
if (cap === "output:audio") return "Output audio"
|
||||
if (cap === "output:image") return "Output image"
|
||||
if (cap === "output:video") return "Output video"
|
||||
return "Output PDF"
|
||||
function chips(model: Model) {
|
||||
const list: string[] = []
|
||||
if (model.capabilities.toolcall) list.push("toolcall")
|
||||
if (model.capabilities.attachment) list.push("attachment")
|
||||
if (model.capabilities.input.image) list.push("vision")
|
||||
if (model.capabilities.input.audio || model.capabilities.output.audio) list.push("audio")
|
||||
return list
|
||||
}
|
||||
|
||||
function step(max: number) {
|
||||
@@ -96,9 +72,63 @@ function point(event: PointerEvent, root: HTMLElement, max: number) {
|
||||
return snap(ratio * max, max)
|
||||
}
|
||||
|
||||
function rangeLabel(low: number, high: number, max: number) {
|
||||
const top = high >= max && max > 0 ? `${fmtContext(high)}+` : fmtContext(high)
|
||||
return `${fmtContext(low)} - ${top}`
|
||||
function Stat(props: { label: string; value: string; sub?: string; mono?: boolean }) {
|
||||
return (
|
||||
<div class="model-stat">
|
||||
<span>{props.label}</span>
|
||||
<strong classList={{ mono: props.mono }}>
|
||||
{props.value}
|
||||
<Show when={props.sub}>{(sub) => <small>{sub()}</small>}</Show>
|
||||
</strong>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const caps = [
|
||||
{ key: "toolcall", label: "toolcall" },
|
||||
{ key: "attachment", label: "attachment" },
|
||||
{ key: "input:image", label: "vision" },
|
||||
{ key: "input:audio", label: "audio" },
|
||||
] satisfies { key: Capability; label: string }[]
|
||||
|
||||
type Option = { value: string; label: string }
|
||||
|
||||
function FilterSelect(props: { label: string; value: string; options: Option[]; onSelect: (value: string) => void }) {
|
||||
const current = () => props.options.find((option) => option.value === props.value)?.label ?? props.value
|
||||
|
||||
function choose(value: string, event: MouseEvent & { currentTarget: HTMLButtonElement }) {
|
||||
props.onSelect(value)
|
||||
event.currentTarget.closest("details")?.removeAttribute("open")
|
||||
}
|
||||
|
||||
function toggle(event: Event & { currentTarget: HTMLDetailsElement }) {
|
||||
if (!event.currentTarget.open) return
|
||||
event.currentTarget.parentElement?.querySelectorAll(".models-select[open]").forEach((node) => {
|
||||
if (node !== event.currentTarget) node.removeAttribute("open")
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<details class="models-select" onToggle={toggle}>
|
||||
<summary aria-label={props.label}>{current()}</summary>
|
||||
<div class="models-select-menu" role="listbox" aria-label={props.label}>
|
||||
<For each={props.options}>
|
||||
{(option) => (
|
||||
<button
|
||||
class="models-select-option"
|
||||
classList={{ selected: option.value === props.value }}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={option.value === props.value}
|
||||
onClick={(event) => choose(option.value, event)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModelsRoute() {
|
||||
@@ -209,7 +239,7 @@ export function ModelsDefaultRoute() {
|
||||
</div>
|
||||
|
||||
<footer class="drawer-footer">
|
||||
<Button variant="secondary" onClick={state.close}>
|
||||
<Button variant="ghost" onClick={state.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" disabled={Boolean(state.ctx.saving()) || !state.choice()} onClick={state.save}>
|
||||
@@ -261,104 +291,113 @@ export function ModelsAvailableRoute() {
|
||||
|
||||
return (
|
||||
<Show when={state.snap()}>
|
||||
<ConfigPage title="Explore" actions={<Tag>{state.models().length}</Tag>}>
|
||||
<ConfigToolbar title="Model Filters" description="Search and filter configured provider models.">
|
||||
<label>
|
||||
<span>Search</span>
|
||||
<input
|
||||
value={state.search()}
|
||||
placeholder="Model name"
|
||||
onInput={(event) => state.setSearch(event.currentTarget.value)}
|
||||
<ConfigPage
|
||||
title={
|
||||
<span class="config-title-count">
|
||||
Explore models
|
||||
<Tag>{state.models().length}</Tag>
|
||||
</span>
|
||||
}
|
||||
description="All models exposed by configured providers. Set defaults under Models > Defaults."
|
||||
>
|
||||
<section class="models-filter-card">
|
||||
<div class="models-filter-row models-filter-primary">
|
||||
<label class="models-filter-field models-search-field">
|
||||
<span>Search</span>
|
||||
<input
|
||||
value={state.search()}
|
||||
placeholder="Search by name or ID..."
|
||||
onInput={(event) => state.setSearch(event.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<FilterSelect
|
||||
label="Provider"
|
||||
value={state.filter()}
|
||||
options={[{ value: "all", label: "All providers" }, ...state.providers().map((provider) => ({ value: provider.id, label: provider.name }))]}
|
||||
onSelect={state.setFilter}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Provider</span>
|
||||
<select value={state.filter()} onChange={(event) => state.setFilter(event.currentTarget.value)}>
|
||||
<option value="all">All providers</option>
|
||||
<For each={state.providers()}>{(provider) => <option value={provider.id}>{provider.name}</option>}</For>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Cost</span>
|
||||
<select value={state.price()} onChange={(event) => state.setPrice(event.currentTarget.value)}>
|
||||
<option value="all">Free and paid</option>
|
||||
<option value="free">Free only</option>
|
||||
<option value="paid">Paid only</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Reasoning</span>
|
||||
<select value={state.reason()} onChange={(event) => state.setReason(event.currentTarget.value)}>
|
||||
<option value="all">Any reasoning</option>
|
||||
<option value="reasoning">Reasoning only</option>
|
||||
<option value="standard">No reasoning</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Favorites</span>
|
||||
<select
|
||||
value={state.starred() ? "favorites" : "all"}
|
||||
onChange={(event) => state.setStarred(event.currentTarget.value === "favorites")}
|
||||
<FilterSelect
|
||||
label="Cost"
|
||||
value={state.price()}
|
||||
options={[
|
||||
{ value: "all", label: "Any cost" },
|
||||
{ value: "free", label: "Free" },
|
||||
{ value: "paid", label: "Paid" },
|
||||
]}
|
||||
onSelect={state.setPrice}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="Reasoning"
|
||||
value={state.reason()}
|
||||
options={[
|
||||
{ value: "all", label: "Reasoning · any" },
|
||||
{ value: "reasoning", label: "Reasoning · yes" },
|
||||
{ value: "standard", label: "Reasoning · no" },
|
||||
]}
|
||||
onSelect={state.setReason}
|
||||
/>
|
||||
<button
|
||||
class="models-favorite-filter"
|
||||
classList={{ selected: state.starred() }}
|
||||
type="button"
|
||||
aria-pressed={state.starred()}
|
||||
onClick={() => state.setStarred(!state.starred())}
|
||||
>
|
||||
<option value="all">All models</option>
|
||||
<option value="favorites">Favorites only</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="context-filter">
|
||||
<span>Context</span>
|
||||
<div
|
||||
class="context-range"
|
||||
style={`--context-min: ${pct(state.low(), state.max())}%; --context-max: ${pct(state.top(), state.max())}%;`}
|
||||
>
|
||||
<span class="context-track" aria-hidden="true" />
|
||||
<span class="context-fill" aria-hidden="true" />
|
||||
<button
|
||||
class="context-min"
|
||||
aria-label="Minimum context"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax={state.top()}
|
||||
aria-valuenow={state.low()}
|
||||
disabled={state.max() === 0}
|
||||
role="slider"
|
||||
type="button"
|
||||
onKeyDown={(event) => key("min", event)}
|
||||
onPointerDown={(event) => drag("min", event)}
|
||||
/>
|
||||
<button
|
||||
class="context-max"
|
||||
aria-label="Maximum context"
|
||||
aria-valuemin={state.low()}
|
||||
aria-valuemax={state.max()}
|
||||
aria-valuenow={state.top()}
|
||||
disabled={state.max() === 0}
|
||||
role="slider"
|
||||
type="button"
|
||||
onKeyDown={(event) => key("max", event)}
|
||||
onPointerDown={(event) => drag("max", event)}
|
||||
/>
|
||||
</div>
|
||||
<small>{rangeLabel(state.low(), state.top(), state.max())}</small>
|
||||
</label>
|
||||
<details class="multi-check">
|
||||
<summary>{state.caps().length ? `${state.caps().length} capabilities` : "Capabilities"}</summary>
|
||||
<div class="multi-check-menu">
|
||||
<For each={state.capabilities}>
|
||||
{(cap) => (
|
||||
<label>
|
||||
<input type="checkbox" checked={state.caps().includes(cap)} onChange={() => state.toggle(cap)} />
|
||||
<span>{capLabel(cap)}</span>
|
||||
</label>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</details>
|
||||
</ConfigToolbar>
|
||||
<span class="model-star" classList={{ active: state.starred() }} aria-hidden="true" />
|
||||
Favorites
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="models">
|
||||
<div class="models-filter-row models-filter-secondary">
|
||||
<div class="models-context-filter">
|
||||
<span>Context max</span>
|
||||
<div
|
||||
class="context-range"
|
||||
style={`--context-min: 0%; --context-max: ${pct(state.top(), state.max())}%;`}
|
||||
>
|
||||
<span class="context-track" aria-hidden="true" />
|
||||
<span class="context-fill" aria-hidden="true" />
|
||||
<button
|
||||
class="context-max"
|
||||
aria-label="Maximum context"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax={state.max()}
|
||||
aria-valuenow={state.top()}
|
||||
disabled={state.max() === 0}
|
||||
role="slider"
|
||||
type="button"
|
||||
onKeyDown={(event) => key("max", event)}
|
||||
onPointerDown={(event) => drag("max", event)}
|
||||
/>
|
||||
</div>
|
||||
<strong class="models-context-value mono">{fmtContext(state.top())}</strong>
|
||||
</div>
|
||||
<div class="models-capabilities-filter">
|
||||
<span>Capabilities</span>
|
||||
<div class="models-capabilities-list">
|
||||
<For each={caps}>
|
||||
{(cap) => (
|
||||
<button
|
||||
class="models-capability-toggle"
|
||||
classList={{ selected: state.caps().includes(cap.key) }}
|
||||
type="button"
|
||||
aria-pressed={state.caps().includes(cap.key)}
|
||||
onClick={() => state.toggle(cap.key)}
|
||||
>
|
||||
{cap.label}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="models explore-models">
|
||||
<Show when={state.models().length} fallback={<p class="empty">No models match the current filters.</p>}>
|
||||
<For each={state.models()}>
|
||||
{(item) => (
|
||||
<article class="model">
|
||||
<article class="model explore-model-card">
|
||||
<div class="model-main">
|
||||
<div class="model-title">
|
||||
<button
|
||||
@@ -374,62 +413,25 @@ export function ModelsAvailableRoute() {
|
||||
<span>{item.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tags">
|
||||
<Tag>{item.provider.name}</Tag>
|
||||
<Tag>{item.model.isFree ? "free" : "paid"}</Tag>
|
||||
<div class="tags model-tags">
|
||||
<Tag tone="brand">{item.provider.id}</Tag>
|
||||
<Tag tone={item.model.isFree ? "success" : "neutral"}>{item.model.isFree ? "free" : "paid"}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={desc(item.model)}>{(value) => <p class="model-description">{value()}</p>}</Show>
|
||||
<div class="model-info-grid">
|
||||
<Show when={item.model.family}>
|
||||
<div class="model-stat">
|
||||
<span>Family</span>
|
||||
<strong>{title(item.model.family!)}</strong>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={item.model.release_date}>
|
||||
<div class="model-stat">
|
||||
<span>Released</span>
|
||||
<strong>{fmtDate(item.model.release_date)}</strong>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="model-stat">
|
||||
<span>Input</span>
|
||||
<strong>{fmtPrice(item.model.cost.input)}</strong>
|
||||
</div>
|
||||
<div class="model-stat">
|
||||
<span>Output</span>
|
||||
<strong>{fmtPrice(item.model.cost.output)}</strong>
|
||||
</div>
|
||||
<Show when={fmtCachedPrice(item.model.cost)}>
|
||||
{(value) => (
|
||||
<div class="model-stat">
|
||||
<span>Cached</span>
|
||||
<strong>{value()}</strong>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<div class="model-stat">
|
||||
<span>Avg Cost</span>
|
||||
<strong>{fmtPrice(avgPrice(item.model.cost))}</strong>
|
||||
</div>
|
||||
<div class="model-stat">
|
||||
<span>Context</span>
|
||||
<strong>{fmtContext(item.model.limit.context)}</strong>
|
||||
</div>
|
||||
<div class="model-stat">
|
||||
<span>Reasoning</span>
|
||||
<strong>{item.model.capabilities.reasoning ? "Yes" : "No"}</strong>
|
||||
</div>
|
||||
<div class="model-stat">
|
||||
<span>Input caps</span>
|
||||
<strong>{mods(item.model.capabilities.input)}</strong>
|
||||
</div>
|
||||
<div class="model-stat">
|
||||
<span>Output caps</span>
|
||||
<strong>{mods(item.model.capabilities.output)}</strong>
|
||||
</div>
|
||||
<Stat label="Family" value={item.model.family ? title(item.model.family) : "Unknown"} />
|
||||
<Stat label="Released" value={item.model.release_date ? fmtDate(item.model.release_date) : "Unknown"} />
|
||||
<Stat label="Context" value={fmtContext(item.model.limit.context)} mono />
|
||||
<Stat label="Input" value={money(item.model.cost.input)} sub="/ 1M tok" mono />
|
||||
<Stat label="Output" value={money(item.model.cost.output)} sub="/ 1M tok" mono />
|
||||
<Stat label="Reasoning" value={item.model.capabilities.reasoning ? "Yes" : "No"} />
|
||||
</div>
|
||||
<Show when={chips(item.model).length}>
|
||||
<div class="tags model-capabilities">
|
||||
<For each={chips(item.model)}>{(cap) => <Tag>{cap}</Tag>}</For>
|
||||
</div>
|
||||
</Show>
|
||||
</article>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -96,7 +96,7 @@ export function ProvidersRoute() {
|
||||
when={state.mode() === "form"}
|
||||
fallback={
|
||||
<>
|
||||
<header class="drawer-header add-provider-header">
|
||||
<header class="drawer-header">
|
||||
<div>
|
||||
<h2>Add provider</h2>
|
||||
<span>Choose a provider, then enter credentials.</span>
|
||||
@@ -155,7 +155,7 @@ export function ProvidersRoute() {
|
||||
</Show>
|
||||
</Card>
|
||||
</div>
|
||||
<footer class="drawer-footer add-provider-footer">
|
||||
<footer class="drawer-footer">
|
||||
<Button variant="ghost" onClick={state.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -280,7 +280,7 @@ export function ProvidersRoute() {
|
||||
</div>
|
||||
|
||||
<footer class="drawer-footer">
|
||||
<Button variant="secondary" onClick={state.close}>
|
||||
<Button variant="ghost" onClick={state.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" disabled={Boolean(state.ctx.saving())} onClick={state.save}>
|
||||
@@ -387,7 +387,7 @@ export function ProvidersRoute() {
|
||||
</div>
|
||||
|
||||
<footer class="drawer-footer">
|
||||
<Button variant="secondary" onClick={state.close}>
|
||||
<Button variant="ghost" onClick={state.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Show when={state.method()?.type === "api"}>
|
||||
|
||||
@@ -53,6 +53,16 @@ body {
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.kilo-console select option {
|
||||
background: var(--popover);
|
||||
color: var(--popover-foreground);
|
||||
}
|
||||
|
||||
.kilo-console select option:checked {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.kilo-console input:focus,
|
||||
.kilo-console select:focus,
|
||||
.kilo-console textarea:focus {
|
||||
|
||||
@@ -3,6 +3,266 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.kilo-console .models-filter-card {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--card);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.kilo-console .models-filter-row {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kilo-console .models-filter-primary {
|
||||
grid-template-columns: minmax(14rem, 1.6fr) repeat(3, minmax(8.75rem, 1fr)) auto;
|
||||
}
|
||||
|
||||
.kilo-console .models-filter-field {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kilo-console .models-filter-field > span {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kilo-console .models-filter-field input,
|
||||
.kilo-console .models-select summary,
|
||||
.kilo-console .models-favorite-filter,
|
||||
.kilo-console .models-capability-toggle {
|
||||
height: 2rem;
|
||||
min-height: 2rem;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.kilo-console .models-filter-field input {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.kilo-console .models-select {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kilo-console .models-select summary {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in oklab, var(--input) 35%, transparent);
|
||||
color: var(--foreground);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
padding: 0 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kilo-console .models-select summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kilo-console .models-select summary::marker {
|
||||
content: "";
|
||||
}
|
||||
|
||||
.kilo-console .models-select summary::after {
|
||||
flex: 0 0 auto;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 6L8 10L12 6' stroke='%23a1a1aa' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1rem;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.kilo-console .models-select[open] summary,
|
||||
.kilo-console .models-select summary:focus-visible {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--primary) 25%, transparent);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.kilo-console .models-select-menu {
|
||||
position: absolute;
|
||||
z-index: 35;
|
||||
top: calc(100% + 0.25rem);
|
||||
left: 0;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-height: 14rem;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--popover);
|
||||
box-shadow: 0 1rem 2rem color-mix(in oklab, black 24%, transparent);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.kilo-console .models-select-option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 1.75rem;
|
||||
border: 0;
|
||||
border-radius: calc(var(--radius-md) - 0.125rem);
|
||||
background: transparent;
|
||||
color: var(--popover-foreground);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
padding: 0.25rem 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.kilo-console .models-select-option:hover,
|
||||
.kilo-console .models-select-option:focus-visible {
|
||||
background: color-mix(in oklab, var(--primary) 12%, transparent);
|
||||
color: var(--foreground);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.kilo-console .models-select-option.selected {
|
||||
background: color-mix(in oklab, var(--primary) 20%, transparent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.kilo-console .models-favorite-filter,
|
||||
.kilo-console .models-capability-toggle {
|
||||
display: inline-flex;
|
||||
gap: 0.375rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in oklab, var(--input) 35%, transparent);
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.kilo-console .models-favorite-filter:hover,
|
||||
.kilo-console .models-capability-toggle:hover,
|
||||
.kilo-console .models-favorite-filter.selected,
|
||||
.kilo-console .models-capability-toggle.selected {
|
||||
border-color: var(--primary);
|
||||
background: color-mix(in oklab, var(--primary) 10%, transparent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.kilo-console .models-favorite-filter .model-star {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.kilo-console .models-filter-secondary {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kilo-console .models-context-filter {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
min-width: 16rem;
|
||||
}
|
||||
|
||||
.kilo-console .models-context-filter > span,
|
||||
.kilo-console .models-capabilities-filter > span {
|
||||
flex: 0 0 auto;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kilo-console .models-context-filter .context-range {
|
||||
flex: 1 1 auto;
|
||||
height: 1rem;
|
||||
min-height: 1rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.kilo-console .models-context-filter .context-track,
|
||||
.kilo-console .models-context-filter .context-fill {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 0.25rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.kilo-console .models-context-filter .context-track {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.kilo-console .models-context-filter .context-fill {
|
||||
right: calc(100% - var(--context-max));
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.kilo-console .models-context-filter .context-max {
|
||||
top: 50%;
|
||||
left: var(--context-max);
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
min-height: 0.875rem;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--foreground);
|
||||
box-shadow: 0 0 0 1px color-mix(in oklab, var(--background) 70%, transparent);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.kilo-console .models-context-value {
|
||||
flex: 0 0 3.5rem;
|
||||
color: var(--foreground);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.45;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.kilo-console .models-capabilities-filter {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kilo-console .models-capabilities-list {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.kilo-console .explore-models {
|
||||
grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr));
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.kilo-console .model {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
@@ -12,11 +272,11 @@
|
||||
}
|
||||
|
||||
.kilo-console .model strong,
|
||||
.kilo-console .model span {
|
||||
.kilo-console .model span:where(:not([data-component="tag"])) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.kilo-console .model span {
|
||||
.kilo-console .model span:where(:not([data-component="tag"])) {
|
||||
color: var(--text-weaker);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
@@ -38,6 +298,7 @@
|
||||
|
||||
.kilo-console .model-star {
|
||||
flex: 0 0 auto;
|
||||
display: block;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
border: 0;
|
||||
@@ -116,21 +377,67 @@
|
||||
.kilo-console .model-description {
|
||||
margin: 0;
|
||||
color: var(--text-weak);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.kilo-console .explore-model-card {
|
||||
gap: 0.625rem;
|
||||
border-color: var(--border);
|
||||
background: var(--card);
|
||||
padding: 0.875rem;
|
||||
}
|
||||
|
||||
.kilo-console .explore-model-card .model-main {
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.kilo-console .explore-model-card .model-title {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.kilo-console .explore-model-card .model-title strong {
|
||||
color: var(--foreground);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.kilo-console .explore-model-card .model-title span {
|
||||
margin-top: 0.125rem;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.kilo-console .explore-model-card .model-tags {
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.kilo-console .model-capabilities {
|
||||
justify-content: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.kilo-console .model-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
overflow: hidden;
|
||||
gap: 1px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.kilo-console .model-stat {
|
||||
border: 1px solid var(--border-weak-base);
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--background-stronger) 42%, transparent);
|
||||
padding: 0.55rem 0.65rem;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: var(--card);
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
.kilo-console .model-stat span,
|
||||
@@ -140,15 +447,25 @@
|
||||
|
||||
.kilo-console .model-stat span {
|
||||
color: var(--text-weaker);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.kilo-console .model-stat strong {
|
||||
margin-top: 0.15rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-items: baseline;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--text-strong);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.kilo-console .model-stat small {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.625rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -116,42 +116,38 @@
|
||||
box-shadow: -1.5rem 0 4rem color-mix(in srgb, black 22%, transparent);
|
||||
}
|
||||
|
||||
.kilo-console .drawer-header,
|
||||
.kilo-console .drawer-footer {
|
||||
.kilo-console .drawer-header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border-weak-base);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.kilo-console .add-provider-header {
|
||||
padding: 1.25rem 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.kilo-console .add-provider-header h2 {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.kilo-console .add-provider-header span {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
}
|
||||
|
||||
.kilo-console .drawer-header h2 {
|
||||
margin: 0 0 0.25rem;
|
||||
color: var(--text-strong);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.kilo-console .drawer-header span {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.kilo-console .drawer-footer {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--border-weak-base);
|
||||
border-bottom: 0;
|
||||
margin-top: auto;
|
||||
padding: 0.75rem 1.25rem;
|
||||
}
|
||||
|
||||
.kilo-console .drawer-search {
|
||||
@@ -182,9 +178,9 @@
|
||||
.kilo-console .add-provider-field h3 {
|
||||
padding: 0 16px;
|
||||
color: var(--foreground);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.kilo-console .add-provider-search {
|
||||
@@ -216,6 +212,8 @@
|
||||
|
||||
.kilo-console .add-provider-picker {
|
||||
flex: 1 1 auto;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.375rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0;
|
||||
@@ -241,11 +239,27 @@
|
||||
|
||||
.kilo-console .add-provider-option {
|
||||
width: auto;
|
||||
min-height: 4.75rem;
|
||||
min-height: 3rem;
|
||||
border-color: var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card);
|
||||
padding: 0.625rem 0.75rem;
|
||||
margin-right: 0;
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
.kilo-console .provider-option > div:not(.tags) > strong,
|
||||
.kilo-console .provider-option .model-title strong {
|
||||
color: var(--foreground);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.kilo-console .provider-option > div:not(.tags) > span,
|
||||
.kilo-console .provider-option .model-title span {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.kilo-console .add-provider-option.selected {
|
||||
@@ -261,15 +275,17 @@
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.kilo-console .provider-option strong,
|
||||
.kilo-console .provider-option span {
|
||||
.kilo-console .provider-option > div:not(.tags) > strong,
|
||||
.kilo-console .provider-option > div:not(.tags) > span,
|
||||
.kilo-console .provider-option .model-title strong,
|
||||
.kilo-console .provider-option .model-title span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.kilo-console .provider-option span {
|
||||
.kilo-console .provider-option > div:not(.tags) > span,
|
||||
.kilo-console .provider-option .model-title span {
|
||||
overflow: hidden;
|
||||
color: var(--text-weaker);
|
||||
font-size: var(--font-size-small);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -287,10 +303,6 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.kilo-console .add-provider-footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.kilo-console .provider-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
.kilo-console .model-defaults {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.kilo-console .model-default-fields {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@media (max-width: 980px) {
|
||||
.kilo-console .config-page-header,
|
||||
.kilo-console .config-toolbar,
|
||||
.kilo-console .models-filter-primary,
|
||||
.kilo-console .details,
|
||||
.kilo-console .model-controls,
|
||||
.kilo-console .builder,
|
||||
@@ -42,6 +43,7 @@
|
||||
}
|
||||
|
||||
.kilo-console .compact,
|
||||
.kilo-console .model-defaults,
|
||||
.kilo-console .mini-list.columns,
|
||||
.kilo-console .mini-item,
|
||||
.kilo-console .mini-item.simple {
|
||||
@@ -51,9 +53,30 @@
|
||||
.kilo-console .metrics {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.kilo-console .models-filter-secondary {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kilo-console .models-context-filter,
|
||||
.kilo-console .models-capabilities-filter {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.kilo-console .explore-models,
|
||||
.kilo-console .model-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.kilo-console .models-capabilities-filter {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kilo-console .metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user