@lynn123411/dsh-model-submenu 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DETAILS.md +8 -2
- package/README.md +10 -8
- package/lib/client.js +1158 -326
- package/lib/client.js.map +1 -1
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +26 -20
- package/src/client/ModelPicker.tsx +305 -0
- package/src/client/ModelRow.tsx +127 -0
- package/src/client/ModelSubmenu.module.css +338 -97
- package/src/client/ModelSubmenu.tsx +237 -233
- package/src/client/effort-memory.ts +90 -0
- package/src/client/facts-source.ts +93 -0
- package/src/client/favorites.ts +103 -0
- package/src/client/icons.tsx +36 -0
- package/src/client/index.ts +29 -8
- package/src/client/locales.ts +66 -33
- package/src/client/model-facts.ts +198 -0
- package/src/client/rows.ts +125 -0
- package/src/client/selection.ts +112 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-model reasoning level this browser last chose.
|
|
3
|
+
*
|
|
4
|
+
* The Host stores one level per session, so switching models away and back
|
|
5
|
+
* forgets what was picked for the first one. This module keeps that memory
|
|
6
|
+
* beside the favorite table, keyed the same way: provider route key plus model
|
|
7
|
+
* id. Remembering the provider default removes the row, so "follow the
|
|
8
|
+
* provider" stays distinguishable from "remembered a level".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** One remembered reasoning level. */
|
|
12
|
+
export interface RememberedEffort {
|
|
13
|
+
readonly provider: string
|
|
14
|
+
readonly model: string
|
|
15
|
+
readonly effort: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The single browser-wide storage key holding the whole memory. */
|
|
19
|
+
export const EFFORTS_KEY = 'dsh-model-submenu.efforts'
|
|
20
|
+
|
|
21
|
+
/** The part of `Storage` this module uses; absent or throwing reads mean "none". */
|
|
22
|
+
export type EffortStorage = Pick<Storage, 'getItem' | 'setItem'>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read the memory, keeping only rows naming both halves and a level.
|
|
26
|
+
* @param storage - browser storage, or undefined when the browser withholds it.
|
|
27
|
+
* @returns the remembered rows; empty when unreadable.
|
|
28
|
+
*/
|
|
29
|
+
export function readEfforts(storage: EffortStorage | undefined): RememberedEffort[] {
|
|
30
|
+
const raw = getItem(storage)
|
|
31
|
+
if (raw === null) return []
|
|
32
|
+
let parsed: unknown
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw)
|
|
35
|
+
} catch {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(parsed)) return []
|
|
39
|
+
return parsed.filter((row): row is RememberedEffort => typeof row === 'object' && row !== null
|
|
40
|
+
&& typeof (row as RememberedEffort).provider === 'string'
|
|
41
|
+
&& typeof (row as RememberedEffort).model === 'string'
|
|
42
|
+
&& typeof (row as RememberedEffort).effort === 'string'
|
|
43
|
+
&& (row as RememberedEffort).effort !== '')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param memory - the rows to search.
|
|
48
|
+
* @param provider - the provider route key.
|
|
49
|
+
* @param model - the model id inside that provider.
|
|
50
|
+
* @returns the level last chosen for that exact model, or undefined.
|
|
51
|
+
*/
|
|
52
|
+
export function effortFor(
|
|
53
|
+
memory: readonly RememberedEffort[],
|
|
54
|
+
provider: string,
|
|
55
|
+
model: string,
|
|
56
|
+
): string | undefined {
|
|
57
|
+
return memory.find((row) => row.provider === provider && row.model === model)?.effort
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Remember one model's level, or forget it when the choice is the provider default.
|
|
62
|
+
* @param storage - browser storage, or undefined when writes are unavailable.
|
|
63
|
+
* @param provider - the provider route key.
|
|
64
|
+
* @param model - the model id inside that provider.
|
|
65
|
+
* @param effort - the chosen level, undefined meaning the provider default.
|
|
66
|
+
* @returns the memory after the change, whether or not the write landed.
|
|
67
|
+
*/
|
|
68
|
+
export function rememberEffort(
|
|
69
|
+
storage: EffortStorage | undefined,
|
|
70
|
+
provider: string,
|
|
71
|
+
model: string,
|
|
72
|
+
effort: string | undefined,
|
|
73
|
+
): RememberedEffort[] {
|
|
74
|
+
const rest = readEfforts(storage).filter((row) => row.provider !== provider || row.model !== model)
|
|
75
|
+
const next = effort === undefined ? rest : [...rest, { provider, model, effort }]
|
|
76
|
+
try {
|
|
77
|
+
storage?.setItem(EFFORTS_KEY, JSON.stringify(next))
|
|
78
|
+
} catch {
|
|
79
|
+
// A full quota or a locked-down browser loses the memory, not the menu.
|
|
80
|
+
}
|
|
81
|
+
return next
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getItem(storage: EffortStorage | undefined): string | null {
|
|
85
|
+
try {
|
|
86
|
+
return storage?.getItem(EFFORTS_KEY) ?? null
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's live answer to "what does the settings document know about this
|
|
3
|
+
* model?".
|
|
4
|
+
*
|
|
5
|
+
* Two host answers meet here: the shared settings mirror (which the settings
|
|
6
|
+
* page already keeps warm) and the LLM provider directory that says which
|
|
7
|
+
* settings namespace holds each provider's profile. Both can change while the
|
|
8
|
+
* page is open — a saved profile, a reconnected host — so the join is rebuilt
|
|
9
|
+
* whenever the mirror or the directory reports a change, and readers subscribe
|
|
10
|
+
* to the lookup itself.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ModelFacts } from './model-facts.ts'
|
|
14
|
+
import {
|
|
15
|
+
buildModelFacts,
|
|
16
|
+
type ConfigurableProviderRow,
|
|
17
|
+
type NamespaceRow,
|
|
18
|
+
} from './model-facts.ts'
|
|
19
|
+
|
|
20
|
+
/** One lookup answering both metadata fields for a provider and model. */
|
|
21
|
+
export type ModelFactsLookup = (provider: string, model: string) => ModelFacts
|
|
22
|
+
|
|
23
|
+
/** The mirror face this source reads; a subset of the settings provider's. */
|
|
24
|
+
export interface DescribeFace {
|
|
25
|
+
getSnapshot(): { readonly view: { readonly namespaces: readonly NamespaceRow[] } | undefined }
|
|
26
|
+
subscribe(listener: () => void): () => void
|
|
27
|
+
ensure(): Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One remote answer, as the gateway returns it. */
|
|
31
|
+
export interface DirectoryAnswer {
|
|
32
|
+
readonly ok: boolean
|
|
33
|
+
readonly value?: readonly ConfigurableProviderRow[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A stable, subscribable lookup over the current settings answer. */
|
|
37
|
+
export interface FactsSource {
|
|
38
|
+
/** @returns the lookup for the current answer; the identity changes on every rebuild. */
|
|
39
|
+
getSnapshot(): ModelFactsLookup
|
|
40
|
+
/** @param listener - called after each rebuild. @returns the disposer. */
|
|
41
|
+
subscribe(listener: () => void): () => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const EMPTY: ModelFactsLookup = () => ({ contextWindow: undefined, images: 'unknown' })
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Join the settings mirror with the provider directory.
|
|
48
|
+
* @param deps.describe - the settings mirror's describe face.
|
|
49
|
+
* @param deps.listProviders - reads the LLM provider directory once.
|
|
50
|
+
* @returns the source, plus `start` which wires both reads and returns its disposer.
|
|
51
|
+
*/
|
|
52
|
+
export function createFactsSource(deps: {
|
|
53
|
+
readonly describe: DescribeFace
|
|
54
|
+
readonly listProviders: () => Promise<DirectoryAnswer>
|
|
55
|
+
}): { readonly source: FactsSource; readonly start: () => () => void } {
|
|
56
|
+
const listeners = new Set<() => void>()
|
|
57
|
+
let providers: readonly ConfigurableProviderRow[] = []
|
|
58
|
+
let lookup: ModelFactsLookup = EMPTY
|
|
59
|
+
|
|
60
|
+
const rebuild = () => {
|
|
61
|
+
const namespaces = deps.describe.getSnapshot().view?.namespaces ?? []
|
|
62
|
+
lookup = providers.length === 0 || namespaces.length === 0
|
|
63
|
+
? EMPTY
|
|
64
|
+
: buildModelFacts(providers, namespaces)
|
|
65
|
+
for (const listener of [...listeners]) listener()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
source: {
|
|
70
|
+
getSnapshot: () => lookup,
|
|
71
|
+
subscribe: (listener) => {
|
|
72
|
+
listeners.add(listener)
|
|
73
|
+
return () => { listeners.delete(listener) }
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
start: () => {
|
|
77
|
+
const off = deps.describe.subscribe(rebuild)
|
|
78
|
+
void deps.describe.ensure().catch(() => { /* a failed read leaves the empty lookup in place */ })
|
|
79
|
+
void deps.listProviders().then(
|
|
80
|
+
(answer) => {
|
|
81
|
+
providers = answer.ok && answer.value !== undefined ? answer.value : []
|
|
82
|
+
rebuild()
|
|
83
|
+
},
|
|
84
|
+
() => { /* the directory being unavailable leaves every model's facts unknown */ },
|
|
85
|
+
)
|
|
86
|
+
rebuild()
|
|
87
|
+
return () => {
|
|
88
|
+
off()
|
|
89
|
+
listeners.clear()
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The favorite-model table the picker keeps in the browser.
|
|
3
|
+
*
|
|
4
|
+
* One browser-wide key holds `{ provider, model }` rows: the provider route key
|
|
5
|
+
* and the model id are the catalog's own identity, so a favorite survives a
|
|
6
|
+
* display-name change and never collides across providers. A value that cannot
|
|
7
|
+
* be read or does not parse reads as an empty table — a privacy-mode browser or
|
|
8
|
+
* a hand-edited row must never keep the menu from opening. Rows whose provider
|
|
9
|
+
* leaves the catalog are kept, so restoring the provider restores the star.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ModelRef } from './rows.ts'
|
|
13
|
+
|
|
14
|
+
/** One favorited model, identified the way the catalog identifies it. */
|
|
15
|
+
export type FavoriteModel = ModelRef
|
|
16
|
+
|
|
17
|
+
/** The single browser-wide storage key holding the whole table. */
|
|
18
|
+
export const FAVORITES_KEY = 'dsh-model-submenu.favorites'
|
|
19
|
+
|
|
20
|
+
/** The part of `Storage` this module uses; absent or throwing reads mean "none". */
|
|
21
|
+
export type FavoritesStorage = Pick<Storage, 'getItem' | 'setItem'>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read the table, keeping only rows that name both halves.
|
|
25
|
+
* @param storage - browser storage, or undefined when the browser withholds it.
|
|
26
|
+
* @returns the stored favorites in stored order; empty when unreadable.
|
|
27
|
+
*/
|
|
28
|
+
export function readFavorites(storage: FavoritesStorage | undefined): FavoriteModel[] {
|
|
29
|
+
const raw = getItem(storage)
|
|
30
|
+
if (raw === null) return []
|
|
31
|
+
let parsed: unknown
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(raw)
|
|
34
|
+
} catch {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(parsed)) return []
|
|
38
|
+
return parsed.filter((row): row is FavoriteModel => typeof row === 'object' && row !== null
|
|
39
|
+
&& typeof (row as FavoriteModel).provider === 'string'
|
|
40
|
+
&& typeof (row as FavoriteModel).model === 'string')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Add or remove one model, writing the whole table back.
|
|
45
|
+
* @param storage - browser storage, or undefined when writes are unavailable.
|
|
46
|
+
* @param provider - the provider route key.
|
|
47
|
+
* @param model - the model id inside that provider.
|
|
48
|
+
* @returns the table after the change, whether or not the write landed.
|
|
49
|
+
*/
|
|
50
|
+
export function toggleFavorite(
|
|
51
|
+
storage: FavoritesStorage | undefined,
|
|
52
|
+
provider: string,
|
|
53
|
+
model: string,
|
|
54
|
+
): FavoriteModel[] {
|
|
55
|
+
const current = readFavorites(storage)
|
|
56
|
+
const next = isFavorite(current, provider, model)
|
|
57
|
+
? current.filter((row) => row.provider !== provider || row.model !== model)
|
|
58
|
+
: [...current, { provider, model }]
|
|
59
|
+
try {
|
|
60
|
+
storage?.setItem(FAVORITES_KEY, JSON.stringify(next))
|
|
61
|
+
} catch {
|
|
62
|
+
// A full quota or a locked-down browser loses the star, not the menu.
|
|
63
|
+
}
|
|
64
|
+
return next
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param favorites - the table to search.
|
|
69
|
+
* @param provider - the provider route key.
|
|
70
|
+
* @param model - the model id inside that provider.
|
|
71
|
+
* @returns whether that exact pair is favorited.
|
|
72
|
+
*/
|
|
73
|
+
export function isFavorite(
|
|
74
|
+
favorites: readonly FavoriteModel[],
|
|
75
|
+
provider: string,
|
|
76
|
+
model: string,
|
|
77
|
+
): boolean {
|
|
78
|
+
return favorites.some((row) => row.provider === provider && row.model === model)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stable-partition a catalog so favorited rows lead, each part keeping the
|
|
83
|
+
* catalog's own order.
|
|
84
|
+
* @param rows - the catalog in its display order.
|
|
85
|
+
* @param favorited - predicate answering whether one row is favorited.
|
|
86
|
+
* @returns a new list; the input order when nothing is favorited.
|
|
87
|
+
*/
|
|
88
|
+
export function orderWithFavorites<T>(
|
|
89
|
+
rows: readonly T[],
|
|
90
|
+
favorited: (row: T) => boolean,
|
|
91
|
+
): readonly T[] {
|
|
92
|
+
const starred = rows.filter(favorited)
|
|
93
|
+
if (starred.length === 0) return rows
|
|
94
|
+
return [...starred, ...rows.filter((row) => !favorited(row))]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getItem(storage: FavoritesStorage | undefined): string | null {
|
|
98
|
+
try {
|
|
99
|
+
return storage?.getItem(FAVORITES_KEY) ?? null
|
|
100
|
+
} catch {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one glyph this plugin draws itself.
|
|
3
|
+
*
|
|
4
|
+
* The shared icon library carries no star, so the favorite mark is defined here:
|
|
5
|
+
* a five-point star whose outline and fill use the same path, which keeps the
|
|
6
|
+
* hover-only outline and the standing filled state identical in geometry.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render the favorite star.
|
|
11
|
+
* @param props.filled - draw the solid star; otherwise the outline.
|
|
12
|
+
* @param props.size - square edge in pixels, matching the shared icon set's default.
|
|
13
|
+
* @param props.className - extra class for colour and placement.
|
|
14
|
+
* @returns the inline SVG.
|
|
15
|
+
*/
|
|
16
|
+
export function StarIcon({ filled, size = 14, className }: {
|
|
17
|
+
readonly filled: boolean
|
|
18
|
+
readonly size?: number | undefined
|
|
19
|
+
readonly className?: string | undefined
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<svg
|
|
23
|
+
width={size}
|
|
24
|
+
height={size}
|
|
25
|
+
viewBox="0 0 24 24"
|
|
26
|
+
fill={filled ? 'currentColor' : 'none'}
|
|
27
|
+
stroke="currentColor"
|
|
28
|
+
strokeWidth={filled ? 0 : 1.6}
|
|
29
|
+
strokeLinejoin="round"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
className={className}
|
|
32
|
+
>
|
|
33
|
+
<path d="M12 3.2l2.7 5.6 6.1.85-4.45 4.3 1.08 6.05L12 17.1l-5.43 2.9 1.08-6.05L3.2 9.65l6.1-.85z" />
|
|
34
|
+
</svg>
|
|
35
|
+
)
|
|
36
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -2,26 +2,41 @@ import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
|
2
2
|
// Type-only: `slots` and its LocaleNamespaceMap merge come from the renderer and
|
|
3
3
|
// the slot package; the composer seat's SlotMap entry and owner props from
|
|
4
4
|
// ui-conversation; the `modelDirectories` service and the seat's injected face
|
|
5
|
-
// from ui-model-selection's browser half
|
|
5
|
+
// from ui-model-selection's browser half; `configForms` from the settings
|
|
6
|
+
// provider, whose shared mirror answers the metadata line.
|
|
6
7
|
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
7
8
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
8
9
|
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
9
10
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
10
11
|
import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'
|
|
12
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
11
13
|
import { ModelSubmenu } from './ModelSubmenu.tsx'
|
|
14
|
+
import { createFactsSource, type DescribeFace } from './facts-source.ts'
|
|
12
15
|
import { NS, en, zh } from './locales.ts'
|
|
13
16
|
|
|
14
17
|
export { ModelSubmenu } from './ModelSubmenu.tsx'
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Cordis services required by the browser half. `slots` is the contribution
|
|
18
|
-
* registry, `sessions` tells whether a session may select a model,
|
|
21
|
+
* registry, `sessions` tells whether a session may select a model,
|
|
19
22
|
* `modelDirectories` owns the per-session catalog snapshot this seat renders
|
|
20
|
-
* from
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* from, and `configForms` carries the settings provider's shared describe
|
|
24
|
+
* mirror, which answers each model's context window and accepted modalities.
|
|
25
|
+
* `remote`, `remote.llm`, and `remote.session` are declared because a service
|
|
26
|
+
* method runs on the calling context: `directoryFor` reads
|
|
27
|
+
* `this.ctx.remote.session`, and the provider directory that names each
|
|
28
|
+
* settings address is read through `this.ctx.remote.llm`.
|
|
23
29
|
*/
|
|
24
|
-
export const inject = [
|
|
30
|
+
export const inject = [
|
|
31
|
+
'configForms',
|
|
32
|
+
'locale',
|
|
33
|
+
'modelDirectories',
|
|
34
|
+
'remote',
|
|
35
|
+
'remote.llm',
|
|
36
|
+
'remote.session',
|
|
37
|
+
'sessions',
|
|
38
|
+
'slots',
|
|
39
|
+
]
|
|
25
40
|
|
|
26
41
|
/**
|
|
27
42
|
* Client plugin body: register this plugin's dictionaries, then take over the
|
|
@@ -31,9 +46,14 @@ export const inject = ['locale', 'modelDirectories', 'remote', 'remote.session',
|
|
|
31
46
|
*/
|
|
32
47
|
export function apply(ctx: ClientContext): void {
|
|
33
48
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-model-submenu: dictionaries')
|
|
34
|
-
ctx.inject(['slots', 'modelDirectories'], (scope) => {
|
|
49
|
+
ctx.inject(['slots', 'modelDirectories', 'configForms', 'remote'], (scope) => {
|
|
35
50
|
const models = scope.modelDirectories
|
|
36
51
|
const sessions = scope.sessions
|
|
52
|
+
const facts = createFactsSource({
|
|
53
|
+
describe: scope.configForms.describe() as DescribeFace,
|
|
54
|
+
listProviders: () => scope.remote.llm.listConfigurableProviders(),
|
|
55
|
+
})
|
|
56
|
+
ctx.effect(() => facts.start(), 'dsh-model-submenu: model metadata')
|
|
37
57
|
scope.slots.inject('conversation.input.model', () => scope.slots.register({
|
|
38
58
|
name: 'conversation.input.model',
|
|
39
59
|
priority: -100,
|
|
@@ -48,6 +68,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
48
68
|
return {
|
|
49
69
|
available,
|
|
50
70
|
directory: directory.store,
|
|
71
|
+
factsSource: facts.source,
|
|
51
72
|
load: () => {
|
|
52
73
|
if (available) directory.load().catch(() => {})
|
|
53
74
|
},
|
|
@@ -56,4 +77,4 @@ export function apply(ctx: ClientContext): void {
|
|
|
56
77
|
},
|
|
57
78
|
}, ModelSubmenu))
|
|
58
79
|
})
|
|
59
|
-
}
|
|
80
|
+
}
|
package/src/client/locales.ts
CHANGED
|
@@ -1,76 +1,109 @@
|
|
|
1
|
-
/** Locale bundles for the composer model
|
|
1
|
+
/** Locale bundles for the composer model menu and the picker it opens. */
|
|
2
2
|
|
|
3
3
|
/** Dictionary namespace owned by this plugin. */
|
|
4
4
|
export const NS = 'modelSubmenu'
|
|
5
5
|
|
|
6
|
-
/** Locale keys the
|
|
6
|
+
/** Locale keys the menu and the picker render. */
|
|
7
7
|
export type ModelSubmenuLocaleKey =
|
|
8
|
-
| 'back'
|
|
9
|
-
| 'provider.modelCount'
|
|
10
|
-
| 'action.reload'
|
|
11
8
|
| 'trigger.fallback'
|
|
12
9
|
| 'trigger.loading'
|
|
13
10
|
| 'trigger.selectAria'
|
|
14
11
|
| 'trigger.aria'
|
|
15
12
|
| 'trigger.ariaEffort'
|
|
16
13
|
| 'menu.aria'
|
|
17
|
-
| 'menu.provider'
|
|
18
14
|
| 'menu.effort'
|
|
15
|
+
| 'menu.more'
|
|
16
|
+
| 'menu.hint'
|
|
19
17
|
| 'effort.providerDefault'
|
|
20
18
|
| 'status.loading'
|
|
19
|
+
| 'status.empty'
|
|
20
|
+
| 'status.failed'
|
|
21
|
+
| 'status.providerFailed'
|
|
22
|
+
| 'action.reload'
|
|
21
23
|
| 'error.action'
|
|
22
24
|
| 'error.sessionInUse'
|
|
23
|
-
| '
|
|
24
|
-
| '
|
|
25
|
-
| '
|
|
25
|
+
| 'picker.title'
|
|
26
|
+
| 'picker.close'
|
|
27
|
+
| 'picker.search'
|
|
28
|
+
| 'picker.searchEmpty'
|
|
29
|
+
| 'picker.all'
|
|
30
|
+
| 'picker.favorite'
|
|
31
|
+
| 'picker.unfavorite'
|
|
32
|
+
| 'picker.hintNavigate'
|
|
33
|
+
| 'picker.hintSelect'
|
|
34
|
+
| 'picker.hintClose'
|
|
35
|
+
| 'meta.effort'
|
|
36
|
+
| 'meta.images'
|
|
26
37
|
|
|
27
38
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
28
39
|
interface LocaleNamespaceMap {
|
|
29
|
-
/** The composer model
|
|
40
|
+
/** The composer model menu's copy. */
|
|
30
41
|
modelSubmenu: ModelSubmenuLocaleKey
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
35
46
|
export const zh: Record<ModelSubmenuLocaleKey, string> = {
|
|
36
|
-
'back': '返回',
|
|
37
|
-
'provider.modelCount': '{count} 个模型',
|
|
38
|
-
'action.reload': '重新加载',
|
|
39
47
|
'trigger.fallback': '选择模型',
|
|
40
48
|
'trigger.loading': '正在加载模型…',
|
|
41
49
|
'trigger.selectAria': '选择模型',
|
|
42
50
|
'trigger.aria': '选择模型,当前 {model}',
|
|
43
|
-
'trigger.ariaEffort': '选择模型,当前 {model}
|
|
44
|
-
'menu.aria': '
|
|
45
|
-
'menu.
|
|
46
|
-
'menu.
|
|
47
|
-
'
|
|
48
|
-
'
|
|
51
|
+
'trigger.ariaEffort': '选择模型,当前 {model},思考等级 {effort}',
|
|
52
|
+
'menu.aria': '模型与思考等级',
|
|
53
|
+
'menu.effort': '思考',
|
|
54
|
+
'menu.more': '更多模型…',
|
|
55
|
+
'menu.hint': '提示:切换模型或思考程度会使既有的提示词缓存失效。建议新建会话,避免额外的 token 消耗。',
|
|
56
|
+
'effort.providerDefault': '默认',
|
|
57
|
+
'status.loading': '正在加载模型…',
|
|
58
|
+
'status.empty': '没有可用的模型。',
|
|
59
|
+
'status.failed': '模型列表加载失败:{message}',
|
|
60
|
+
'status.providerFailed': '{name} 加载失败:{message}',
|
|
61
|
+
'action.reload': '重新加载',
|
|
49
62
|
'error.action': '模型操作失败:{message}',
|
|
50
63
|
'error.sessionInUse': '当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。',
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
64
|
+
'picker.title': '切换模型',
|
|
65
|
+
'picker.close': '关闭',
|
|
66
|
+
'picker.search': '搜索模型或提供商…',
|
|
67
|
+
'picker.searchEmpty': '没有匹配的模型。',
|
|
68
|
+
'picker.all': '全部',
|
|
69
|
+
'picker.favorite': '收藏 {model}',
|
|
70
|
+
'picker.unfavorite': '取消收藏 {model}',
|
|
71
|
+
'picker.hintNavigate': '导航',
|
|
72
|
+
'picker.hintSelect': '选择',
|
|
73
|
+
'picker.hintClose': '关闭',
|
|
74
|
+
'meta.effort': '思考',
|
|
75
|
+
'meta.images': '图片输入',
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
/** English dictionary, checked complete against the zh key set. */
|
|
57
79
|
export const en: Record<ModelSubmenuLocaleKey, string> = {
|
|
58
|
-
'back': 'Back',
|
|
59
|
-
'provider.modelCount': '{count} models',
|
|
60
|
-
'action.reload': 'Reload',
|
|
61
80
|
'trigger.fallback': 'Select model',
|
|
62
81
|
'trigger.loading': 'Loading models…',
|
|
63
82
|
'trigger.selectAria': 'Select model',
|
|
64
83
|
'trigger.aria': 'Select model, current {model}',
|
|
65
|
-
'trigger.ariaEffort': 'Select model, current {model},
|
|
66
|
-
'menu.aria': '
|
|
67
|
-
'menu.provider': 'Provider',
|
|
84
|
+
'trigger.ariaEffort': 'Select model, current {model}, effort {effort}',
|
|
85
|
+
'menu.aria': 'Model and effort',
|
|
68
86
|
'menu.effort': 'Effort',
|
|
87
|
+
'menu.more': 'More models…',
|
|
88
|
+
'menu.hint': 'Changing the model or the effort invalidates the existing prompt cache. Start a new session to avoid the extra token cost.',
|
|
69
89
|
'effort.providerDefault': 'Default',
|
|
70
|
-
'status.loading': '
|
|
90
|
+
'status.loading': 'Loading models…',
|
|
91
|
+
'status.empty': 'No models available.',
|
|
92
|
+
'status.failed': 'Model list failed to load: {message}',
|
|
93
|
+
'status.providerFailed': '{name} failed to load: {message}',
|
|
94
|
+
'action.reload': 'Reload',
|
|
71
95
|
'error.action': 'Model operation failed: {message}',
|
|
72
96
|
'error.sessionInUse': 'This session is already in use, possibly by another running DSH instance (such as dsh web or the desktop app). Quit other running DSH instances and try again.',
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
|
|
97
|
+
'picker.title': 'Switch model',
|
|
98
|
+
'picker.close': 'Close',
|
|
99
|
+
'picker.search': 'Search models or providers…',
|
|
100
|
+
'picker.searchEmpty': 'No matching models.',
|
|
101
|
+
'picker.all': 'All',
|
|
102
|
+
'picker.favorite': 'Favorite {model}',
|
|
103
|
+
'picker.unfavorite': 'Remove {model} from favorites',
|
|
104
|
+
'picker.hintNavigate': 'navigate',
|
|
105
|
+
'picker.hintSelect': 'select',
|
|
106
|
+
'picker.hintClose': 'close',
|
|
107
|
+
'meta.effort': 'Effort',
|
|
108
|
+
'meta.images': 'Images',
|
|
109
|
+
}
|