Skip to content

Commit

Permalink
feat: better UI for model selection (credit @cfahlgren1)
Browse files Browse the repository at this point in the history
  • Loading branch information
Neet-Nestor committed Nov 9, 2024
1 parent 1884408 commit 48be91a
Show file tree
Hide file tree
Showing 24 changed files with 950 additions and 193 deletions.
3 changes: 2 additions & 1 deletion app/client/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChatCompletionFinishReason, CompletionUsage } from "@mlc-ai/web-llm";
import { CacheType, Model } from "../store";
import { ModelFamily } from "../constant";
export const ROLES = ["system", "user", "assistant"] as const;
export type MessageRole = (typeof ROLES)[number];

Expand Down Expand Up @@ -58,7 +59,7 @@ export interface ModelRecord {
provider?: string;
size?: string;
quantization?: string;
family?: string;
family: ModelFamily;
recommended_config?: {
temperature?: number;
context_window_size?: number;
Expand Down
31 changes: 10 additions & 21 deletions app/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,9 @@ import {
ListItem,
Modal,
Popover,
Selector,
showConfirm,
showPrompt,
showToast,
Tooltip,
} from "./ui-lib";
import { useNavigate } from "react-router-dom";
import {
Expand All @@ -83,16 +81,16 @@ import {
UNFINISHED_INPUT,
} from "../constant";
import { Avatar, AvatarPicker } from "./emoji";
import { ContextPrompts, TemplateAvatar, TemplateConfig } from "./template";
import { ContextPrompts, TemplateAvatar } from "./template";
import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter";
import { MultimodalContent } from "../client/api";
import { Template, useTemplateStore } from "../store/template";
import Image from "next/image";
import { MLCLLMContext, WebLLMContext } from "../context";
import EyeIcon from "../icons/eye.svg";
import { ChatImage } from "../typing";
import ModelSelect from "./model-select";

export function ScrollDownToast(prop: { show: boolean; onclick: () => void }) {
return (
Expand Down Expand Up @@ -544,23 +542,14 @@ export function ChatActions(props: {
fullWidth
/>
{showModelSelector && (
<Selector
defaultSelectedValue={currentModel}
items={models.map((m) => ({
title: m.name,
value: m.name,
family: m.family,
icon: isVisionModel(m.name) ? (
<Tooltip content={<div>Vision Model</div>} direction="bottom">
<EyeIcon />
</Tooltip>
) : undefined,
}))}
onClose={() => setShowModelSelector(false)}
onSelection={(s) => {
if (s.length === 0) return;
config.selectModel(s[0] as Model);
showToast(s[0]);
<ModelSelect
onClose={() => {
setShowModelSelector(false);
}}
availableModels={models.map((m) => m.name)}
onSelectModel={(modelName) => {
config.selectModel(modelName as Model);
showToast(modelName);
}}
/>
)}
Expand Down
26 changes: 22 additions & 4 deletions app/components/model-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ModelConfig,
useAppConfig,
ModelClient,
Model,
} from "../store";

import CancelIcon from "../icons/cancel.svg";
Expand All @@ -11,13 +12,15 @@ import ConnectIcon from "../icons/connection.svg";

import Locale from "../locales";
import { InputRange } from "./input-range";
import { List, ListItem, Modal, Select } from "./ui-lib";
import { List, ListItem, Modal, Select, showToast } from "./ui-lib";
import React, { useState } from "react";
import { IconButton } from "./button";
import ModelSelect from "./model-select";

export function ModelConfigList() {
const config = useAppConfig();
const models = config.models;
const [showModelSelector, setShowModelSelector] = useState(false);
const [showApiConnectModel, setShowApiConnectModel] = useState(false);

const [endpointInput, setEndpointInput] = useState<string>(
Expand Down Expand Up @@ -59,8 +62,13 @@ export function ModelConfigList() {
<ListItem title={Locale.Settings.Model}>
<Select
value={config.modelConfig.model}
onChange={(e) => {
config.selectModel(e.target.value);
onClick={(e) => {
e.preventDefault();
setShowModelSelector(true);
}}
onMouseDown={(e) => {
// Prevent the dropdown list from opening
e.preventDefault();
}}
>
{models.map((v, i) => (
Expand Down Expand Up @@ -237,7 +245,17 @@ export function ModelConfigList() {
</ListItem>
</>
)}

{showModelSelector && (
<ModelSelect
onClose={() => {
setShowModelSelector(false);
}}
availableModels={models.map((m) => m.name)}
onSelectModel={(modelName) => {
config.selectModel(modelName as Model);
}}
/>
)}
{showApiConnectModel && (
<div className="screen-model-container">
<Modal
Expand Down
59 changes: 59 additions & 0 deletions app/components/model-row.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
.row {
display: flex;
flex-direction: column;
border: var(--border-in-light);
border-radius: 10px;
overflow: hidden;
font-size: .8rem;

.summary-continer {
padding: 0.75rem;
transition: background-color 0.2s ease-in-out;
cursor: pointer;
height: 100%;

&:hover {
background-color: var(--second);
}

.summary {
display: flex;
justify-content: space-between;
align-items: center;

.summary-model-info {
display: flex;
align-items: center;
gap: 10px;
}
}
}

.expanded {
padding: .75rem;

.variant-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem;
border-radius: 0.375rem;
cursor: pointer;
transition: background-color 0.2s ease-in-out;

&:hover {
background-color: var(--gray);
}

.variant-label {
font-size: 0.75rem;
color: var(--black);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
margin-right: 0.5rem;
}
}
}
}
83 changes: 83 additions & 0 deletions app/components/model-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from "react";
import { ChevronDown, ChevronUp } from "lucide-react";

import style from "./model-row.module.scss";

interface ModelRowProps {
baseModel: string;
variants: string[];
isExpanded: boolean;
hasSingleVariant: boolean;
determineModelIcon: (model: string) => JSX.Element;
extractModelDetails: (model: string) => {
displayName: string;
quantBadge: string | null;
};
onSelectModel: (model: string) => void;
onClose: () => void;
handleToggleExpand: (modelName: string) => void;
}

const ModelRow: React.FC<ModelRowProps> = ({
baseModel,
variants,
isExpanded,
hasSingleVariant,
determineModelIcon,
extractModelDetails,
onSelectModel,
onClose,
handleToggleExpand,
}) => {
const { quantBadge } = hasSingleVariant
? extractModelDetails(variants[0])
: { quantBadge: null };

return (
<div className={style["row"]} key={baseModel}>
<div
className={style["summary-continer"]}
onClick={() => {
if (hasSingleVariant) {
onSelectModel(variants[0]);
onClose();
} else {
handleToggleExpand(baseModel);
}
}}
>
<div className={style["summary"]}>
<div className={style["summary-model-info"]}>
{determineModelIcon(variants[0])}
<span>{baseModel}</span>
{hasSingleVariant && quantBadge && <span>{quantBadge}</span>}
</div>
{!hasSingleVariant && (isExpanded ? <ChevronUp /> : <ChevronDown />)}
</div>
</div>
{isExpanded && !hasSingleVariant && (
<div className={style["expanded"]}>
{variants.map((variant) => {
const { quantBadge } = extractModelDetails(variant);
return (
<div
key={variant}
onClick={() => {
onSelectModel(variant);
onClose();
}}
className={style["variant-option"]}
>
<span className={style["variant-label"]}>
{quantBadge || variant}
</span>
</div>
);
})}
</div>
)}
</div>
);
};

export default ModelRow;
71 changes: 71 additions & 0 deletions app/components/model-select.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
.header {
display: flex;
flex-direction: column;
width: 100%;
position: relative;
gap: 20px;
padding-bottom: 20px;
}

.input-container {
position: relative;
width: 100%;
}

input.input[type=text] {
width: 100%;
max-width: 100%;
padding: 0.5rem 0.5rem 0.5rem 2rem;
}

.input-icon {
position: absolute;
left: 0.5rem;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
width: 1rem;
height: 1rem;
}

.model-list {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-auto-rows: auto;
align-items: start;
}

.model-family-filter {
display: flex;
flex-wrap: wrap;
gap: .4rem;

.model-family-button {
.icon {
position: relative;
height: 1rem;
width: 1rem;

svg,
img {
max-width: 100%;
max-height: 100%;
filter: none !important;
}
}
}
}

.model-icon {
height: 1rem;
width: 1rem;
position: relative;

svg,
img {
max-width: 100%;
max-height: 100%;
filter: none !important;
}
}
Loading

0 comments on commit 48be91a

Please sign in to comment.