mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 04:18:19 +00:00
feat: support custom GHCP model selection
This commit is contained in:
1 parent
1aacbfd2a8
commit
631a515986
7 files changed
+324
-26
No files matched your search
@@ -72,6 +72,17 @@ interface ProviderModelSelectorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CUSTOM_MODEL_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$/;
|
||||
const CUSTOM_MODEL_ID_SEPARATOR_PATTERN = /[._:/-]/;
|
||||
|
||||
function isCustomModelId(value: string): boolean {
|
||||
const trimmedValue = value.trim();
|
||||
return (
|
||||
CUSTOM_MODEL_ID_PATTERN.test(trimmedValue) &&
|
||||
CUSTOM_MODEL_ID_SEPARATOR_PATTERN.test(trimmedValue)
|
||||
);
|
||||
}
|
||||
|
||||
function PaidBadge({ label }: { label: string }) {
|
||||
return (
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1">
|
||||
@@ -396,6 +407,25 @@ export function FlexibleModelSelector({
|
||||
() => routingHints.get(normalizeModelValue(value, routing).toLowerCase()),
|
||||
[routing, routingHints, value]
|
||||
);
|
||||
const createCustomModelOption = (query: string) => {
|
||||
const customModelId = query.trim();
|
||||
if (!isCustomModelId(customModelId)) return null;
|
||||
|
||||
return {
|
||||
value: customModelId,
|
||||
groupKey: 'custom',
|
||||
searchText: customModelId,
|
||||
triggerContent: <span className="truncate font-mono text-xs">{customModelId}</span>,
|
||||
itemContent: (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{customModelId}</span>
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1">
|
||||
{t('providerModelSelector.customModel')}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const recommendedOptions = resolvedCatalogModels.flatMap((model) => {
|
||||
const routingHint = routingHints.get(model.id.toLowerCase());
|
||||
@@ -544,6 +574,7 @@ export function FlexibleModelSelector({
|
||||
: t('providerModelSelector.noModelsAvailable')
|
||||
}
|
||||
triggerClassName="h-9"
|
||||
createOption={createCustomModelOption}
|
||||
groups={[
|
||||
...(selectedValueMissing && legacySelectedOption
|
||||
? [
|
||||
@@ -597,6 +628,14 @@ export function FlexibleModelSelector({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'custom',
|
||||
label: (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('providerModelSelector.customModel')}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
options={[
|
||||
...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
|
||||
|
||||
@@ -25,6 +25,7 @@ interface SearchableSelectProps {
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
options: SearchableSelectOption[];
|
||||
createOption?: (query: string) => SearchableSelectOption | null | undefined;
|
||||
groups?: SearchableSelectGroup[];
|
||||
placeholder: string;
|
||||
searchPlaceholder: string;
|
||||
@@ -47,6 +48,7 @@ export function SearchableSelect({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
createOption,
|
||||
groups,
|
||||
placeholder,
|
||||
searchPlaceholder,
|
||||
@@ -64,20 +66,32 @@ export function SearchableSelect({
|
||||
const optionRefs = React.useRef<Record<string, HTMLButtonElement | null>>({});
|
||||
|
||||
const selectedOption = React.useMemo(
|
||||
() => options.find((option) => option.value === value),
|
||||
[options, value]
|
||||
() =>
|
||||
options.find((option) => option.value === value) ??
|
||||
(value ? (createOption?.(value) ?? undefined) : undefined),
|
||||
[createOption, options, value]
|
||||
);
|
||||
|
||||
const filteredOptions = React.useMemo(() => {
|
||||
const normalizedQuery = normalizeSearch(query);
|
||||
if (!normalizedQuery) return options;
|
||||
|
||||
return options.filter((option) =>
|
||||
const matchedOptions = options.filter((option) =>
|
||||
[option.searchText, ...(option.keywords ?? [])].some((candidate) =>
|
||||
normalizeSearch(candidate).includes(normalizedQuery)
|
||||
)
|
||||
);
|
||||
}, [options, query]);
|
||||
const createdOption = createOption?.(query.trim()) ?? null;
|
||||
if (!createdOption) return matchedOptions;
|
||||
|
||||
const normalizedCreatedValue = normalizeSearch(createdOption.value);
|
||||
const optionAlreadyExists = options.some(
|
||||
(option) => normalizeSearch(option.value) === normalizedCreatedValue
|
||||
);
|
||||
if (optionAlreadyExists) return matchedOptions;
|
||||
|
||||
return [...matchedOptions, createdOption];
|
||||
}, [createOption, options, query]);
|
||||
|
||||
const groupedOptions = React.useMemo(() => {
|
||||
const knownGroups = new Map((groups ?? []).map((group) => [group.key, group]));
|
||||
|
||||
Reference in new issue
Block a user