diff options
| -rw-r--r-- | packages/app/src/components/select-dialog.tsx | 70 | ||||
| -rw-r--r-- | packages/app/src/context/local.tsx | 6 | ||||
| -rw-r--r-- | packages/app/src/pages/index.tsx | 30 | ||||
| -rw-r--r-- | packages/app/src/utils/path.ts | 5 |
4 files changed, 75 insertions, 36 deletions
diff --git a/packages/app/src/components/select-dialog.tsx b/packages/app/src/components/select-dialog.tsx index 2ff64f5da..ade7489d1 100644 --- a/packages/app/src/components/select-dialog.tsx +++ b/packages/app/src/components/select-dialog.tsx @@ -1,22 +1,18 @@ -import { createEffect, Show, For, createMemo, type JSX } from "solid-js" +import { createEffect, Show, For, createMemo, type JSX, createResource } from "solid-js" import { Dialog } from "@kobalte/core/dialog" import { Icon, IconButton } from "@/ui" import { createStore } from "solid-js/store" -import { entries, flatMap, groupBy, map, mapValues, pipe } from "remeda" +import { entries, flatMap, groupBy, map, pipe } from "remeda" import { createList } from "solid-list" import fuzzysort from "fuzzysort" interface SelectDialogProps<T> { - items: T[] + items: T[] | ((filter: string) => Promise<T[]>) key: (item: T) => string render: (item: T) => JSX.Element + filter?: string[] current?: T placeholder?: string - filter?: - | false - | { - keys: string[] - } groupBy?: (x: T) => string onSelect?: (value: T | undefined) => void onClose?: () => void @@ -29,24 +25,31 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) { mouseActive: false, }) - const grouped = createMemo(() => { - const needle = store.filter.toLowerCase() - const result = pipe( - props.items, - (x) => - !needle || !props.filter - ? x - : fuzzysort.go(needle, x, { keys: props.filter && props.filter.keys }).map((x) => x.obj), - groupBy((x) => (props.groupBy ? props.groupBy(x) : "")), - mapValues((x) => x.sort((a, b) => props.key(a).localeCompare(props.key(b)))), - entries(), - map(([k, v]) => ({ category: k, items: v })), - ) - return result - }) + const [grouped] = createResource( + () => store.filter, + async (filter) => { + const needle = filter.toLowerCase() + const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || [] + const result = pipe( + all, + (x) => { + if (!needle) return x + if (!props.filter && Array.isArray(x) && x.every((e) => typeof e === "string")) { + return fuzzysort.go(needle, x).map((x) => x.target) as T[] + } + return fuzzysort.go(needle, x, { keys: props.filter! }).map((x) => x.obj) + }, + groupBy((x) => (props.groupBy ? props.groupBy(x) : "")), + // mapValues((x) => x.sort((a, b) => props.key(a).localeCompare(props.key(b)))), + entries(), + map(([k, v]) => ({ category: k, items: v })), + ) + return result + }, + ) const flat = createMemo(() => { return pipe( - grouped(), + grouped() || [], flatMap((x) => x.items), ) }) @@ -55,7 +58,11 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) { initialActive: props.current ? props.key(props.current) : undefined, loop: true, }) - const resetSelection = () => list.setActive(props.key(flat()[0])) + const resetSelection = () => { + const all = flat() + if (all.length === 0) return + list.setActive(props.key(all[0])) + } createEffect(() => { store.filter @@ -64,8 +71,9 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) { }) createEffect(() => { - if (store.mouseActive) return - if (list.active() === props.key(flat()[0])) { + const all = flat() + if (store.mouseActive || all.length === 0) return + if (list.active() === props.key(all[0])) { scrollRef?.scrollTo(0, 0) return } @@ -156,9 +164,11 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) { <For each={grouped()}> {(group) => ( <> - <div class="top-0 sticky z-10 bg-background-panel p-2 text-xs text-text-muted/60 tracking-wider uppercase"> - {group.category} - </div> + <Show when={group.category}> + <div class="top-0 sticky z-10 bg-background-panel p-2 text-xs text-text-muted/60 tracking-wider uppercase"> + {group.category} + </div> + </Show> <div class="p-2"> <For each={group.items}> {(item) => ( diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index c52fe0db4..c74ae21ab 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -163,7 +163,7 @@ function init() { }) } - const open = async (path: string) => { + const open = async (path: string, options?: { pin?: boolean }) => { const relative = path.replace(sync.data.path.directory + "/", "") if (!store.node[relative]) { const parent = relative.split("/").slice(0, -1).join("/") @@ -181,6 +181,7 @@ function init() { ] }) setStore("active", relative) + if (options?.pin) setStore("node", path, "pinned", true) if (store.node[relative].loaded) return return load(relative) } @@ -199,6 +200,8 @@ function init() { }) } + const search = (query: string) => sdk.find.files({ query: { query } }).then((x) => x.data!) + const bus = useEvent() bus.listen((event) => { switch (event.type) { @@ -303,6 +306,7 @@ function init() { !x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"), ) }, + search, } })() diff --git a/packages/app/src/pages/index.tsx b/packages/app/src/pages/index.tsx index 9133f40d8..acb756621 100644 --- a/packages/app/src/pages/index.tsx +++ b/packages/app/src/pages/index.tsx @@ -20,6 +20,7 @@ import type { LocalFile } from "@/context/local" import SessionList from "@/components/session-list" import SessionTimeline from "@/components/session-timeline" import { createStore } from "solid-js/store" +import { getDirectory, getFilename } from "@/utils" export default function Page() { const sdk = useSDK() @@ -30,6 +31,7 @@ export default function Page() { prompt: "", dragging: undefined as "left" | "right" | undefined, modelSelectOpen: false, + fileSelectOpen: false, }) let inputRef: HTMLInputElement | undefined = undefined @@ -47,12 +49,12 @@ export default function Page() { const handleKeyDown = (e: KeyboardEvent) => { if (e.getModifierState(MOD) && e.shiftKey && e.key.toLowerCase() === "p") { e.preventDefault() - setStore("modelSelectOpen", true) + // TODO: command palette return } if (e.getModifierState(MOD) && e.key.toLowerCase() === "p") { e.preventDefault() - setStore("modelSelectOpen", true) + setStore("fileSelectOpen", true) return } @@ -554,14 +556,32 @@ export default function Page() { </div> </div> )} - filter={{ - keys: ["provider.name", "name", "id"], - }} + filter={["provider.name", "name", "id"]} groupBy={(x) => x.provider.name} onClose={() => setStore("modelSelectOpen", false)} onSelect={(x) => local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined)} /> </Show> + <Show when={store.fileSelectOpen}> + <SelectDialog + items={local.file.search} + key={(x) => x} + render={(i) => ( + <div class="w-full flex items-center justify-between"> + <div class="flex items-center gap-x-2 text-text-muted grow min-w-0"> + <FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" /> + <span class="text-xs text-text whitespace-nowrap">{getFilename(i)}</span> + <span class="text-xs text-text-muted/80 whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0"> + {getDirectory(i)} + </span> + </div> + <div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div> + </div> + )} + onClose={() => setStore("fileSelectOpen", false)} + onSelect={(x) => (x ? local.file.open(x, { pin: true }) : undefined)} + /> + </Show> </div> ) } diff --git a/packages/app/src/utils/path.ts b/packages/app/src/utils/path.ts index d6a066e80..9c026ca41 100644 --- a/packages/app/src/utils/path.ts +++ b/packages/app/src/utils/path.ts @@ -3,6 +3,11 @@ export function getFilename(path: string) { return parts[parts.length - 1] } +export function getDirectory(path: string) { + const parts = path.split("/") + return parts.slice(0, parts.length - 1).join("/") +} + export function getFileExtension(path: string) { const parts = path.split(".") return parts[parts.length - 1] |
