Updates
This commit is contained in:
parent
6d833c7d3b
commit
979728e6c8
@ -1,4 +1,4 @@
|
|||||||
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
import React from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import Button from "../layout/Button";
|
import Button from "../layout/Button";
|
||||||
@ -25,6 +25,8 @@ export default function ModalComponent({ open, setOpen, ...props }: Props) {
|
|||||||
"flex flex-col items-center justify-center p-4",
|
"flex flex-col items-center justify-center p-4",
|
||||||
"twui-modal-root"
|
"twui-modal-root"
|
||||||
)}
|
)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
|
@ -80,6 +80,8 @@ export default function PopoverComponent({
|
|||||||
onMouseLeave={
|
onMouseLeave={
|
||||||
trigger === "hover" ? popoverLeaveFn : props.onMouseLeave
|
trigger === "hover" ? popoverLeaveFn : props.onMouseLeave
|
||||||
}
|
}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
{/* <div
|
{/* <div
|
||||||
className="absolute w-0 h-0 border-8 border-transparent bg-white"
|
className="absolute w-0 h-0 border-8 border-transparent bg-white"
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--breakpoint-xs: 350px;
|
--breakpoint-xs: 350px;
|
||||||
|
--breakpoint-xxs: 300px;
|
||||||
|
--breakpoint-xxl: 1600px;
|
||||||
|
|
||||||
--color-background-light: #ffffff;
|
--color-background-light: #ffffff;
|
||||||
--color-foreground-light: #171717;
|
--color-foreground-light: #171717;
|
||||||
|
@ -69,7 +69,13 @@ export default function AceEditor({
|
|||||||
theme: darkMode
|
theme: darkMode
|
||||||
? "ace/theme/tomorrow_night_eighties"
|
? "ace/theme/tomorrow_night_eighties"
|
||||||
: "ace/theme/ace_light",
|
: "ace/theme/ace_light",
|
||||||
value: content,
|
value: (() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(content), null, 4);
|
||||||
|
} catch (error) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
placeholder: placeholder ? placeholder : "",
|
placeholder: placeholder ? placeholder : "",
|
||||||
enableBasicAutocompletion: true,
|
enableBasicAutocompletion: true,
|
||||||
enableLiveAutocompletion: true,
|
enableLiveAutocompletion: true,
|
||||||
@ -108,7 +114,7 @@ export default function AceEditor({
|
|||||||
return function () {
|
return function () {
|
||||||
editor.destroy();
|
editor.destroy();
|
||||||
};
|
};
|
||||||
}, [refresh, darkMode, ready, externalRefresh]);
|
}, [refresh, darkMode, ready, externalRefresh, content]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const htmlClassName = document.documentElement.className;
|
const htmlClassName = document.documentElement.className;
|
||||||
|
@ -84,7 +84,7 @@ export default function TinyMCEEditor<KeyType extends string>({
|
|||||||
if (defaultValue) editor.setContent(defaultValue);
|
if (defaultValue) editor.setContent(defaultValue);
|
||||||
setReady(true);
|
setReady(true);
|
||||||
|
|
||||||
editor.on("input", (e) => {
|
editor.on("change", (e) => {
|
||||||
changeHandler?.(editor.getContent());
|
changeHandler?.(editor.getContent());
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ export default function TinyMCEEditor<KeyType extends string>({
|
|||||||
useParentStyles(editor);
|
useParentStyles(editor);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
base_url: "https://datasquirel.com/tinymce-public",
|
base_url: "https://www.datasquirel.com/tinymce-public",
|
||||||
body_class: "twui-tinymce",
|
body_class: "twui-tinymce",
|
||||||
placeholder,
|
placeholder,
|
||||||
relative_urls: true,
|
relative_urls: true,
|
||||||
@ -118,6 +118,9 @@ export default function TinyMCEEditor<KeyType extends string>({
|
|||||||
"bg-background-light dark:bg-background-dark",
|
"bg-background-light dark:bg-background-dark",
|
||||||
wrapperWrapperProps?.className
|
wrapperWrapperProps?.className
|
||||||
)}
|
)}
|
||||||
|
onInput={(e) => {
|
||||||
|
console.log(`Input Detected`);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{showLabel && (
|
{showLabel && (
|
||||||
<label
|
<label
|
||||||
|
@ -16,7 +16,8 @@ export default function useTinyMCE() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = "https://datasquirel.com/tinymce-public/tinymce.min.js";
|
script.src =
|
||||||
|
"https://www.datasquirel.com/tinymce-public/tinymce.min.js";
|
||||||
script.async = true;
|
script.async = true;
|
||||||
|
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
@ -40,7 +40,7 @@ export default function Card({
|
|||||||
ref={elRef}
|
ref={elRef}
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-row items-center p-4 rounded-default bg-white dark:bg-white/10",
|
"flex flex-row items-center p-4 rounded-default bg-background-light dark:bg-background-dark",
|
||||||
"border border-slate-200 dark:border-white/10 border-solid",
|
"border border-slate-200 dark:border-white/10 border-solid",
|
||||||
noHover ? "" : "twui-card",
|
noHover ? "" : "twui-card",
|
||||||
props.className
|
props.className
|
||||||
|
59
components/lib/elements/CopySlug.tsx
Normal file
59
components/lib/elements/CopySlug.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React, {
|
||||||
|
ComponentProps,
|
||||||
|
Dispatch,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
} from "react";
|
||||||
|
import { Copy, LucideProps } from "lucide-react";
|
||||||
|
import Button from "../layout/Button";
|
||||||
|
|
||||||
|
type Props = Omit<ComponentProps<typeof Button>, "title"> & {
|
||||||
|
slugText: string;
|
||||||
|
justIcon?: boolean;
|
||||||
|
noIcon?: boolean;
|
||||||
|
title?: string;
|
||||||
|
outlined?: boolean;
|
||||||
|
successMsg?: string | ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
iconProps?: LucideProps;
|
||||||
|
setToastOpen?: Dispatch<SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CopySlug({
|
||||||
|
slugText,
|
||||||
|
justIcon,
|
||||||
|
noIcon,
|
||||||
|
title,
|
||||||
|
outlined,
|
||||||
|
successMsg,
|
||||||
|
iconProps,
|
||||||
|
icon,
|
||||||
|
setToastOpen,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
title={title || slugText}
|
||||||
|
size="smaller"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
{...props}
|
||||||
|
onClick={(e) => {
|
||||||
|
navigator.clipboard.writeText(slugText).then(() => {
|
||||||
|
setToastOpen?.(false);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setToastOpen?.(true);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
props.onClick?.(e);
|
||||||
|
}}
|
||||||
|
style={{ ...(outlined ? {} : { padding: 0 }), ...props.style }}
|
||||||
|
>
|
||||||
|
{noIcon
|
||||||
|
? null
|
||||||
|
: icon || <Copy size={outlined ? 15 : 20} {...iconProps} />}
|
||||||
|
{!justIcon && (title ? title : "Copy Slug")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@ -103,8 +103,8 @@ export default function HeaderNavLinkComponent({
|
|||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
target={mainLinkComponent}
|
target={mainLinkComponent}
|
||||||
position="bottom-right"
|
position="center"
|
||||||
// hoverOpen
|
hoverOpen
|
||||||
className="hidden xl:flex"
|
className="hidden xl:flex"
|
||||||
>
|
>
|
||||||
{dropdown ? (
|
{dropdown ? (
|
||||||
|
@ -130,7 +130,8 @@ export default function LinkList({
|
|||||||
{...link.linkProps}
|
{...link.linkProps}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"p-2 cursor-pointer whitespace-nowrap",
|
"p-2 cursor-pointer whitespace-nowrap",
|
||||||
linkProps?.className
|
linkProps?.className,
|
||||||
|
link.linkProps?.className
|
||||||
)}
|
)}
|
||||||
strict={link.strict}
|
strict={link.strict}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -23,7 +23,7 @@ export default function Paper({
|
|||||||
{...props}
|
{...props}
|
||||||
ref={componentRef as any}
|
ref={componentRef as any}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-col items-start p-4 rounded bg-white dark:bg-white/10 gap-4",
|
"flex flex-col items-start p-4 rounded bg-background-light dark:bg-background-dark gap-4",
|
||||||
"border border-slate-200 dark:border-white/10 border-solid w-full",
|
"border border-slate-200 dark:border-white/10 border-solid w-full",
|
||||||
"relative",
|
"relative",
|
||||||
"twui-paper",
|
"twui-paper",
|
||||||
|
@ -39,7 +39,9 @@ export default function Search<KeyType extends string>({
|
|||||||
placeholder,
|
placeholder,
|
||||||
...props
|
...props
|
||||||
}: SearchProps<KeyType>) {
|
}: SearchProps<KeyType>) {
|
||||||
const [input, setInput] = React.useState("");
|
const [input, setInput] = React.useState(
|
||||||
|
props.defaultValue?.toString() || ""
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
@ -26,6 +26,7 @@ export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
|
|||||||
*/
|
*/
|
||||||
switchComponent?: ReactNode;
|
switchComponent?: ReactNode;
|
||||||
setActiveValue?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setActiveValue?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
changeHandler?: (value: TWUITabsObject) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,6 +44,7 @@ export default function Tabs({
|
|||||||
debounce = 100,
|
debounce = 100,
|
||||||
switchComponent,
|
switchComponent,
|
||||||
setActiveValue: existingSetActiveValue,
|
setActiveValue: existingSetActiveValue,
|
||||||
|
changeHandler,
|
||||||
...props
|
...props
|
||||||
}: TWUI_TOGGLE_PROPS) {
|
}: TWUI_TOGGLE_PROPS) {
|
||||||
const finalTabsContentArray = tabsContentArray
|
const finalTabsContentArray = tabsContentArray
|
||||||
@ -70,6 +72,9 @@ export default function Tabs({
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
existingSetActiveValue?.(activeValue);
|
existingSetActiveValue?.(activeValue);
|
||||||
|
if (targetContent && activeValue) {
|
||||||
|
changeHandler?.(targetContent);
|
||||||
|
}
|
||||||
}, [activeValue]);
|
}, [activeValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -51,7 +51,7 @@ export default function Tag({
|
|||||||
? "bg-orange-700 outline-orange-700"
|
? "bg-orange-700 outline-orange-700"
|
||||||
: color == "gray"
|
: color == "gray"
|
||||||
? twMerge(
|
? twMerge(
|
||||||
"bg-slate-100 outline-slate-200 dark:bg-white/10 dark:outline-white/20",
|
"bg-slate-100 outline-slate-200 dark:bg-gray-dark dark:outline-gray-dark",
|
||||||
"text-slate-800 dark:text-white"
|
"text-slate-800 dark:text-white"
|
||||||
)
|
)
|
||||||
: "bg-primary text-white outline-primbg-primary twui-tag-primary",
|
: "bg-primary text-white outline-primbg-primary twui-tag-primary",
|
||||||
|
81
components/lib/elements/ai/AIPromptActionSection.tsx
Normal file
81
components/lib/elements/ai/AIPromptActionSection.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Send, X } from "lucide-react";
|
||||||
|
import React, { Dispatch, SetStateAction } from "react";
|
||||||
|
import { ChatCompletionMessageParam } from "openai/resources/index";
|
||||||
|
import Row from "../../layout/Row";
|
||||||
|
import Button from "../../layout/Button";
|
||||||
|
import CopySlug from "../CopySlug";
|
||||||
|
import AIPromptHistoryModal from "./AIPromptHistoryModal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
streamRes: string;
|
||||||
|
setStreamRes: Dispatch<SetStateAction<string>>;
|
||||||
|
setPrompt: Dispatch<SetStateAction<string>>;
|
||||||
|
loading: boolean;
|
||||||
|
promptFn: (prompt: string) => void;
|
||||||
|
history: ChatCompletionMessageParam[];
|
||||||
|
prompt: string;
|
||||||
|
currentPromptRef: React.MutableRefObject<string>;
|
||||||
|
promptInputRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AIPromptActionSection({
|
||||||
|
streamRes,
|
||||||
|
setStreamRes,
|
||||||
|
loading,
|
||||||
|
promptFn,
|
||||||
|
history,
|
||||||
|
prompt,
|
||||||
|
setPrompt,
|
||||||
|
currentPromptRef,
|
||||||
|
promptInputRef,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Row className="w-full justify-between">
|
||||||
|
<Row className="gap-4">
|
||||||
|
{streamRes.match(/./) && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button
|
||||||
|
title="Clear AI Result"
|
||||||
|
variant="ghost"
|
||||||
|
size="smaller"
|
||||||
|
color="gray"
|
||||||
|
className="px-0"
|
||||||
|
beforeIcon={<X size={20} />}
|
||||||
|
onClick={() => {
|
||||||
|
setStreamRes("");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CopySlug
|
||||||
|
slugText={streamRes}
|
||||||
|
justIcon
|
||||||
|
iconProps={{ size: 18 }}
|
||||||
|
title="Copy Content"
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<AIPromptHistoryModal history={history} />
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Button
|
||||||
|
title="Send Prompt"
|
||||||
|
beforeIcon={<Send size={20} />}
|
||||||
|
loading={loading}
|
||||||
|
className="p-2"
|
||||||
|
onClick={() => {
|
||||||
|
currentPromptRef.current = prompt;
|
||||||
|
setTimeout(() => {
|
||||||
|
setPrompt("");
|
||||||
|
if (promptInputRef.current) {
|
||||||
|
promptInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
promptFn(prompt);
|
||||||
|
}}
|
||||||
|
loadingProps={{ size: "smaller" }}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
98
components/lib/elements/ai/AIPromptBlock.tsx
Normal file
98
components/lib/elements/ai/AIPromptBlock.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { ChatCompletionMessageParam } from "openai/resources/index";
|
||||||
|
import React from "react";
|
||||||
|
import Paper from "../Paper";
|
||||||
|
import Stack from "../../layout/Stack";
|
||||||
|
import AIPromptPreview from "./AIPromptPreview";
|
||||||
|
import LoadingOverlay from "../LoadingOverlay";
|
||||||
|
import Textarea from "../../form/Textarea";
|
||||||
|
import AIPromptActionSection from "./AIPromptActionSection";
|
||||||
|
import Card from "../Card";
|
||||||
|
import Row from "../../layout/Row";
|
||||||
|
import Span from "../../layout/Span";
|
||||||
|
import { MessageCircleMore } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
model?: string;
|
||||||
|
promptFn: (prompt: string) => void;
|
||||||
|
history?: ChatCompletionMessageParam[];
|
||||||
|
loading?: boolean;
|
||||||
|
mdRes?: string;
|
||||||
|
setMdRes: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AIPromptBlock({
|
||||||
|
model,
|
||||||
|
promptFn,
|
||||||
|
history = [],
|
||||||
|
loading = false,
|
||||||
|
mdRes = "",
|
||||||
|
setMdRes,
|
||||||
|
}: Props) {
|
||||||
|
const [prompt, setPrompt] = React.useState("");
|
||||||
|
const currentPromptRef = React.useRef("");
|
||||||
|
const promptInputRef = React.useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className="">
|
||||||
|
<Stack className="w-full">
|
||||||
|
{currentPromptRef.current && (
|
||||||
|
<Row className="w-full justify-end">
|
||||||
|
<Card className="py-1.5 px-2.5 text-xs">
|
||||||
|
<Row>
|
||||||
|
<Span>{currentPromptRef.current}</Span>
|
||||||
|
<MessageCircleMore
|
||||||
|
size={15}
|
||||||
|
opacity={0.5}
|
||||||
|
className="-mt-[1px]"
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
<AIPromptPreview
|
||||||
|
setStreamRes={setMdRes}
|
||||||
|
streamRes={mdRes}
|
||||||
|
history={history}
|
||||||
|
/>
|
||||||
|
<Stack className="w-full relative">
|
||||||
|
{loading && <LoadingOverlay />}
|
||||||
|
<Textarea
|
||||||
|
placeholder={model ? `Prompt ${model}` : "Prompt AI"}
|
||||||
|
wrapperProps={{ className: "outline-none" }}
|
||||||
|
wrapperWrapperProps={{ className: "w-full" }}
|
||||||
|
value={prompt}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPrompt(e.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key == "Enter" && !e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
currentPromptRef.current = prompt;
|
||||||
|
setTimeout(() => {
|
||||||
|
setPrompt("");
|
||||||
|
if (promptInputRef.current) {
|
||||||
|
promptInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
promptFn(prompt);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
componentRef={promptInputRef}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<AIPromptActionSection
|
||||||
|
loading={loading}
|
||||||
|
promptFn={promptFn}
|
||||||
|
setStreamRes={setMdRes}
|
||||||
|
streamRes={mdRes}
|
||||||
|
history={history}
|
||||||
|
prompt={prompt}
|
||||||
|
setPrompt={setPrompt}
|
||||||
|
currentPromptRef={currentPromptRef}
|
||||||
|
promptInputRef={promptInputRef as any}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
99
components/lib/elements/ai/AIPromptHistoryModal.tsx
Normal file
99
components/lib/elements/ai/AIPromptHistoryModal.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ChatCompletionMessageParam } from "openai/resources/index";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { Bot, User } from "lucide-react";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
import Button from "../../layout/Button";
|
||||||
|
import Stack from "../../layout/Stack";
|
||||||
|
import H2 from "../../layout/H2";
|
||||||
|
import Span from "../../layout/Span";
|
||||||
|
import Divider from "../../layout/Divider";
|
||||||
|
import Row from "../../layout/Row";
|
||||||
|
import Card from "../Card";
|
||||||
|
import Border from "../Border";
|
||||||
|
import MarkdownEditorPreviewComponent from "../../mdx/markdown/MarkdownEditorPreviewComponent";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
history: ChatCompletionMessageParam[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AIPromptHistoryModal({ history }: Props) {
|
||||||
|
if (!history[0]) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
target={
|
||||||
|
<Button
|
||||||
|
title="View Chat History"
|
||||||
|
size="smaller"
|
||||||
|
color="gray"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
View History
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className="max-w-[900px] bg-slate-100 dark:bg-white/5 xl:p-10"
|
||||||
|
>
|
||||||
|
<Stack className="gap-10 w-full">
|
||||||
|
<Stack className="gap-1">
|
||||||
|
<H2 className="!text-xl m-0">Chat History</H2>
|
||||||
|
<Span className="text-xs">
|
||||||
|
AI chat history for this session.
|
||||||
|
</Span>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{history.map((hst, index) => {
|
||||||
|
if (hst.role == "user") {
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
key={index}
|
||||||
|
className="w-full items-start justify-end"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={twMerge(
|
||||||
|
"bg-background-dark text-foreground-dark dark:!bg-background-light dark:text-foreground-light"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{hst.content?.toString()}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Border className="w-10 h-10 rounded-full p-2 items-center justify-center">
|
||||||
|
<User />
|
||||||
|
</Border>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
key={index}
|
||||||
|
className="w-full items-start flex-nowrap"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Border
|
||||||
|
className={twMerge(
|
||||||
|
"w-10 h-10 rounded-full items-center justify-center bg-white p-2",
|
||||||
|
"dark:bg-background-dark"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Bot />
|
||||||
|
</Border>
|
||||||
|
</Stack>
|
||||||
|
<Card className="grow overflow-x-auto xl:p-8">
|
||||||
|
<MarkdownEditorPreviewComponent
|
||||||
|
value={hst.content?.toString() || ""}
|
||||||
|
maxHeight="none"
|
||||||
|
wrapperProps={{
|
||||||
|
className:
|
||||||
|
"border-none p-0 ai-response-content w-full",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
51
components/lib/elements/ai/AIPromptPreview.tsx
Normal file
51
components/lib/elements/ai/AIPromptPreview.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import Divider from "@/src/components/twui/layout/Divider";
|
||||||
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
|
import React from "react";
|
||||||
|
import MarkdownEditorPreviewComponent from "@/src/components/twui/mdx/markdown/MarkdownEditorPreviewComponent";
|
||||||
|
import { ChatCompletionMessageParam } from "openai/resources/index";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
streamRes: string;
|
||||||
|
setStreamRes: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
history: ChatCompletionMessageParam[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AIPromptPreview({
|
||||||
|
setStreamRes,
|
||||||
|
streamRes,
|
||||||
|
history,
|
||||||
|
}: Props) {
|
||||||
|
const responseContentRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const isContentInterrupted = React.useRef(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isContentInterrupted.current) return;
|
||||||
|
|
||||||
|
if (responseContentRef.current) {
|
||||||
|
responseContentRef.current.scrollTop =
|
||||||
|
responseContentRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [streamRes]);
|
||||||
|
|
||||||
|
if (!streamRes?.match(/./)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className="w-full">
|
||||||
|
<MarkdownEditorPreviewComponent
|
||||||
|
value={streamRes}
|
||||||
|
maxHeight="40vh"
|
||||||
|
wrapperProps={{
|
||||||
|
className: "border-none p-0 ai-response-content",
|
||||||
|
componentRef: responseContentRef as any,
|
||||||
|
onMouseEnter: () => {
|
||||||
|
isContentInterrupted.current = true;
|
||||||
|
},
|
||||||
|
onMouseLeave: () => {
|
||||||
|
isContentInterrupted.current = false;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -11,10 +11,14 @@ import Row from "../layout/Row";
|
|||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
import Span from "../layout/Span";
|
import Span from "../layout/Span";
|
||||||
|
|
||||||
export type CheckboxProps = React.DetailedHTMLProps<
|
export type CheckboxProps = Omit<
|
||||||
|
React.DetailedHTMLProps<
|
||||||
React.HTMLAttributes<HTMLDivElement>,
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
|
>,
|
||||||
|
"title"
|
||||||
> & {
|
> & {
|
||||||
|
title?: string | ReactNode;
|
||||||
wrapperProps?: DetailedHTMLProps<
|
wrapperProps?: DetailedHTMLProps<
|
||||||
HTMLAttributes<HTMLDivElement>,
|
HTMLAttributes<HTMLDivElement>,
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
@ -29,6 +33,7 @@ export type CheckboxProps = React.DetailedHTMLProps<
|
|||||||
setChecked?: React.Dispatch<React.SetStateAction<boolean>>;
|
setChecked?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
noLabel?: boolean;
|
||||||
size?: number;
|
size?: number;
|
||||||
changeHandler?: (value: boolean) => void;
|
changeHandler?: (value: boolean) => void;
|
||||||
info?: string | ReactNode;
|
info?: string | ReactNode;
|
||||||
@ -54,6 +59,8 @@ export default function Checkbox({
|
|||||||
changeHandler,
|
changeHandler,
|
||||||
info,
|
info,
|
||||||
wrapperWrapperProps,
|
wrapperWrapperProps,
|
||||||
|
noLabel,
|
||||||
|
title,
|
||||||
...props
|
...props
|
||||||
}: CheckboxProps) {
|
}: CheckboxProps) {
|
||||||
const finalSize = size || 20;
|
const finalSize = size || 20;
|
||||||
@ -62,8 +69,8 @@ export default function Checkbox({
|
|||||||
defaultChecked || externalChecked || false
|
defaultChecked || externalChecked || false
|
||||||
);
|
);
|
||||||
|
|
||||||
const finalTitle = props.title
|
const finalTitle = title
|
||||||
? props.title
|
? title
|
||||||
: `Checkbox-${Math.round(Math.random() * 100000)}`;
|
: `Checkbox-${Math.round(Math.random() * 100000)}`;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -112,6 +119,7 @@ export default function Checkbox({
|
|||||||
>
|
>
|
||||||
{checked && <CheckMarkSVG />}
|
{checked && <CheckMarkSVG />}
|
||||||
</div>
|
</div>
|
||||||
|
{!noLabel && (
|
||||||
<Stack className="gap-0.5">
|
<Stack className="gap-0.5">
|
||||||
<div
|
<div
|
||||||
{...labelProps}
|
{...labelProps}
|
||||||
@ -123,6 +131,7 @@ export default function Checkbox({
|
|||||||
{label || finalTitle}
|
{label || finalTitle}
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{info && (
|
{info && (
|
||||||
<Row className="gap-1" title={info.toString()}>
|
<Row className="gap-1" title={info.toString()}>
|
||||||
|
@ -12,12 +12,23 @@ import Row from "../layout/Row";
|
|||||||
import Input from "./Input";
|
import Input from "./Input";
|
||||||
import Loading from "../elements/Loading";
|
import Loading from "../elements/Loading";
|
||||||
|
|
||||||
|
type FileInputUtils = {
|
||||||
|
clearFileInput?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
type ImageUploadProps = DetailedHTMLProps<
|
type ImageUploadProps = DetailedHTMLProps<
|
||||||
React.HTMLAttributes<HTMLDivElement>,
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
> & {
|
> & {
|
||||||
onChangeHandler?: (
|
onChangeHandler?: (
|
||||||
fileData: FileInputToBase64FunctionReturn | undefined
|
fileData: FileInputToBase64FunctionReturn | undefined,
|
||||||
|
inputRef?: React.RefObject<HTMLInputElement | null>,
|
||||||
|
utils?: FileInputUtils
|
||||||
|
) => any;
|
||||||
|
changeHandler?: (
|
||||||
|
fileData: FileInputToBase64FunctionReturn | undefined,
|
||||||
|
inputRef?: React.RefObject<HTMLInputElement | null>,
|
||||||
|
utils?: FileInputUtils
|
||||||
) => any;
|
) => any;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
fileInputProps?: DetailedHTMLProps<
|
fileInputProps?: DetailedHTMLProps<
|
||||||
@ -74,6 +85,7 @@ export default function FileUpload({
|
|||||||
loading,
|
loading,
|
||||||
multiple,
|
multiple,
|
||||||
onClear,
|
onClear,
|
||||||
|
changeHandler,
|
||||||
...props
|
...props
|
||||||
}: ImageUploadProps) {
|
}: ImageUploadProps) {
|
||||||
const [file, setFile] = React.useState<
|
const [file, setFile] = React.useState<
|
||||||
@ -98,6 +110,19 @@ export default function FileUpload({
|
|||||||
}
|
}
|
||||||
}, [existingFile]);
|
}, [existingFile]);
|
||||||
|
|
||||||
|
function clearFileInput() {
|
||||||
|
setFile(undefined);
|
||||||
|
externalSetFile?.(undefined);
|
||||||
|
onChangeHandler?.(undefined);
|
||||||
|
changeHandler?.(undefined);
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = "";
|
||||||
|
}
|
||||||
|
onClear?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileInputUtils: FileInputUtils = { clearFileInput };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
{...props}
|
{...props}
|
||||||
@ -136,7 +161,12 @@ export default function FileUpload({
|
|||||||
(res) => {
|
(res) => {
|
||||||
setFile(res);
|
setFile(res);
|
||||||
externalSetFile?.(res);
|
externalSetFile?.(res);
|
||||||
onChangeHandler?.(res);
|
onChangeHandler?.(
|
||||||
|
res,
|
||||||
|
inputRef,
|
||||||
|
fileInputUtils
|
||||||
|
);
|
||||||
|
changeHandler?.(res, inputRef, fileInputUtils);
|
||||||
fileInputProps?.onChange?.(e);
|
fileInputProps?.onChange?.(e);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -191,13 +221,7 @@ export default function FileUpload({
|
|||||||
"hover:bg-white dark:hover:bg-black"
|
"hover:bg-white dark:hover:bg-black"
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
setFile(undefined);
|
clearFileInput();
|
||||||
externalSetFile?.(undefined);
|
|
||||||
onChangeHandler?.(undefined);
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.value = "";
|
|
||||||
}
|
|
||||||
onClear?.();
|
|
||||||
}}
|
}}
|
||||||
title="Cancel File Upload Button"
|
title="Cancel File Upload Button"
|
||||||
>
|
>
|
||||||
@ -250,6 +274,7 @@ export default function FileUpload({
|
|||||||
setFile(undefined);
|
setFile(undefined);
|
||||||
externalSetFile?.(undefined);
|
externalSetFile?.(undefined);
|
||||||
onChangeHandler?.(undefined);
|
onChangeHandler?.(undefined);
|
||||||
|
changeHandler?.(undefined);
|
||||||
setFileUrl(undefined);
|
setFileUrl(undefined);
|
||||||
}}
|
}}
|
||||||
title="Cancel File Button"
|
title="Cancel File Button"
|
||||||
@ -300,6 +325,7 @@ export default function FileUpload({
|
|||||||
setFile(res);
|
setFile(res);
|
||||||
externalSetFile?.(res);
|
externalSetFile?.(res);
|
||||||
onChangeHandler?.(res);
|
onChangeHandler?.(res);
|
||||||
|
changeHandler?.(res);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -75,6 +75,10 @@ export type InputProps<KeyType extends string> = DetailedHTMLProps<
|
|||||||
info?: string | ReactNode;
|
info?: string | ReactNode;
|
||||||
ready?: boolean;
|
ready?: boolean;
|
||||||
validity?: TWUISelectValidityObject;
|
validity?: TWUISelectValidityObject;
|
||||||
|
clearInputProps?: React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let refreshes = 0;
|
let refreshes = 0;
|
||||||
@ -114,6 +118,7 @@ export default function Input<KeyType extends string>(
|
|||||||
info,
|
info,
|
||||||
changeHandler,
|
changeHandler,
|
||||||
validity: existingValidity,
|
validity: existingValidity,
|
||||||
|
clearInputProps,
|
||||||
...props
|
...props
|
||||||
} = inputProps;
|
} = inputProps;
|
||||||
|
|
||||||
@ -256,7 +261,7 @@ export default function Input<KeyType extends string>(
|
|||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-full outline-none bg-transparent",
|
"w-full outline-none bg-transparent grow",
|
||||||
"twui-textarea",
|
"twui-textarea",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
@ -285,7 +290,7 @@ export default function Input<KeyType extends string>(
|
|||||||
"w-full outline-none bg-transparent border-none",
|
"w-full outline-none bg-transparent border-none",
|
||||||
"hover:border-none hover:outline-none focus:border-none focus:outline-none",
|
"hover:border-none hover:outline-none focus:border-none focus:outline-none",
|
||||||
"dark:bg-transparent dark:outline-none dark:border-none",
|
"dark:bg-transparent dark:outline-none dark:border-none",
|
||||||
"p-0",
|
"p-0 grow",
|
||||||
"twui-input",
|
"twui-input",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
@ -301,6 +306,7 @@ export default function Input<KeyType extends string>(
|
|||||||
onChange={handleValueChange}
|
onChange={handleValueChange}
|
||||||
type={inputType}
|
type={inputType}
|
||||||
defaultValue={defaultInitialValue}
|
defaultValue={defaultInitialValue}
|
||||||
|
autoComplete={autoComplete}
|
||||||
value={props.value ? getFinalValue(props.value) : undefined}
|
value={props.value ? getFinalValue(props.value) : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -381,10 +387,12 @@ export default function Input<KeyType extends string>(
|
|||||||
{props.type == "search" || props.readOnly ? null : (
|
{props.type == "search" || props.readOnly ? null : (
|
||||||
<div
|
<div
|
||||||
title="Clear Input Field"
|
title="Clear Input Field"
|
||||||
|
{...clearInputProps}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"p-1 -my-2 -mx-1 opacity-0 cursor-pointer",
|
"p-1 -my-2 -mx-1 opacity-0 cursor-pointer w-7 h-7",
|
||||||
"bg-background-light dark:bg-background-dark",
|
"bg-background-light dark:bg-background-dark",
|
||||||
"twui-clear-input-field-button"
|
"twui-clear-input-field-button",
|
||||||
|
clearInputProps?.className
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -397,9 +405,10 @@ export default function Input<KeyType extends string>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateValue("");
|
updateValue("");
|
||||||
|
clearInputProps?.onClick?.(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X size={15} />
|
<X className="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
79
components/lib/form/Radios.tsx
Normal file
79
components/lib/form/Radios.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
ComponentProps,
|
||||||
|
DetailedHTMLProps,
|
||||||
|
InputHTMLAttributes,
|
||||||
|
LabelHTMLAttributes,
|
||||||
|
} from "react";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import twuiSlugify from "../utils/slugify";
|
||||||
|
import twuiSlugToNormalText from "../utils/slug-to-normal-text";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type Value = {
|
||||||
|
value: string;
|
||||||
|
title?: string;
|
||||||
|
default?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TWUI_FORM_RADIO_PROPS = {
|
||||||
|
values: Value[];
|
||||||
|
name: string;
|
||||||
|
inputProps?: DetailedHTMLProps<
|
||||||
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
>;
|
||||||
|
labelProps?: DetailedHTMLProps<
|
||||||
|
LabelHTMLAttributes<HTMLLabelElement>,
|
||||||
|
HTMLLabelElement
|
||||||
|
>;
|
||||||
|
wrapperProps?: ComponentProps<typeof Row>;
|
||||||
|
changeHandler?: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Form Radios Component
|
||||||
|
* @className twui-textarea
|
||||||
|
*/
|
||||||
|
export default function Radios({
|
||||||
|
values,
|
||||||
|
name,
|
||||||
|
inputProps,
|
||||||
|
labelProps,
|
||||||
|
wrapperProps,
|
||||||
|
changeHandler,
|
||||||
|
}: TWUI_FORM_RADIO_PROPS) {
|
||||||
|
const finalName = twuiSlugify(name);
|
||||||
|
const finalTitle = twuiSlugToNormalText(finalName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
title={finalTitle}
|
||||||
|
{...wrapperProps}
|
||||||
|
className={twMerge("gap-4", wrapperProps?.className)}
|
||||||
|
>
|
||||||
|
{values.map((v, i) => {
|
||||||
|
const valueName = twuiSlugify(`${finalName}-${v.value}`);
|
||||||
|
const valueTitle = v.title || twuiSlugToNormalText(v.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row key={i} className="gap-1.5">
|
||||||
|
<input
|
||||||
|
id={valueName}
|
||||||
|
type="radio"
|
||||||
|
defaultChecked={v.default}
|
||||||
|
name={finalName}
|
||||||
|
onChange={(e) => {
|
||||||
|
const targetValue = v.value;
|
||||||
|
changeHandler?.(targetValue);
|
||||||
|
}}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
<label htmlFor={valueName} {...labelProps}>
|
||||||
|
{valueTitle}
|
||||||
|
</label>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
@ -47,17 +47,23 @@ export default function SearchSelect<
|
|||||||
const [currentOptions, setCurrentOptions] =
|
const [currentOptions, setCurrentOptions] =
|
||||||
React.useState<TWUISelectOptionObject<KeyType, T>[]>(options);
|
React.useState<TWUISelectOptionObject<KeyType, T>[]>(options);
|
||||||
|
|
||||||
const defaultOption = options.find((opt) => opt.default) || options[0];
|
const defaultOption = (options.find((opt) => opt.default) || options[0]) as
|
||||||
|
| TWUISelectOptionObject<KeyType, T>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
const [value, setValue] = React.useState<
|
const [value, setValue] = React.useState<
|
||||||
TWUISelectOptionObject<KeyType, T>
|
TWUISelectOptionObject<KeyType, T> | undefined
|
||||||
>({
|
>(
|
||||||
value: defaultOption.value,
|
defaultOption
|
||||||
data: defaultOption.data,
|
? {
|
||||||
});
|
value: defaultOption?.value,
|
||||||
|
data: defaultOption?.data,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
const [inputValue, setInputValue] = React.useState<string>(
|
const [inputValue, setInputValue] = React.useState<string>(
|
||||||
defaultOption.value
|
defaultOption?.value || ""
|
||||||
);
|
);
|
||||||
const [selectIndex, setSelectIndex] = React.useState<number | undefined>();
|
const [selectIndex, setSelectIndex] = React.useState<number | undefined>();
|
||||||
|
|
||||||
@ -91,11 +97,14 @@ export default function SearchSelect<
|
|||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (value) {
|
||||||
dispatchState?.(value.data);
|
dispatchState?.(value.data);
|
||||||
setInputValue(value.value);
|
setInputValue(value.value);
|
||||||
|
changeHandler?.(value.value);
|
||||||
|
}
|
||||||
|
|
||||||
clearTimeout(focusTimeout);
|
clearTimeout(focusTimeout);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
changeHandler?.(value.value);
|
|
||||||
setSelectIndex(undefined);
|
setSelectIndex(undefined);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
@ -246,7 +255,7 @@ export default function SearchSelect<
|
|||||||
targetWrapperProps={{ className: "w-full" }}
|
targetWrapperProps={{ className: "w-full" }}
|
||||||
contentWrapperProps={{ className: "w-full" }}
|
contentWrapperProps={{ className: "w-full" }}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
externalOpen={open}
|
externalOpen={currentOptions?.[0] && open}
|
||||||
>
|
>
|
||||||
<Paper
|
<Paper
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@ -255,7 +264,8 @@ export default function SearchSelect<
|
|||||||
componentRef={contentWrapperRef}
|
componentRef={contentWrapperRef}
|
||||||
>
|
>
|
||||||
<Stack className="w-full items-start gap-0">
|
<Stack className="w-full items-start gap-0">
|
||||||
{currentOptions.map((_o, index) => {
|
{currentOptions?.[0]
|
||||||
|
? currentOptions.map((_o, index) => {
|
||||||
const isTargetOption = index === selectIndex;
|
const isTargetOption = index === selectIndex;
|
||||||
const targetOptionClasses = twMerge(
|
const targetOptionClasses = twMerge(
|
||||||
"bg-background-dark dark:bg-background-light text-foreground-dark dark:text-foreground-light",
|
"bg-background-dark dark:bg-background-light text-foreground-dark dark:text-foreground-light",
|
||||||
@ -285,7 +295,8 @@ export default function SearchSelect<
|
|||||||
<Divider />
|
<Divider />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
: null}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -25,8 +25,8 @@ export type TWUISelectValidityObject = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TWUISelectOptionObject<
|
export type TWUISelectOptionObject<
|
||||||
KeyType extends string,
|
KeyType extends string = string,
|
||||||
T extends { [k: string]: any } = any
|
T extends { [k: string]: any } = { [k: string]: any }
|
||||||
> = {
|
> = {
|
||||||
title?: string;
|
title?: string;
|
||||||
value: KeyType;
|
value: KeyType;
|
||||||
@ -36,7 +36,7 @@ export type TWUISelectOptionObject<
|
|||||||
|
|
||||||
export type TWUISelectProps<
|
export type TWUISelectProps<
|
||||||
KeyType extends string,
|
KeyType extends string,
|
||||||
T extends { [k: string]: any } = any
|
T extends { [k: string]: any } = { [k: string]: any }
|
||||||
> = DetailedHTMLProps<
|
> = DetailedHTMLProps<
|
||||||
SelectHTMLAttributes<HTMLSelectElement>,
|
SelectHTMLAttributes<HTMLSelectElement>,
|
||||||
HTMLSelectElement
|
HTMLSelectElement
|
||||||
@ -168,6 +168,7 @@ export default function Select<
|
|||||||
|
|
||||||
<select
|
<select
|
||||||
id={selectID}
|
id={selectID}
|
||||||
|
aria-label={props["aria-label"] || props.title}
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-full pl-3 py-2 rounded-default appearance-none pr-8",
|
"w-full pl-3 py-2 rounded-default appearance-none pr-8",
|
||||||
|
@ -73,7 +73,10 @@ export default function useWebSocket<
|
|||||||
* # Connect to Websocket
|
* # Connect to Websocket
|
||||||
*/
|
*/
|
||||||
const connect = React.useCallback(() => {
|
const connect = React.useCallback(() => {
|
||||||
const wsURL = url;
|
const domain = window.location.origin;
|
||||||
|
const wsURL = url.startsWith(`ws`)
|
||||||
|
? url
|
||||||
|
: domain.replace(/^http/, "ws") + ("/" + url).replace(/\/\//g, "/");
|
||||||
if (!wsURL) return;
|
if (!wsURL) return;
|
||||||
|
|
||||||
let ws = new WebSocket(wsURL);
|
let ws = new WebSocket(wsURL);
|
||||||
|
@ -18,9 +18,10 @@ export default function useWebSocketEventHandler<
|
|||||||
const dataEventListenerCallback = (e: Event) => {
|
const dataEventListenerCallback = (e: Event) => {
|
||||||
const customEvent = e as CustomEvent;
|
const customEvent = e as CustomEvent;
|
||||||
const data = customEvent.detail.data as T | undefined;
|
const data = customEvent.detail.data as T | undefined;
|
||||||
const message = customEvent.detail.message as string | undefined;
|
const __msg = customEvent.detail.message as string | undefined;
|
||||||
|
|
||||||
if (data) setData(data);
|
if (data) setData(data);
|
||||||
if (message) setMessage(message);
|
if (__msg && typeof __msg == "string") setMessage(__msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const messageEventName: (typeof WebSocketEventNames)[number] =
|
const messageEventName: (typeof WebSocketEventNames)[number] =
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { DetailedHTMLProps, ImgHTMLAttributes } from "react";
|
import React, { DetailedHTMLProps, ImgHTMLAttributes, ReactNode } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export type TWUIImageProps = DetailedHTMLProps<
|
export type TWUIImageProps = DetailedHTMLProps<
|
||||||
@ -14,13 +14,23 @@ export type TWUIImageProps = DetailedHTMLProps<
|
|||||||
fallbackImageSrc?: string;
|
fallbackImageSrc?: string;
|
||||||
srcLight?: string;
|
srcLight?: string;
|
||||||
srcDark?: string;
|
srcDark?: string;
|
||||||
|
imgErrSrc?: string;
|
||||||
|
imgErrComp?: ReactNode;
|
||||||
|
imgErrSrcLight?: string;
|
||||||
|
imgErrSrcDark?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # Image Component
|
* # Image Component
|
||||||
* @className twui-img
|
* @className twui-img
|
||||||
*/
|
*/
|
||||||
export default function Img({ ...props }: TWUIImageProps) {
|
export default function Img({
|
||||||
|
imgErrSrc,
|
||||||
|
imgErrComp,
|
||||||
|
imgErrSrcDark,
|
||||||
|
imgErrSrcLight,
|
||||||
|
...props
|
||||||
|
}: TWUIImageProps) {
|
||||||
const width = props.size || props.width;
|
const width = props.size || props.width;
|
||||||
const height = props.size || props.height;
|
const height = props.size || props.height;
|
||||||
const sizeRatio = width && height ? Number(width) / Number(height) : 1;
|
const sizeRatio = width && height ? Number(width) / Number(height) : 1;
|
||||||
@ -70,13 +80,16 @@ export default function Img({ ...props }: TWUIImageProps) {
|
|||||||
|
|
||||||
if (imageError) {
|
if (imageError) {
|
||||||
return (
|
return (
|
||||||
|
imgErrComp || (
|
||||||
<img
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
{...interpolatedProps}
|
{...interpolatedProps}
|
||||||
src={
|
src={
|
||||||
|
imgErrSrc ||
|
||||||
"https://static.datasquirel.com/images/user-images/user-2/castcord-image-preset_thumbnail.jpg"
|
"https://static.datasquirel.com/images/user-images/user-2/castcord-image-preset_thumbnail.jpg"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ export function useMDXComponents(params?: Params) {
|
|||||||
if (React.isValidElement(children) && children.props) {
|
if (React.isValidElement(children) && children.props) {
|
||||||
return (
|
return (
|
||||||
<CodeBlock {...props} backgroundColor={codeBgColor}>
|
<CodeBlock {...props} backgroundColor={codeBgColor}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
{children.props.children}
|
{children.props.children}
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
);
|
);
|
||||||
|
@ -58,6 +58,21 @@ export const work = {
|
|||||||
Devops: {
|
Devops: {
|
||||||
href: "/work/devops",
|
href: "/work/devops",
|
||||||
portfolio: [
|
portfolio: [
|
||||||
|
{
|
||||||
|
title: "TurboCI",
|
||||||
|
description: "Cloud VPS orchestrator that runs any workload",
|
||||||
|
href: "https://turboci.tben.me",
|
||||||
|
technologies: [
|
||||||
|
"Bun",
|
||||||
|
"Shell",
|
||||||
|
"Typescript",
|
||||||
|
"APIs",
|
||||||
|
"Hetzner",
|
||||||
|
"AWS",
|
||||||
|
"GCP",
|
||||||
|
"Azure",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Personal Mail Server",
|
title: "Personal Mail Server",
|
||||||
description:
|
description:
|
||||||
@ -81,7 +96,22 @@ export const work = {
|
|||||||
href: "/work/devops",
|
href: "/work/devops",
|
||||||
portfolio: [
|
portfolio: [
|
||||||
{
|
{
|
||||||
title: "Turbo Sync NPM Module",
|
title: "TurboCI",
|
||||||
|
description: "Cloud VPS orchestrator that runs any workload",
|
||||||
|
href: "https://turboci.tben.me",
|
||||||
|
technologies: [
|
||||||
|
"Bun",
|
||||||
|
"Shell",
|
||||||
|
"Typescript",
|
||||||
|
"APIs",
|
||||||
|
"Hetzner",
|
||||||
|
"AWS",
|
||||||
|
"GCP",
|
||||||
|
"Azure",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Turbo Sync",
|
||||||
description:
|
description:
|
||||||
"The easiest way to synchronize local and remote directories in real time",
|
"The easiest way to synchronize local and remote directories in real time",
|
||||||
href: "https://git.tben.me/Moduletrace/turbo-sync",
|
href: "https://git.tben.me/Moduletrace/turbo-sync",
|
||||||
|
Loading…
Reference in New Issue
Block a user