Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mathpix #449

Closed
wants to merge 5 commits into from
Closed
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
5 changes: 4 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,7 @@ PUBLIC_APP_DISCLAIMER=#set to 1 to show a disclaimer on login page
# PUBLIC_APP_ASSETS=huggingchat
# PUBLIC_APP_COLOR=yellow
# PUBLIC_APP_DATA_SHARING=1
# PUBLIC_APP_DISCLAIMER=1
# PUBLIC_APP_DISCLAIMER=1

MATHPIX_APP_ID=#your mathpix app id here
MATHPIX_APP_KEY=#your mathpix app key here
104 changes: 104 additions & 0 deletions src/lib/components/FileDropzone.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<script lang="ts">
import CarbonGeneratePdf from "~icons/carbon/generate-pdf";
import EosIconsLoading from "~icons/eos-icons/loading";
let fileList: File[];
$: fileList = [];

$: file_error = false;
$: file_error_message = "";

export let value = "";
export let onDrag = false;

async function handleFileUpload(file: File) {
// file to blob
let pdf_blob = new Blob([file], { type: "application/pdf" });

const formData = new FormData();
formData.append("file", pdf_blob);

try {
const response = await fetch("/convertPdf", {
method: "POST",
body: formData,
});

if (!response.ok) {
throw new Error("Failed to convert PDF");
}

let { result } = await response.json();
return result;
} catch (error) {
console.error("PDF File conversion error:", error);
return "";
}
}

async function dropHandle(event: DragEvent) {
event.preventDefault();

if (event.dataTransfer && event.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
for (let i = 0; i < event.dataTransfer.items.length; i++) {
// If dropped items aren't files, reject them
if (event.dataTransfer.items[i].kind === "file") {
const file = event.dataTransfer.items[i].getAsFile();
if (file) {
fileList = [...fileList, file];
if (event.dataTransfer.items[i].type !== "application/pdf") {
console.log("Only PDF files are supported");
file_error = true;
file_error_message = "Only PDF files are supported";
break;
}
try {
let mardown_conversion = await handleFileUpload(file);
value += mardown_conversion;
} catch (error) {
file_error = true;
file_error_message = "Failed to convert PDF: " + error;
break;
}
}
}
}
}

if (file_error === true) {
// Sleep for 2 sec
await new Promise((r) => setTimeout(r, 1000));
}
fileList = [];
onDrag = false;
}
</script>

<div
id="dropzone"
on:drop={dropHandle}
class="relative flex w-full max-w-4xl flex-col items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500"
>
<div class="items-center object-center" />
<div class="object-center">
{#if fileList.length === 0}
<div class="mt-3 flex justify-center">
<CarbonGeneratePdf class="text-5xl text-gray-500 dark:text-gray-400" />
</div>
<p class="mb-3 mt-3 text-sm text-gray-500 dark:text-gray-400">
Drag and drop your <span class="font-semibold">PDF</span> file here
</p>
{:else if file_error === true}
<p class="mb-3 mt-3 text-sm text-gray-500 dark:text-gray-400">
Error {file_error_message}
</p>
{:else}
{#each fileList as file}
<p class="mb-3 mt-3 flex items-center text-sm text-gray-500 dark:text-gray-400">
<EosIconsLoading class="mr-2 text-3xl" />
<span class="font-semibold">{file.name}</span>: {file.size} bytes is being converted
</p>
{/each}
{/if}
</div>
</div>
158 changes: 93 additions & 65 deletions src/lib/components/chat/ChatWindow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import WebSearchToggle from "../WebSearchToggle.svelte";
import type { WebSearchMessage } from "$lib/types/WebSearch";
import LoginModal from "../LoginModal.svelte";
import FileDropzone from "../FileDropzone.svelte";

export let messages: Message[] = [];
export let loading = false;
Expand Down Expand Up @@ -44,6 +45,24 @@
dispatch("message", message);
message = "";
};

let lastTarget: EventTarget | null = null;
let onDrag = false;

const onDragEnter = (e: DragEvent) => {
lastTarget = e.target;
onDrag = true;
};

const onDragLeave = (e: DragEvent) => {
if (e.target === lastTarget) {
onDrag = false;
}
};

const onDragOver = (e: DragEvent) => {
e.preventDefault();
};
</script>

<div class="relative min-h-0 min-w-0">
Expand All @@ -69,78 +88,87 @@
/>
<div
class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:dark:bg-gray-900 sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto"
on:dragover={onDragOver}
on:dragenter={onDragEnter}
on:dragleave={onDragLeave}
>
<div class="flex w-full pb-3 max-md:justify-between">
{#if settings?.searchEnabled}
<WebSearchToggle />
{/if}
{#if loading}
<StopGeneratingBtn
classNames={settings?.searchEnabled ? "md:-translate-x-1/2 md:mx-auto" : "mx-auto"}
on:click={() => dispatch("stop")}
/>
{/if}
</div>
<form
on:submit|preventDefault={handleSubmit}
class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500
{isReadOnly ? 'opacity-30' : ''}"
>
<div class="flex w-full flex-1 border-none bg-transparent">
<ChatInput
placeholder="Ask anything"
bind:value={message}
on:submit={handleSubmit}
on:keypress={() => {
if (loginRequired) loginModalOpen = true;
}}
maxRows={4}
disabled={isReadOnly}
/>

{#if onDrag}
<FileDropzone bind:value={message} bind:onDrag />
{:else}
<div class="flex w-full pb-3 max-md:justify-between">
{#if settings?.searchEnabled}
<WebSearchToggle />
{/if}
{#if loading}
<button
class="btn mx-1 my-1 inline-block h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:hidden"
<StopGeneratingBtn
classNames={settings?.searchEnabled ? "md:-translate-x-1/2 md:mx-auto" : "mx-auto"}
on:click={() => dispatch("stop")}
>
<CarbonStopFilledAlt />
</button>
<div
class="mx-1 my-1 hidden h-[2.4rem] items-center p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:flex"
>
<EosIconsLoading />
</div>
{:else}
/>
{/if}
</div>
<form
on:submit|preventDefault={handleSubmit}
class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500
{isReadOnly ? 'opacity-30' : ''}"
>
<div class="flex w-full flex-1 border-none bg-transparent">
<ChatInput
placeholder="Ask anything"
bind:value={message}
on:submit={handleSubmit}
on:keypress={() => {
if (loginRequired) loginModalOpen = true;
}}
maxRows={4}
disabled={isReadOnly}
/>

{#if loading}
<button
class="btn mx-1 my-1 inline-block h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 enabled:hover:text-gray-700 disabled:opacity-60 enabled:dark:hover:text-gray-100 dark:disabled:opacity-40 md:hidden"
on:click={() => dispatch("stop")}
>
<CarbonStopFilledAlt />
</button>
<div
class="mx-1 my-1 hidden h-[2.4rem] items-center p-1 px-[0.7rem] text-gray-400 enabled:hover:text-gray-700 disabled:opacity-60 enabled:dark:hover:text-gray-100 dark:disabled:opacity-40 md:flex"
>
<EosIconsLoading />
</div>
{:else}
<button
class="btn mx-1 my-1 h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 enabled:hover:text-gray-700 disabled:opacity-60 enabled:dark:hover:text-gray-100 dark:disabled:opacity-40"
disabled={!message || isReadOnly}
type="submit"
>
<CarbonSendAltFilled />
</button>
{/if}
</div>
</form>
<div
class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-sm:gap-2"
>
<p>
Model: <a
href={currentModel.modelUrl || "https://huggingface.co/" + currentModel.name}
target="_blank"
rel="noreferrer"
class="hover:underline">{currentModel.displayName}</a
> <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may be inaccurate
or false.
</p>
{#if messages.length}
<button
class="btn mx-1 my-1 h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100"
disabled={!message || isReadOnly}
type="submit"
class="flex flex-none items-center hover:text-gray-400 hover:underline max-sm:rounded-lg max-sm:bg-gray-50 max-sm:px-2.5 dark:max-sm:bg-gray-800"
type="button"
on:click={() => dispatch("share")}
>
<CarbonSendAltFilled />
<CarbonExport class="text-[.6rem] sm:mr-1.5 sm:text-primary-500" />
<div class="max-sm:hidden">Share this conversation</div>
</button>
{/if}
</div>
</form>
<div class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-sm:gap-2">
<p>
Model: <a
href={currentModel.modelUrl || "https://huggingface.co/" + currentModel.name}
target="_blank"
rel="noreferrer"
class="hover:underline">{currentModel.displayName}</a
> <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may be inaccurate
or false.
</p>
{#if messages.length}
<button
class="flex flex-none items-center hover:text-gray-400 hover:underline max-sm:rounded-lg max-sm:bg-gray-50 max-sm:px-2.5 dark:max-sm:bg-gray-800"
type="button"
on:click={() => dispatch("share")}
>
<CarbonExport class="text-[.6rem] sm:mr-1.5 sm:text-primary-500" />
<div class="max-sm:hidden">Share this conversation</div>
</button>
{/if}
</div>
{/if}
</div>
</div>
72 changes: 72 additions & 0 deletions src/lib/server/pdfconvert/requestPdfConversion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { MATHPIX_APP_ID, MATHPIX_APP_KEY } from "$env/static/private";

type MathpixOptions = {
conversion_formats: {
'md': boolean;
'docx': boolean;
'tex.zip': boolean;
'html': boolean;
};
math_inline_delimiters: string[];
rm_spaces: boolean;
};

const defaultOptions: MathpixOptions = {
conversion_formats: {
'md': true,
'docx': false,
'tex.zip': false,
'html': false,
},
math_inline_delimiters: ['$', '$'],
rm_spaces: true,
};

// See https://docs.mathpix.com/#process-a-pdf
async function requestPdfConversion(file: Blob, option: MathpixOptions = defaultOptions): Promise<string> {
const BASE_URL = 'https://api.mathpix.com/v3';
const headers = {
'app_id': MATHPIX_APP_ID,
'app_key': MATHPIX_APP_KEY
};

// Start the conversion
const formData = new FormData();
formData.append('file', file);
formData.append('options_json', JSON.stringify(option));

const initialResponse = await fetch(`${BASE_URL}/pdf`, {
method: 'POST',
headers: headers,
body: formData
});

const { pdf_id } = await initialResponse.json();

// Check conversion status
let conversionCompleted = false;
while (!conversionCompleted) {
const statusResponse = await fetch(`${BASE_URL}/converter/${pdf_id}`, {
method: 'GET',
headers: headers
});

const statusData = await statusResponse.json();
if (statusData.status === "completed") {
conversionCompleted = true;
} else {
// Wait for a 1 second before polling again
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

// Fetch the markdown data
const markdownResponse = await fetch(`${BASE_URL}/converter/${pdf_id}.md`, {
method: 'GET',
headers: headers
});

return await markdownResponse.text();
}

export { requestPdfConversion };
8 changes: 8 additions & 0 deletions src/routes/convertPdf/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { requestPdfConversion } from '$lib/server/pdfconvert/requestPdfConversion';
import { json } from '@sveltejs/kit';

export async function POST({ request }) {
const blob = await request.blob()
const result = await requestPdfConversion(blob);
return json({ result }, { status: 201 });
}