Correct react implementation of rust backend
This commit is contained in:
parent
9035e00da3
commit
4a2a9a7489
6 changed files with 352 additions and 38 deletions
|
|
@ -1,39 +1,235 @@
|
||||||
import React, { useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import ChatHeader from "src/components/ui/chat/chat-header";
|
import ChatHeader from "src/components/ui/chat/chat-header";
|
||||||
import ChatWindow from "src/components/ui/chat/chat-window";
|
import ChatWindow from "src/components/ui/chat/chat-window";
|
||||||
import MessageInput from "src/components/ui/chat/message-input";
|
import MessageInput from "src/components/ui/chat/message-input";
|
||||||
|
import {
|
||||||
|
createQuery,
|
||||||
|
getQueryResult,
|
||||||
|
getQueryStatus,
|
||||||
|
listFiles,
|
||||||
|
} from "src/lib/api";
|
||||||
|
|
||||||
|
const createId = () =>
|
||||||
|
(globalThis.crypto?.randomUUID?.() ?? `id-${Date.now()}-${Math.random()}`);
|
||||||
|
|
||||||
|
const INTRO_MESSAGE = {
|
||||||
|
id: "intro",
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
"Ask me about the demo PDFs and I'll respond with the best matches pulled from the processed files.",
|
||||||
|
};
|
||||||
|
|
||||||
export default function ChatLayout() {
|
export default function ChatLayout() {
|
||||||
const [messages, setMessages] = useState([
|
const [messages, setMessages] = useState([INTRO_MESSAGE]);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [files, setFiles] = useState([]);
|
||||||
|
const [errorToast, setErrorToast] = useState("");
|
||||||
|
const pollAbortRef = useRef(null);
|
||||||
|
|
||||||
|
const showError = useCallback((message) => {
|
||||||
|
setErrorToast(message);
|
||||||
|
window.setTimeout(() => setErrorToast(""), 5000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const refreshFiles = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const latest = await listFiles();
|
||||||
|
setFiles(latest);
|
||||||
|
} catch (error) {
|
||||||
|
showError(error.message ?? "Failed to load files");
|
||||||
|
}
|
||||||
|
}, [showError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshFiles();
|
||||||
|
}, [refreshFiles]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (pollAbortRef.current) {
|
||||||
|
pollAbortRef.current.aborted = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const buildAssistantMarkdown = useCallback((result) => {
|
||||||
|
if (!result || typeof result !== "object") {
|
||||||
|
return "I could not find a response for that request.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalAnswer = result.final_answer?.trim();
|
||||||
|
const relationships = result.relationships?.trim();
|
||||||
|
const relatedFiles = Array.isArray(result.related_files)
|
||||||
|
? result.related_files
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const fileLines = relatedFiles
|
||||||
|
.filter((f) => f && typeof f === "object")
|
||||||
|
.map((file) => {
|
||||||
|
const filename = file.filename || file.id || "download";
|
||||||
|
const linkTarget = `/storage/${encodeURIComponent(filename)}`;
|
||||||
|
const description = file.description?.trim();
|
||||||
|
const score =
|
||||||
|
typeof file.score === "number"
|
||||||
|
? ` _(score: ${file.score.toFixed(3)})_`
|
||||||
|
: "";
|
||||||
|
const detail = description ? ` — ${description}` : "";
|
||||||
|
return `- [${filename}](${linkTarget})${detail}${score}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
let content =
|
||||||
|
finalAnswer ||
|
||||||
|
"I could not determine an answer from the indexed documents yet.";
|
||||||
|
|
||||||
|
if (fileLines.length) {
|
||||||
|
content += `\n\n**Related Files**\n${fileLines.join("\n")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relationships && relationships !== finalAnswer) {
|
||||||
|
content += `\n\n---\n${relationships}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileLines.length && (!finalAnswer || finalAnswer.length < 10)) {
|
||||||
|
content +=
|
||||||
|
"\n\n_No analyzed documents matched yet. Try seeding demo data or wait for processing to finish._";
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const waitForResult = useCallback(async (id) => {
|
||||||
|
const abortState = { aborted: false };
|
||||||
|
pollAbortRef.current = abortState;
|
||||||
|
const timeoutMs = 120_000;
|
||||||
|
const intervalMs = 1_500;
|
||||||
|
const started = Date.now();
|
||||||
|
|
||||||
|
while (!abortState.aborted) {
|
||||||
|
if (Date.now() - started > timeoutMs) {
|
||||||
|
throw new Error("Timed out waiting for the query to finish");
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusPayload = await getQueryStatus(id);
|
||||||
|
const status = statusPayload?.status;
|
||||||
|
|
||||||
|
if (status === "Completed") {
|
||||||
|
const resultPayload = await getQueryResult(id);
|
||||||
|
return resultPayload?.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "Failed") {
|
||||||
|
const resultPayload = await getQueryResult(id);
|
||||||
|
const reason = resultPayload?.result?.error || "Query failed";
|
||||||
|
throw new Error(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "Cancelled") {
|
||||||
|
throw new Error("Query was cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "not_found") {
|
||||||
|
throw new Error("Query was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, intervalMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Query polling was aborted");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSend = useCallback(
|
||||||
|
async (text) => {
|
||||||
|
if (isProcessing) {
|
||||||
|
showError("Please wait for the current response to finish.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userEntry = {
|
||||||
|
id: createId(),
|
||||||
|
role: "user",
|
||||||
|
content: text,
|
||||||
|
};
|
||||||
|
setMessages((prev) => [...prev, userEntry]);
|
||||||
|
|
||||||
|
const placeholderId = createId();
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
{
|
{
|
||||||
|
id: placeholderId,
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "Hello — I can help you with code, explanations, and more.",
|
content: "_Analyzing indexed documents..._",
|
||||||
|
pending: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function handleSend(text) {
|
setIsProcessing(true);
|
||||||
const userMsg = { role: "user", content: text };
|
|
||||||
setMessages((s) => [...s, userMsg]);
|
|
||||||
|
|
||||||
// fake assistant reply after short delay
|
try {
|
||||||
setTimeout(() => {
|
const payload = { q: text, top_k: 5 };
|
||||||
setMessages((s) => [
|
const created = await createQuery(payload);
|
||||||
...s,
|
const result = await waitForResult(created.id);
|
||||||
{ role: "assistant", content: `You said: ${text}` },
|
const content = buildAssistantMarkdown(result);
|
||||||
]);
|
setMessages((prev) =>
|
||||||
}, 600);
|
prev.map((message) =>
|
||||||
|
message.id === placeholderId
|
||||||
|
? { ...message, content, pending: false }
|
||||||
|
: message
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error?.message || "Something went wrong.";
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((entry) =>
|
||||||
|
entry.id === placeholderId
|
||||||
|
? {
|
||||||
|
...entry,
|
||||||
|
content: `⚠️ ${message}`,
|
||||||
|
pending: false,
|
||||||
|
error: true,
|
||||||
}
|
}
|
||||||
|
: entry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
showError(message);
|
||||||
|
} finally {
|
||||||
|
pollAbortRef.current = null;
|
||||||
|
setIsProcessing(false);
|
||||||
|
refreshFiles();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
isProcessing,
|
||||||
|
showError,
|
||||||
|
refreshFiles,
|
||||||
|
waitForResult,
|
||||||
|
buildAssistantMarkdown,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
function handleDeleteAll() {
|
const handleDeleteAll = useCallback(() => {
|
||||||
if (!window.confirm("Delete all messages?")) return;
|
if (!window.confirm("Delete all messages?")) {
|
||||||
setMessages([]);
|
return;
|
||||||
}
|
}
|
||||||
|
setMessages([INTRO_MESSAGE]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const latestFileSummary = useMemo(() => {
|
||||||
|
if (!files.length) return "No files indexed yet.";
|
||||||
|
const pending = files.filter((f) => f.pending_analysis).length;
|
||||||
|
const ready = files.length - pending;
|
||||||
|
return `${ready} ready • ${pending} processing`;
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-start w-full max-w-3xl gap-4 p-4">
|
<div className="flex flex-col flex-start w-full max-w-3xl gap-4 p-4">
|
||||||
<ChatHeader />
|
<ChatHeader
|
||||||
|
onClear={handleDeleteAll}
|
||||||
|
busy={isProcessing}
|
||||||
|
fileSummary={latestFileSummary}
|
||||||
|
errorMessage={errorToast}
|
||||||
|
/>
|
||||||
<ChatWindow messages={messages} />
|
<ChatWindow messages={messages} />
|
||||||
<MessageInput onSend={handleSend} onDeleteAll={handleDeleteAll} />
|
<MessageInput onSend={handleSend} disabled={isProcessing} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
import { Flame } from "lucide-react";
|
import { Flame } from "lucide-react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
|
|
||||||
export default function FlameButton({ onClick }) {
|
export default function FlameButton({ onClick, disabled = false }) {
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="bg-gray-700 cursor-pointer p-2 rounded-2xl border-2 border-gray-600"
|
className={`bg-gray-700 p-2 rounded-2xl border-2 border-gray-600 ${
|
||||||
whileHover={{ scale: 1.1 }}
|
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||||
whileTap={{ scale: 0.9 }}
|
}`}
|
||||||
|
whileHover={disabled ? undefined : { scale: 1.1 }}
|
||||||
|
whileTap={disabled ? undefined : { scale: 0.9 }}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ opacity: disabled ? 0.5 : 1 }}
|
||||||
>
|
>
|
||||||
<Flame />
|
<Flame />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,30 @@
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { Rocket } from "lucide-react";
|
import { Rocket } from "lucide-react";
|
||||||
import DeleteButton from "src/components/ui/button/delete-button";
|
import DeleteButton from "src/components/ui/button/delete-button";
|
||||||
import SchematicButton from "../button/schematic-button";
|
import SchematicButton from "../button/schematic-button";
|
||||||
|
|
||||||
export default function ChatHeader({ title = "Title of Chat" }) {
|
export default function ChatHeader({
|
||||||
|
title = "Title of Chat",
|
||||||
|
onClear,
|
||||||
|
busy = false,
|
||||||
|
fileSummary,
|
||||||
|
errorMessage,
|
||||||
|
}) {
|
||||||
const isDebug = useMemo(() => {
|
const isDebug = useMemo(() => {
|
||||||
const p = new URLSearchParams(window.location.search);
|
const p = new URLSearchParams(window.location.search);
|
||||||
return p.get("debug") === "1";
|
return p.get("debug") === "1";
|
||||||
}, []);
|
}, []);
|
||||||
const [ingesting, setIngesting] = useState(false);
|
const [ingesting, setIngesting] = useState(false);
|
||||||
const [toast, setToast] = useState("");
|
const [toast, setToast] = useState("");
|
||||||
|
const [externalToast, setExternalToast] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!errorMessage) return;
|
||||||
|
setExternalToast(errorMessage);
|
||||||
|
const timer = window.setTimeout(() => setExternalToast(""), 5000);
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [errorMessage]);
|
||||||
|
|
||||||
async function triggerDemoIngest() {
|
async function triggerDemoIngest() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -39,7 +53,12 @@ export default function ChatHeader({ title = "Title of Chat" }) {
|
||||||
<h1 className="text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
|
<h1 className="text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
<DeleteButton />
|
{fileSummary && (
|
||||||
|
<div className="text-xs text-slate-300 bg-gray-800/80 border border-gray-700 rounded px-3 py-1">
|
||||||
|
{fileSummary}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DeleteButton onClick={onClear} disabled={busy} />
|
||||||
{isDebug && (
|
{isDebug && (
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={triggerDemoIngest}
|
onClick={triggerDemoIngest}
|
||||||
|
|
@ -59,6 +78,11 @@ export default function ChatHeader({ title = "Title of Chat" }) {
|
||||||
{toast}
|
{toast}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{externalToast && (
|
||||||
|
<div className="mt-2 text-xs text-red-300 bg-red-900/40 border border-red-700 rounded px-2 py-1 inline-block">
|
||||||
|
{externalToast}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import { MARKDOWN_COMPONENTS } from "src/config/markdown";
|
||||||
|
|
||||||
function MessageBubble({ message }) {
|
function MessageBubble({ message }) {
|
||||||
const isUser = message.role === "user";
|
const isUser = message.role === "user";
|
||||||
|
const isError = !!message.error;
|
||||||
return (
|
return (
|
||||||
<div className={`flex ${isUser ? "justify-end" : "justify-start"} py-2`}>
|
<div className={`flex ${isUser ? "justify-end" : "justify-start"} py-2`}>
|
||||||
<div
|
<div
|
||||||
className={`p-3 rounded-xl ${isUser ? "bg-indigo-600 text-white rounded-tr-sm" : "bg-gray-700 text-slate-100 rounded-tl-sm"}`}
|
className={`p-3 rounded-xl ${isUser ? "bg-indigo-600 text-white rounded-tr-sm" : "bg-gray-700 text-slate-100 rounded-tl-sm"} ${isError ? "border border-red-500/60 bg-red-900/50" : ""}`}
|
||||||
>
|
>
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
<div className="text-sm">{message.content}</div>
|
<div className="text-sm">{message.content}</div>
|
||||||
|
|
@ -22,12 +23,21 @@ function MessageBubble({ message }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatWindow({ messages }) {
|
export default function ChatWindow({ messages }) {
|
||||||
|
const bottomRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bottomRef.current) {
|
||||||
|
bottomRef.current.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-auto px-2 pt-4 pb-32">
|
<div className="flex-1 overflow-auto px-2 pt-4 pb-32">
|
||||||
<div className="">
|
<div className="">
|
||||||
{messages.map((m, i) => (
|
{messages.map((m, i) => (
|
||||||
<MessageBubble key={i} message={m} />
|
<MessageBubble key={m.id ?? i} message={m} />
|
||||||
))}
|
))}
|
||||||
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import DeleteButton from "src/components/ui/button/delete-button";
|
|
||||||
import DownButton from "src/components/ui/button/down-button";
|
import DownButton from "src/components/ui/button/down-button";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { BotMessageSquare } from "lucide-react";
|
import { BotMessageSquare } from "lucide-react";
|
||||||
|
|
||||||
export default function MessageInput({ onSend }) {
|
export default function MessageInput({ onSend, disabled = false }) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
|
|
||||||
|
|
@ -15,7 +14,7 @@ export default function MessageInput({ onSend }) {
|
||||||
|
|
||||||
function handleSubmit(e) {
|
function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!text.trim()) return;
|
if (!text.trim() || disabled) return;
|
||||||
onSend(text.trim());
|
onSend(text.trim());
|
||||||
setText("");
|
setText("");
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +37,7 @@ export default function MessageInput({ onSend }) {
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={text}
|
value={text}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
if (disabled) return;
|
||||||
setText(e.target.value);
|
setText(e.target.value);
|
||||||
// auto-resize
|
// auto-resize
|
||||||
const ta = textareaRef.current;
|
const ta = textareaRef.current;
|
||||||
|
|
@ -49,12 +49,17 @@ export default function MessageInput({ onSend }) {
|
||||||
placeholder="Type a message..."
|
placeholder="Type a message..."
|
||||||
rows={1}
|
rows={1}
|
||||||
className="flex-1 mx-2 rounded-md shadow-2sx border-none focus:border-none focus:outline-none resize-none overflow-auto max-h-40"
|
className="flex-1 mx-2 rounded-md shadow-2sx border-none focus:border-none focus:outline-none resize-none overflow-auto max-h-40"
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<motion.button
|
<motion.button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex gap-2 px-4 py-2 bg-gray-700 rounded-xl ml-4 items-center"
|
className={`flex gap-2 px-4 py-2 bg-gray-700 rounded-xl ml-4 items-center ${
|
||||||
whileHover={{ scale: 1.1 }}
|
disabled ? "cursor-not-allowed" : ""
|
||||||
whileTap={{ scale: 0.9 }}
|
}`}
|
||||||
|
whileHover={disabled ? undefined : { scale: 1.1 }}
|
||||||
|
whileTap={disabled ? undefined : { scale: 0.9 }}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ opacity: disabled ? 0.5 : 1 }}
|
||||||
>
|
>
|
||||||
<BotMessageSquare />
|
<BotMessageSquare />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
|
||||||
75
web-app/src/lib/api.js
Normal file
75
web-app/src/lib/api.js
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
const JSON_HEADERS = {
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function parseJsonResponse(response) {
|
||||||
|
const text = await response.text();
|
||||||
|
const hasBody = text !== "";
|
||||||
|
let data;
|
||||||
|
if (hasBody) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(text);
|
||||||
|
} catch (error) {
|
||||||
|
data = { raw: text };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = data?.error || response.statusText || "Request failed";
|
||||||
|
const err = new Error(message);
|
||||||
|
err.status = response.status;
|
||||||
|
err.body = data;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listFiles() {
|
||||||
|
const response = await fetch("/api/files/list", {
|
||||||
|
method: "GET",
|
||||||
|
headers: JSON_HEADERS,
|
||||||
|
});
|
||||||
|
const data = await parseJsonResponse(response);
|
||||||
|
return Array.isArray(data.files) ? data.files : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createQuery(payload) {
|
||||||
|
const response = await fetch("/api/query/create", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...JSON_HEADERS,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const data = await parseJsonResponse(response);
|
||||||
|
if (!data.id) {
|
||||||
|
throw new Error("Query creation did not return an id");
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQueryStatus(id) {
|
||||||
|
const response = await fetch(`/api/query/status?id=${encodeURIComponent(id)}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: JSON_HEADERS,
|
||||||
|
});
|
||||||
|
return parseJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQueryResult(id) {
|
||||||
|
const response = await fetch(`/api/query/result?id=${encodeURIComponent(id)}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: JSON_HEADERS,
|
||||||
|
});
|
||||||
|
return parseJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelQuery(id) {
|
||||||
|
const response = await fetch(`/api/query/cancel?id=${encodeURIComponent(id)}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: JSON_HEADERS,
|
||||||
|
});
|
||||||
|
return parseJsonResponse(response);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue