diff --git a/web/src/lib/components/files/DirectoryInput.svelte b/web/src/lib/components/files/DirectoryInput.svelte index 556e32d..087dd12 100644 --- a/web/src/lib/components/files/DirectoryInput.svelte +++ b/web/src/lib/components/files/DirectoryInput.svelte @@ -26,6 +26,6 @@ const mergedProps = mergeProps({ onclick }, restProps); - + {@render children?.({ directory })} diff --git a/web/src/lib/components/files/FileTypeSelect.svelte b/web/src/lib/components/files/FileTypeSelect.svelte new file mode 100644 index 0000000..8589e0a --- /dev/null +++ b/web/src/lib/components/files/FileTypeSelect.svelte @@ -0,0 +1,59 @@ + + + + +
+ + File Type + + + {#if value === "auto"} + Infer Type + {:else} + {value} + {/if} + + + + + {#if allowAuto} + + + Infer Type + + + {/if} + + {#each languageKeys as langKey (langKey)} + + {langKey} + + {/each} + + + + +
diff --git a/web/src/lib/components/files/MultimodalFileInput.svelte b/web/src/lib/components/files/MultimodalFileInput.svelte index fed02b5..219eb5d 100644 --- a/web/src/lib/components/files/MultimodalFileInput.svelte +++ b/web/src/lib/components/files/MultimodalFileInput.svelte @@ -3,13 +3,15 @@ import { box } from "svelte-toolbelt"; import { RadioGroup } from "bits-ui"; import SingleFileInput from "$lib/components/files/SingleFileInput.svelte"; + import FileTypeSelect from "$lib/components/files/FileTypeSelect.svelte"; - let { state = $bindable(), label = "File", required = false }: MultimodalFileInputProps = $props(); + let { state = $bindable(), label = "File", required = false, fileTypeOverride = true }: MultimodalFileInputProps = $props(); const instance = new MultimodalFileInputState({ state, label: box.with(() => label), required: box.with(() => required), + fileTypeOverride: box.with(() => fileTypeOverride), }); state = instance; @@ -74,12 +76,11 @@ {#snippet radioItem(name: string)} - - {#snippet children({ checked })} - - {name} - - {/snippet} + + {name} {/snippet} @@ -91,11 +92,16 @@ ondrop={handleDrop} ondragleavecapture={handleDragLeave} > - - {@render radioItem("File")} - {@render radioItem("URL")} - {@render radioItem("Text")} - +
+ + {@render radioItem("File")} + {@render radioItem("URL")} + {@render radioItem("Text")} + + {#if fileTypeOverride} + instance.getFileType(), (v) => instance.setFileType(v)} /> + {/if} +
{#if instance.mode === "file"} {@render fileInput()} {:else if instance.mode === "url"} diff --git a/web/src/lib/components/files/index.svelte.ts b/web/src/lib/components/files/index.svelte.ts index 0aceded..946e102 100644 --- a/web/src/lib/components/files/index.svelte.ts +++ b/web/src/lib/components/files/index.svelte.ts @@ -1,5 +1,6 @@ import { type ReadableBoxedValues } from "svelte-toolbelt"; -import { lazyPromise } from "$lib/util"; +import { getExtensionForLanguage, lazyPromise } from "$lib/util"; +import type { BundledLanguage, SpecialLanguage } from "shiki"; export interface FileSystemEntry { fileName: string; @@ -132,6 +133,8 @@ function filesToDirectory(files: FileList): DirectoryEntry { return ret; } +export type FileType = SpecialLanguage | BundledLanguage | "auto"; + export type FileInputMode = "file" | "url" | "text"; export type MultimodalFileInputValueMetadata = { @@ -144,6 +147,7 @@ export type MultimodalFileInputProps = { label?: string | undefined; required?: boolean | undefined; + fileTypeOverride?: boolean | undefined; }; export type MultimodalFileInputStateProps = { @@ -151,14 +155,18 @@ export type MultimodalFileInputStateProps = { } & ReadableBoxedValues<{ label: string; required: boolean; + fileTypeOverride: boolean; }>; export class MultimodalFileInputState { private readonly opts: MultimodalFileInputStateProps; mode: FileInputMode = $state("file"); text: string = $state(""); + textType: FileType = $state("plaintext"); file: File | undefined = $state(undefined); + fileType: FileType = $state("auto"); url: string = $state(""); + urlType: FileType = $state("auto"); private urlResolver = $derived.by(() => { const url = this.url; return lazyPromise(async () => { @@ -185,22 +193,56 @@ export class MultimodalFileInputState { if (this.opts.state) { this.mode = this.opts.state.mode; this.text = this.opts.state.text; + this.textType = this.opts.state.textType; this.file = this.opts.state.file; + this.fileType = this.opts.state.fileType; this.url = this.opts.state.url; + this.urlType = this.opts.state.urlType; this.urlResolver = this.opts.state.urlResolver; } } + getFileType(): FileType { + if (this.mode === "file") { + return this.fileType; + } else if (this.mode === "url") { + return this.urlType; + } else if (this.mode === "text") { + return this.textType; + } + throw new Error("Invalid mode"); + } + + setFileType(fileType: FileType) { + if (this.mode === "file") { + this.fileType = fileType; + } else if (this.mode === "url") { + this.urlType = fileType; + } else if (this.mode === "text") { + this.textType = fileType; + } else { + throw new Error("Invalid mode"); + } + } + + private getExtensionOrBlank() { + const fileType = this.getFileType(); + if (fileType === "auto") { + return ""; + } + return getExtensionForLanguage(fileType); + } + get metadata(): MultimodalFileInputValueMetadata | null { const mode = this.mode; const label = this.opts.label.current; if (mode === "file" && this.file !== undefined) { const file = this.file; - return { type: "file", name: file.name }; + return { type: "file", name: `${file.name}${this.getExtensionOrBlank()}` }; } else if (mode === "url" && this.url !== "") { - return { type: "url", name: this.url }; + return { type: "url", name: `${this.url}${this.getExtensionOrBlank()}` }; } else if (mode === "text" && this.text !== "") { - return { type: "text", name: `${label} (Text Input)` }; + return { type: "text", name: `${label}${this.getExtensionOrBlank()}` }; } else { return null; } @@ -228,20 +270,29 @@ export class MultimodalFileInputState { swapState(other: MultimodalFileInputState) { const mode = this.mode; const text = this.text; + const textType = this.textType; const file = this.file; + const fileType = this.fileType; const url = this.url; + const urlType = this.urlType; const urlResolver = this.urlResolver; this.mode = other.mode; this.text = other.text; + this.textType = other.textType; this.file = other.file; + this.fileType = other.fileType; this.url = other.url; + this.urlType = other.urlType; this.urlResolver = other.urlResolver; other.mode = mode; other.text = text; + other.textType = textType; other.file = file; + other.fileType = fileType; other.url = url; + other.urlType = urlType; other.urlResolver = urlResolver; } } diff --git a/web/src/lib/components/settings-popover/ShikiThemeSelector.svelte b/web/src/lib/components/settings-popover/ShikiThemeSelector.svelte index 40e945a..099b34c 100644 --- a/web/src/lib/components/settings-popover/ShikiThemeSelector.svelte +++ b/web/src/lib/components/settings-popover/ShikiThemeSelector.svelte @@ -22,14 +22,14 @@ {capitalizeFirstLetter(mode)} theme
(triggerLabelW = e[0].target.scrollWidth)} aria-label="Current {mode} syntax highlighting theme" - class="scrolling-text grow text-center text-nowrap" + class="scrolling-text grow text-left text-nowrap" style="--scroll-distance: -{scrollDistance}px;" > {value} diff --git a/web/src/lib/diff-viewer-multi-file.svelte.ts b/web/src/lib/diff-viewer-multi-file.svelte.ts index ca980b0..78afdf4 100644 --- a/web/src/lib/diff-viewer-multi-file.svelte.ts +++ b/web/src/lib/diff-viewer-multi-file.svelte.ts @@ -282,9 +282,7 @@ export class MultiFileDiffViewerState { private static readonly context = new Context("MultiFileDiffViewerState"); static init() { - const state = new MultiFileDiffViewerState(); - MultiFileDiffViewerState.context.set(state); - return state; + return MultiFileDiffViewerState.context.set(new MultiFileDiffViewerState()); } static get() { diff --git a/web/src/lib/util.ts b/web/src/lib/util.ts index 59635b1..a20c9ea 100644 --- a/web/src/lib/util.ts +++ b/web/src/lib/util.ts @@ -373,6 +373,12 @@ const languageMap: { [key: string]: BundledLanguage | SpecialLanguage } = { ".yml": "yaml", }; +const reverseLanguageMap = Object.fromEntries(Object.entries(languageMap).map(([ext, lang]) => [lang, ext])); + +export function getExtensionForLanguage(language: BundledLanguage | SpecialLanguage): string { + return reverseLanguageMap[language] || ".txt"; +} + export function guessLanguageFromExtension(fileName: string): BundledLanguage | SpecialLanguage { const lowerFileName = fileName.toLowerCase(); const extensionIndex = lowerFileName.lastIndexOf("."); diff --git a/web/src/routes/LoadDiffDialog.svelte b/web/src/routes/LoadDiffDialog.svelte index 5928a05..04f2334 100644 --- a/web/src/routes/LoadDiffDialog.svelte +++ b/web/src/routes/LoadDiffDialog.svelte @@ -382,7 +382,7 @@
Load a diff @@ -464,7 +464,7 @@ From Patch File - + Go @@ -533,7 +533,7 @@ - + {@render blacklistPopoverContent()}