| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
- import { Fzf } from "fzf"
- import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
- import { useRemark } from "react-remark"
- import styled from "styled-components"
- import { useExtensionState } from "../../context/ExtensionStateContext"
- import { vscode } from "../../utils/vscode"
- import { highlightFzfMatch } from "../../utils/highlight"
- const OpenAiModelPicker: React.FC = () => {
- const { apiConfiguration, setApiConfiguration, openAiModels, onUpdateApiConfig } = useExtensionState()
- const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openAiModelId || "")
- const [isDropdownVisible, setIsDropdownVisible] = useState(false)
- const [selectedIndex, setSelectedIndex] = useState(-1)
- const dropdownRef = useRef<HTMLDivElement>(null)
- const itemRefs = useRef<(HTMLDivElement | null)[]>([])
- const dropdownListRef = useRef<HTMLDivElement>(null)
- const handleModelChange = (newModelId: string) => {
- // could be setting invalid model id/undefined info but validation will catch it
- const apiConfig = {
- ...apiConfiguration,
- openAiModelId: newModelId,
- }
- setApiConfiguration(apiConfig)
- onUpdateApiConfig(apiConfig)
- setSearchTerm(newModelId)
- }
- useEffect(() => {
- if (apiConfiguration?.openAiModelId && apiConfiguration?.openAiModelId !== searchTerm) {
- setSearchTerm(apiConfiguration?.openAiModelId)
- }
- }, [apiConfiguration, searchTerm])
- useEffect(() => {
- if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
- return
- }
- vscode.postMessage({
- type: "refreshOpenAiModels", values: {
- baseUrl: apiConfiguration?.openAiBaseUrl,
- apiKey: apiConfiguration?.openAiApiKey
- }
- })
- }, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey])
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
- setIsDropdownVisible(false)
- }
- }
- document.addEventListener("mousedown", handleClickOutside)
- return () => {
- document.removeEventListener("mousedown", handleClickOutside)
- }
- }, [])
- const modelIds = useMemo(() => {
- return openAiModels.sort((a, b) => a.localeCompare(b))
- }, [openAiModels])
- const searchableItems = useMemo(() => {
- return modelIds.map((id) => ({
- id,
- html: id,
- }))
- }, [modelIds])
- const fzf = useMemo(() => {
- return new Fzf(searchableItems, {
- selector: item => item.html
- })
- }, [searchableItems])
- const modelSearchResults = useMemo(() => {
- if (!searchTerm) return searchableItems
- const searchResults = fzf.find(searchTerm)
- return searchResults.map(result => ({
- ...result.item,
- html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
- }))
- }, [searchableItems, searchTerm, fzf])
- const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
- if (!isDropdownVisible) return
- switch (event.key) {
- case "ArrowDown":
- event.preventDefault()
- setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
- break
- case "ArrowUp":
- event.preventDefault()
- setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
- break
- case "Enter":
- event.preventDefault()
- if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
- handleModelChange(modelSearchResults[selectedIndex].id)
- setIsDropdownVisible(false)
- }
- break
- case "Escape":
- setIsDropdownVisible(false)
- setSelectedIndex(-1)
- break
- }
- }
- useEffect(() => {
- setSelectedIndex(-1)
- if (dropdownListRef.current) {
- dropdownListRef.current.scrollTop = 0
- }
- }, [searchTerm])
- useEffect(() => {
- if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
- itemRefs.current[selectedIndex]?.scrollIntoView({
- block: "nearest",
- behavior: "smooth",
- })
- }
- }, [selectedIndex])
- return (
- <>
- <style>
- {`
- .model-item-highlight {
- background-color: var(--vscode-editor-findMatchHighlightBackground);
- color: inherit;
- }
- `}
- </style>
- <div>
- <DropdownWrapper ref={dropdownRef}>
- <VSCodeTextField
- id="model-search"
- placeholder="Search and select a model..."
- value={searchTerm}
- onInput={(e) => {
- handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase())
- setIsDropdownVisible(true)
- }}
- onFocus={() => setIsDropdownVisible(true)}
- onKeyDown={handleKeyDown}
- style={{ width: "100%", zIndex: OPENAI_MODEL_PICKER_Z_INDEX, position: "relative" }}>
- {searchTerm && (
- <div
- className="input-icon-button codicon codicon-close"
- aria-label="Clear search"
- onClick={() => {
- handleModelChange("")
- setIsDropdownVisible(true)
- }}
- slot="end"
- style={{
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- height: "100%",
- }}
- />
- )}
- </VSCodeTextField>
- {isDropdownVisible && (
- <DropdownList ref={dropdownListRef}>
- {modelSearchResults.map((item, index) => (
- <DropdownItem
- key={item.id}
- ref={(el) => (itemRefs.current[index] = el)}
- isSelected={index === selectedIndex}
- onMouseEnter={() => setSelectedIndex(index)}
- onClick={() => {
- handleModelChange(item.id)
- setIsDropdownVisible(false)
- }}
- dangerouslySetInnerHTML={{
- __html: item.html,
- }}
- />
- ))}
- </DropdownList>
- )}
- </DropdownWrapper>
- </div>
- </>
- )
- }
- export default OpenAiModelPicker
- // Dropdown
- const DropdownWrapper = styled.div`
- position: relative;
- width: 100%;
- `
- export const OPENAI_MODEL_PICKER_Z_INDEX = 1_000
- const DropdownList = styled.div`
- position: absolute;
- top: calc(100% - 3px);
- left: 0;
- width: calc(100% - 2px);
- max-height: 200px;
- overflow-y: auto;
- background-color: var(--vscode-dropdown-background);
- border: 1px solid var(--vscode-list-activeSelectionBackground);
- z-index: ${OPENAI_MODEL_PICKER_Z_INDEX - 1};
- border-bottom-left-radius: 3px;
- border-bottom-right-radius: 3px;
- `
- const DropdownItem = styled.div<{ isSelected: boolean }>`
- padding: 5px 10px;
- cursor: pointer;
- word-break: break-all;
- white-space: normal;
- background-color: ${({ isSelected }) => (isSelected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")};
- &:hover {
- background-color: var(--vscode-list-activeSelectionBackground);
- }
- `
- // Markdown
- const StyledMarkdown = styled.div`
- font-family:
- var(--vscode-font-family),
- system-ui,
- -apple-system,
- BlinkMacSystemFont,
- "Segoe UI",
- Roboto,
- Oxygen,
- Ubuntu,
- Cantarell,
- "Open Sans",
- "Helvetica Neue",
- sans-serif;
- font-size: 12px;
- color: var(--vscode-descriptionForeground);
- p,
- li,
- ol,
- ul {
- line-height: 1.25;
- margin: 0;
- }
- ol,
- ul {
- padding-left: 1.5em;
- margin-left: 0;
- }
- p {
- white-space: pre-wrap;
- }
- a {
- text-decoration: none;
- }
- a {
- &:hover {
- text-decoration: underline;
- }
- }
- `
- export const ModelDescriptionMarkdown = memo(
- ({
- markdown,
- key,
- isExpanded,
- setIsExpanded,
- }: {
- markdown?: string
- key: string
- isExpanded: boolean
- setIsExpanded: (isExpanded: boolean) => void
- }) => {
- const [reactContent, setMarkdown] = useRemark()
- // const [isExpanded, setIsExpanded] = useState(false)
- const [showSeeMore, setShowSeeMore] = useState(false)
- const textContainerRef = useRef<HTMLDivElement>(null)
- const textRef = useRef<HTMLDivElement>(null)
- useEffect(() => {
- setMarkdown(markdown || "")
- }, [markdown, setMarkdown])
- useEffect(() => {
- if (textRef.current && textContainerRef.current) {
- const { scrollHeight } = textRef.current
- const { clientHeight } = textContainerRef.current
- const isOverflowing = scrollHeight > clientHeight
- setShowSeeMore(isOverflowing)
- // if (!isOverflowing) {
- // setIsExpanded(false)
- // }
- }
- }, [reactContent, setIsExpanded])
- return (
- <StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
- <div
- ref={textContainerRef}
- style={{
- overflowY: isExpanded ? "auto" : "hidden",
- position: "relative",
- wordBreak: "break-word",
- overflowWrap: "anywhere",
- }}>
- <div
- ref={textRef}
- style={{
- display: "-webkit-box",
- WebkitLineClamp: isExpanded ? "unset" : 3,
- WebkitBoxOrient: "vertical",
- overflow: "hidden",
- // whiteSpace: "pre-wrap",
- // wordBreak: "break-word",
- // overflowWrap: "anywhere",
- }}>
- {reactContent}
- </div>
- {!isExpanded && showSeeMore && (
- <div
- style={{
- position: "absolute",
- right: 0,
- bottom: 0,
- display: "flex",
- alignItems: "center",
- }}>
- <div
- style={{
- width: 30,
- height: "1.2em",
- background:
- "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
- }}
- />
- <VSCodeLink
- style={{
- // cursor: "pointer",
- // color: "var(--vscode-textLink-foreground)",
- fontSize: "inherit",
- paddingRight: 0,
- paddingLeft: 3,
- backgroundColor: "var(--vscode-sideBar-background)",
- }}
- onClick={() => setIsExpanded(true)}>
- See more
- </VSCodeLink>
- </div>
- )}
- </div>
- </StyledMarkdown>
- )
- },
- )
|