Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/src/lib/components/files/DirectoryInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
const mergedProps = mergeProps({ onclick }, restProps);
</script>

<Button.Root {...mergedProps}>
<Button.Root type="button" {...mergedProps}>
{@render children?.({ directory })}
</Button.Root>
59 changes: 59 additions & 0 deletions web/src/lib/components/files/FileTypeSelect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script lang="ts" module>
import { type BundledLanguage, bundledLanguages, type SpecialLanguage } from "shiki";

const languageKeys = [...Object.keys(bundledLanguages), "text", "ansi", "plaintext", "txt"] as BundledLanguage | SpecialLanguage[];
</script>

<script lang="ts">
import { Label, Select } from "bits-ui";

interface Props {
value?: BundledLanguage | SpecialLanguage | "auto";
allowAuto?: boolean;
}

let { value = $bindable("auto"), allowAuto = true }: Props = $props();

const uid = $props.id();
const fileTypeId = `file-type-${uid}`;
const fileTypeLabelId = `file-type-label-${uid}`;
</script>

<div class="flex items-center gap-1">
<!-- TODO Label gets active & hover styles, but doesn't open selector -->
<Label.Root id={fileTypeLabelId} for={fileTypeId} class="text-sm">File Type</Label.Root>
<Select.Root type="single" bind:value scrollAlignment="center">
<Select.Trigger id={fileTypeId} aria-labelledby={fileTypeLabelId} class="flex items-center gap-1 rounded-sm border btn-ghost px-2 text-sm">
{#if value === "auto"}
Infer Type
{:else}
{value}
{/if}
<span aria-hidden="true" class="iconify size-4 shrink-0 text-base text-em-disabled octicon--triangle-down-16"></span>
</Select.Trigger>
<Select.Portal>
<Select.Content class="z-100 mt-0.5 flex max-h-64 flex-col rounded-sm border bg-neutral p-1.5 shadow-md">
{#if allowAuto}
<Select.Group class="mb-1">
<Select.Item
value="auto"
class="cursor-default rounded-sm px-2 py-1 text-sm data-highlighted:bg-neutral-3 data-selected:bg-primary data-selected:text-white"
>
Infer Type
</Select.Item>
</Select.Group>
{/if}
<Select.Group class="flex grow flex-col gap-1 overflow-y-auto">
{#each languageKeys as langKey (langKey)}
<Select.Item
value={langKey}
class="cursor-default rounded-sm px-2 py-1 text-sm data-highlighted:bg-neutral-3 data-selected:bg-primary data-selected:text-white"
>
{langKey}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
30 changes: 18 additions & 12 deletions web/src/lib/components/files/MultimodalFileInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -74,12 +76,11 @@
</script>

{#snippet radioItem(name: string)}
<RadioGroup.Item value={name.toLowerCase()}>
{#snippet children({ checked })}
<span class="rounded-sm px-1 py-0.5 text-sm" class:btn-ghost={!checked} class:border={!checked} class:btn-primary={checked}>
{name}
</span>
{/snippet}
<RadioGroup.Item
value={name.toLowerCase()}
class="rounded-sm px-2 text-sm data-[state=checked]:btn-primary data-[state=unchecked]:border data-[state=unchecked]:btn-ghost"
>
{name}
</RadioGroup.Item>
{/snippet}

Expand All @@ -91,11 +92,16 @@
ondrop={handleDrop}
ondragleavecapture={handleDragLeave}
>
<RadioGroup.Root class="mb-1 flex w-full gap-1" bind:value={instance.mode}>
{@render radioItem("File")}
{@render radioItem("URL")}
{@render radioItem("Text")}
</RadioGroup.Root>
<div class="mb-1 flex w-full flex-wrap items-center gap-1">
<RadioGroup.Root class="me-2 flex gap-1" bind:value={instance.mode}>
{@render radioItem("File")}
{@render radioItem("URL")}
{@render radioItem("Text")}
</RadioGroup.Root>
{#if fileTypeOverride}
<FileTypeSelect allowAuto={instance.mode !== "text"} bind:value={() => instance.getFileType(), (v) => instance.setFileType(v)} />
{/if}
</div>
{#if instance.mode === "file"}
{@render fileInput()}
{:else if instance.mode === "url"}
Expand Down
59 changes: 55 additions & 4 deletions web/src/lib/components/files/index.svelte.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 = {
Expand All @@ -144,21 +147,26 @@ export type MultimodalFileInputProps = {

label?: string | undefined;
required?: boolean | undefined;
fileTypeOverride?: boolean | undefined;
};

export type MultimodalFileInputStateProps = {
state: MultimodalFileInputState | undefined;
} & 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 () => {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
<Select.Trigger aria-labelledby={labelId} id={triggerId} class="group flex cursor-pointer items-center justify-between gap-1 px-2 py-1">
<Label.Root id={labelId} for={triggerId} class="cursor-pointer text-sm">{capitalizeFirstLetter(mode)} theme</Label.Root>
<div
class="flex w-44 items-center gap-1 rounded-sm border btn-ghost bg-neutral px-1 py-0.5 text-sm select-none group-hover:btn-ghost-hover group-active:btn-ghost-active"
class="flex w-44 items-center gap-1 rounded-sm border btn-ghost bg-neutral px-2 py-0.5 text-sm select-none group-hover:btn-ghost-hover group-active:btn-ghost-active"
bind:this={anchor}
>
<div bind:clientWidth={triggerLabelContainerW} class="flex grow overflow-hidden" class:reveal-right={scrollDistance !== 0}>
<div
use:resizeObserver={(e) => (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}
Expand Down
4 changes: 1 addition & 3 deletions web/src/lib/diff-viewer-multi-file.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,7 @@ export class MultiFileDiffViewerState {
private static readonly context = new Context<MultiFileDiffViewerState>("MultiFileDiffViewerState");

static init() {
const state = new MultiFileDiffViewerState();
MultiFileDiffViewerState.context.set(state);
return state;
return MultiFileDiffViewerState.context.set(new MultiFileDiffViewerState());
}

static get() {
Expand Down
6 changes: 6 additions & 0 deletions web/src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(".");
Expand Down
6 changes: 3 additions & 3 deletions web/src/routes/LoadDiffDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 z-50 bg-black/50 dark:bg-white/20" />
<Dialog.Content
class="fixed top-1/2 left-1/2 z-50 max-h-svh w-192 max-w-[95%] -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-md bg-neutral shadow-md"
class="fixed top-1/2 left-1/2 z-50 max-h-svh w-192 max-w-full -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-md bg-neutral shadow-md sm:max-w-[95%]"
>
<header class="sticky top-0 z-10 flex flex-row items-center justify-between rounded-t-md bg-neutral-2 p-4">
<Dialog.Title class="text-xl font-semibold">Load a diff</Dialog.Title>
Expand Down Expand Up @@ -464,7 +464,7 @@
<span class="iconify size-6 shrink-0 octicon--file-diff-24"></span>
From Patch File
</h3>
<MultimodalFileInput bind:state={patchFile} required label="Patch File" />
<MultimodalFileInput bind:state={patchFile} required fileTypeOverride={false} label="Patch File" />
<Button.Root type="submit" class="mt-1 rounded-md btn-primary px-2 py-1">Go</Button.Root>
</form>

Expand Down Expand Up @@ -533,7 +533,7 @@
<span class="iconify size-4 shrink-0 octicon--filter-16" aria-hidden="true"></span>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content side="top" class="z-50 overflow-hidden rounded-md border bg-neutral">
<Popover.Content side="top" class="z-50 mx-2 overflow-hidden rounded-md border bg-neutral">
{@render blacklistPopoverContent()}
<Popover.Arrow class="text-edge" />
</Popover.Content>
Expand Down