Skip to content

Commit

Permalink
Merge pull request #4 from ClinicalTrials/feat/inline-table-views
Browse files Browse the repository at this point in the history
Feat/inline table views
  • Loading branch information
kaikun213 authored and GitHub Enterprise committed May 15, 2023
2 parents 5ac5e0f + 47c2ca1 commit 813bff4
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 36 deletions.
12 changes: 8 additions & 4 deletions frontend/src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Props {
}

export const CodeBlock = (props: Props) => {
const { language, value } = props;
const { language, value, } = props;
const queryStore = useQueryStore();

const showExecuteButton = language.toUpperCase() === "SQL";
Expand Down Expand Up @@ -43,18 +43,22 @@ export const CodeBlock = (props: Props) => {
</button>
</Tooltip>
{showExecuteButton && (
<Tooltip title="Execute" side="top">
<Tooltip title="Open & Edit" side="top">
<button
className="flex justify-center items-center rounded bg-none w-6 h-6 p-1 text-xs text-white bg-bcg-green opacity-90 hover:opacity-100"
onClick={handleExecuteQuery}
>
<Icon.IoPlay className="w-full h-auto" />
<Icon.IoOpenOutline className="w-full h-auto" />
</button>
</Tooltip>
)}
</div>
</div>
<SyntaxHighlighter language={language.toLowerCase()} style={oneDark} customStyle={{ margin: 0 }}>
<SyntaxHighlighter
language={language.toLowerCase()}
style={oneDark}
customStyle={{ margin: 0 }}
>
{value}
</SyntaxHighlighter>
</div>
Expand Down
84 changes: 84 additions & 0 deletions frontend/src/components/CodeFlipCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { CodeBlock } from './CodeBlock';
import AutoExecutorTableView from './ExecutionView/AutoExecutorTableView';
import { useState } from 'react';
import Icon from './Icon';
import Tooltip from './kit/Tooltip';
import { ExecutionResult, } from '~/types';

interface Props {
language: string;
value: string;
title: string;
}

const CodeFlipCard = (props: Props) => {
const { language, value, title } = props;
const [showCode, setShowCode] = useState(false);
const [isHidden, setIsHidden] = useState(false);
const [executionResult, setExecutionResult] = useState<ExecutionResult | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);

return (
<div className="relative w-full">
<div className="relative w-screen sm:w-[calc(80vw)] lg:w-[calc(60vw)] 2xl:w-[calc(60vw)] left-[calc(-30vw+50%)]">
<div className="bg-gray-100 dark:bg-zinc-700 px-4 py-2 rounded-lg">
<div className="flex mb-2">
<div className="flex text-center h-auto align-text-center">
{title}
</div>
<div className="ml-auto">
{!isHidden && (
<Tooltip title={showCode ? "Show results" : "Show code"} side="top">
<button
className="flex justify-center items-center rounded bg-none w-6 h-6 p-1 text-xs text-white bg-bcg-green opacity-90 hover:opacity-100"
onClick={() => setShowCode(!showCode)}
>
{showCode ? (
<Icon.BsTable className="w-full h-auto" />
) : (
<Icon.BsCode className="w-full h-auto" />
)}
</button>
</Tooltip>
)}
</div>
<div className="ml-2">
<Tooltip title={isHidden ? "Show" : "Collapse"} side="top">
<button
className="flex justify-center items-center rounded bg-none w-6 h-6 p-1 text-xs text-white bg-bcg-green opacity-90 hover:opacity-100"
onClick={() => setIsHidden(!isHidden)}
>
{isHidden ? (
<Icon.BiExpandAlt className="w-full h-auto" />
) : (
<Icon.BiCollapseAlt className="w-full h-auto" />
)}
</button>
</Tooltip>
</div>
</div>
{!isHidden && (
showCode ? (
<div className="bg-zinc-900 rounded-lg">
<CodeBlock
language={language}
value={value}
/>
</div>
) : (
<AutoExecutorTableView
statement={value}
executionResult={executionResult}
setExecutionResult={setExecutionResult}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
)
)}
</div>
</div>
</div>
)
}

export default CodeFlipCard
61 changes: 43 additions & 18 deletions frontend/src/components/ConversationView/MessageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import Icon from "../Icon";
import { CodeBlock } from "../CodeBlock";
import ThreeDotsLoader from "./ThreeDotsLoader";
import { AgentIcon } from "../CustomIcons/AgentIcon";
import { SqlSnippet } from "../SqlSnippet";
import rehypeRaw from 'rehype-raw'
import { LLMResponse } from "./LLMResponse";
import { Action } from "./Action";
import CodeFlipCard from "../CodeFlipCard";
import Tooltip from "../kit/Tooltip";

interface Props {
message: Message;
Expand Down Expand Up @@ -47,15 +48,21 @@ const MessageView = (props: Props) => {
messageStore.clearMessage((item) => item.id !== message.id);
};

const sqlSnippets = useMemo(() => {
const regex = /```sql([\s\S]*?)```/g;
const sqlSnippets = [];
const codeSnippets = useMemo(() => {
const sqlRegex = /```sql([\s\S]*?)```/g;
const codeSnippets = [];
let match;

while ((match = regex.exec(message.content)) !== null) {
sqlSnippets.push((match[1] as string).trim().replace("\n", ""));
let idx = 1
while ((match = sqlRegex.exec(message.content)) !== null) {
codeSnippets.push({
code: (match[1] as string).trim().replace("\n", " "),
language: "sql",
title: `Table ${idx}`
});
idx += 1;
}
return sqlSnippets;
return codeSnippets;
}, [message.content]);

let messageContent = message.content
Expand Down Expand Up @@ -141,16 +148,29 @@ const MessageView = (props: Props) => {
const child = children[0] as ReactElement;
const match = /language-(\w+)/.exec(child.props.className || "");
const language = match ? match[1] : "SQL";

const code = child.props.children[0].trim().replace("\n", " ")
const tableIdx = codeSnippets.findIndex(s => s.code.includes(code))
if (tableIdx >= 0) {
return (<div>{`Shown in Table ${tableIdx+1}`}</div>)
}
return (
<pre className={`${className || ""} w-full p-0 my-1`} {...props}>
<CodeBlock
key={Math.random()}
language={language || "SQL"}
value={String(child.props.children).replace(/\n$/, "")}
{...props}
/>
</pre>
<div className="flex">
<ThreeDotsLoader />
<div className="ml-2">Loading code...</div>
</div>
);
// Show this instead if no inline-table activation is desired
// return (
// <pre className={`${className || ""} w-full p-0 my-1`} {...props}>
// <CodeBlock
// key={Math.random()}
// language={language || "SQL"}
// value={String(child.props.children).replace(/\n$/, "")}
// {...props}
// />
// </pre>
// )
},
code({ children }) {
return <code className="px-0">{children}</code>;
Expand All @@ -167,10 +187,15 @@ const MessageView = (props: Props) => {
>
{messageContent}
</ReactMarkdown>
{sqlSnippets.length > 0 && (
{codeSnippets.length > 0 && (
<div className="w-full flex flex-row justify-start items-center mt-1">
{sqlSnippets.map((snippet, index) => (
<SqlSnippet key={index} snippet={snippet} />
{codeSnippets.map((snippet, index) => (
<CodeFlipCard
language={snippet.language}
value={snippet.code}
title={snippet.title}
key={index}
/>
))}
</div>
)}
Expand Down
98 changes: 98 additions & 0 deletions frontend/src/components/ExecutionView/AutoExecutorTableView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { use, useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useQueryStore } from "@/store";
import { ExecutionResult, ResponseObject } from "@/types";
import { checkStatementIsSelect, getMessageFromExecutionResult } from "@/utils";
import Icon from "../Icon";
import DataTableView from "./DataTableView";
import NotificationView from "./NotificationView";

interface Props {
statement: string;
executionResult: ExecutionResult | undefined;
setExecutionResult: (result: ExecutionResult | undefined) => void;
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
}

const AutoExecutorTableView = (props: Props) => {
const { statement, executionResult, setExecutionResult, isLoading, setIsLoading } = props;
const executionMessage = executionResult ? getMessageFromExecutionResult(executionResult) : "";

useEffect(() => {
if (!executionResult) {
if (statement !== "" && checkStatementIsSelect(statement)) {
executeStatement(statement);
}
setExecutionResult(undefined);
}
}, [statement]);

const executeStatement = async (statement: string) => {
if (!statement) {
toast.error("Please enter a statement.");
setIsLoading(false);
setExecutionResult(undefined);
return;
}

setIsLoading(true);
setExecutionResult(undefined);
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/sql/execute`, {
method: "POST",
headers: {
"Content-Type": "application/json",
// TODO: Add authorisation header (JWT)
// "Authorization": `Bearer ${API_KEY}`,
},
body: JSON.stringify({
statement,
}),
});
const result = (await response.json()) as ResponseObject<ExecutionResult>;
if (result.data) {
setExecutionResult(result.data);
} else if (!result.data && result.message) {
setExecutionResult({
rawResult: [],
error: result.message,
});
}
} catch (error) {
console.error(error);
toast.error("Failed to execute statement");
} finally {
setIsLoading(false);
}
};

return (
<div className="dark:text-gray-300 flex flex-col justify-start items-start p-4">
<div className="w-full flex flex-col justify-start items-start mt-4">
{isLoading ? (
<div className="w-full flex flex-col justify-center items-center py-6 pt-10">
<Icon.BiLoaderAlt className="w-7 h-auto opacity-70 animate-spin" />
<span className="text-sm font-mono text-gray-500 mt-2">
Executing query...
</span>
</div>
) : (
<>
{executionResult ? (
executionMessage ? (
<NotificationView message={executionMessage} style={executionResult?.error ? "error" : "info"} />
) : (
<DataTableView rawResults={executionResult?.rawResult || []} />
)
) : (
<></>
)}
</>
)}
</div>
</div>
);
};

export default AutoExecutorTableView;
10 changes: 10 additions & 0 deletions frontend/src/components/ExecutionView/DataTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@ interface Props {
rawResults: RawResult[];
}

const getWidth = (rawResult: RawResult, key: string) => {
const value = rawResult[key as string]
const hasWidth = typeof(value) === "string"
return hasWidth && value.length <= 100 ? "25em" : undefined
}

const DataTableView = (props: Props) => {
const { rawResults } = props;
const columns = Object.keys(head(rawResults) || {}).map((key) => {
return {
name: key,
sortable: true,
selector: (row: any) => row[key],
wrap: true,
maxWidth: getWidth(head(rawResults) || {}, key),
padding: "0.5em",
className: "dark:border-zinc-700"
};
});

Expand Down
14 changes: 0 additions & 14 deletions frontend/src/components/SqlSnippet.tsx

This file was deleted.

0 comments on commit 813bff4

Please sign in to comment.