This commit is contained in:
geoffsee
2025-05-22 23:14:01 -04:00
commit 33679583af
242 changed files with 15090 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
import React from "react";
import { IconButton } from "@chakra-ui/react";
import { LucideHammer } from "lucide-react";
import { toolbarButtonZIndex } from "./toolbar/Toolbar";
export default function BuiltWithButton() {
return (
<IconButton
aria-label="Build Info"
icon={<LucideHammer />}
size="md"
bg="transparent"
stroke="text.accent"
color="text.accent"
onClick={() => alert("Built by Geoff Seemueller")}
_hover={{
bg: "transparent",
svg: {
stroke: "accent.secondary",
transition: "stroke 0.3s ease-in-out",
},
}}
zIndex={toolbarButtonZIndex}
/>
);
}

View File

@@ -0,0 +1,53 @@
import { getColorThemes } from "../layout/theme/color-themes";
import { Center, IconButton, VStack } from "@chakra-ui/react";
import userOptionsStore from "../stores/UserOptionsStore";
import { Circle } from "lucide-react";
import { toolbarButtonZIndex } from "./toolbar/Toolbar";
import React from "react";
import { useIsMobile } from "./contexts/MobileContext";
export function ThemeSelectionOptions() {
const children = [];
const isMobile = useIsMobile();
for (const theme of getColorThemes()) {
children.push(
<IconButton
as="div"
key={theme.name}
onClick={() => userOptionsStore.selectTheme(theme.name)}
size="xs"
icon={
<Circle
size={!isMobile ? 16 : 20}
stroke="transparent"
style={{
background: `conic-gradient(${theme.colors.background.primary.startsWith("#") ? theme.colors.background.primary : theme.colors.background.secondary} 0 50%, ${theme.colors.text.secondary} 50% 100%)`,
borderRadius: "50%",
boxShadow: "0 0 0.5px 0.25px #fff",
cursor: "pointer",
transition: "background 0.2s",
}}
/>
}
bg="transparent"
borderRadius="50%" // Ensures the button has a circular shape
stroke="transparent"
color="transparent"
_hover={{
svg: {
transition: "stroke 0.3s ease-in-out", // Smooth transition effect
},
}}
zIndex={toolbarButtonZIndex}
/>,
);
}
return (
<VStack align={!isMobile ? "end" : "start"} p={1.2}>
<Center>{children}</Center>
</VStack>
);
}

View File

@@ -0,0 +1,84 @@
import { motion } from "framer-motion";
import { Box, Center, VStack } from "@chakra-ui/react";
import {
welcome_home_text,
welcome_home_tip,
} from "../static-data/welcome_home_text";
import CustomMarkdownRenderer, {
WelcomeHomeMarkdownRenderer,
} from "./chat/CustomMarkdownRenderer";
function WelcomeHomeMessage({ visible }) {
const containerVariants = {
visible: {
transition: {
staggerChildren: 0.15,
},
},
hidden: {
transition: {
staggerChildren: 0.05,
staggerDirection: -1,
},
},
};
const textVariants = {
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: [0.165, 0.84, 0.44, 1],
},
},
hidden: {
opacity: 0,
y: 20,
transition: {
duration: 0.3,
ease: [0.165, 0.84, 0.44, 1],
},
},
};
return (
<Center>
<VStack spacing={8} align="center" maxW="400px">
{/* Welcome Message */}
<Box
fontSize="sm"
fontStyle="italic"
textAlign="center"
color="text.secondary"
mt={4}
>
<motion.div
variants={containerVariants}
initial="hidden"
animate={visible ? "visible" : "hidden"}
>
<Box userSelect={"none"}>
<motion.div variants={textVariants}>
<WelcomeHomeMarkdownRenderer markdown={welcome_home_text} />
</motion.div>
</Box>
</motion.div>
</Box>
<motion.div variants={textVariants}>
<Box
fontSize="sm"
fontStyle="italic"
textAlign="center"
color="text.secondary"
mt={1}
>
<CustomMarkdownRenderer markdown={welcome_home_tip} />
</Box>
</motion.div>
</VStack>
</Center>
);
}
export default WelcomeHomeMessage;

View File

@@ -0,0 +1,44 @@
import React from "react";
import { Grid, GridItem, Image, Text } from "@chakra-ui/react";
const fontSize = "md";
function AboutComponent() {
return (
<Grid
templateColumns="1fr"
gap={4}
maxW={["100%", "100%", "100%"]}
mx="auto"
className="about-container"
>
<GridItem colSpan={1} justifySelf="center" mb={[6, 6, 8]}>
<Image
src="/me.png"
alt="Geoff Seemueller"
borderRadius="full"
boxSize={["120px", "150px"]}
objectFit="cover"
/>
</GridItem>
<GridItem
colSpan={1}
maxW={["100%", "100%", "container.md"]}
justifySelf="center"
minH={"100%"}
>
<Grid templateColumns="1fr" gap={4} overflowY={"auto"}>
<GridItem>
<Text fontSize={fontSize}>
If you're interested in collaborating on innovative projects that
push technological boundaries and create real value, I'd be keen
to connect and explore potential opportunities.
</Text>
</GridItem>
</Grid>
</GridItem>
</Grid>
);
}
export default AboutComponent;

View File

@@ -0,0 +1,40 @@
import React from "react";
import { IconButton, Tag, TagCloseButton, TagLabel } from "@chakra-ui/react";
import { PaperclipIcon } from "lucide-react";
// Add a new component for UploadedItem
export const UploadedItem: React.FC<{
url: string;
onRemove: () => void;
name: string;
}> = ({ url, onRemove, name }) => (
<Tag size="md" borderRadius="full" variant="solid" colorScheme="teal">
<TagLabel>{name || url.split("/").pop()}</TagLabel>
<TagCloseButton onClick={onRemove} />
</Tag>
);
export const AttachmentButton: React.FC<{
onClick: () => void;
disabled: boolean;
}> = ({ onClick, disabled }) => (
<IconButton
aria-label="Attach"
title="Attach"
bg="transparent"
color="text.tertiary"
icon={<PaperclipIcon size={"1.3337rem"} />}
onClick={onClick}
_hover={{
bg: "transparent",
svg: {
stroke: "accent.secondary",
transition: "stroke 0.3s ease-in-out",
},
}}
variant="ghost"
size="sm"
isDisabled={disabled}
_focus={{ boxShadow: "none" }}
/>
);

View File

@@ -0,0 +1,75 @@
import React, { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Box, Grid, GridItem } from "@chakra-ui/react";
import ChatMessages from "./ChatMessages";
import ChatInput from "./ChatInput";
import chatStore from "../../stores/ClientChatStore";
import menuState from "../../stores/AppMenuStore";
import WelcomeHomeMessage from "../WelcomeHomeMessage";
const Chat = observer(({ height, width }) => {
const scrollRef = useRef();
const [isAndroid, setIsAndroid] = useState(false);
useEffect(() => {
if (typeof window !== "undefined") {
setIsAndroid(/android/i.test(window.navigator.userAgent));
}
}, []);
return (
<Grid
templateRows="1fr auto"
templateColumns="1fr"
height={height}
width={width}
gap={0}
>
<GridItem alignSelf="center" hidden={!(chatStore.messages.length < 1)}>
<WelcomeHomeMessage visible={chatStore.messages.length < 1} />
</GridItem>
<GridItem
overflow="auto"
width="100%"
maxH="100%"
ref={scrollRef}
// If there are attachments, use "100px". Otherwise, use "128px" on Android, "73px" elsewhere.
pb={
chatStore.attachments.length > 0
? "100px"
: isAndroid
? "128px"
: "73px"
}
alignSelf="flex-end"
>
<ChatMessages scrollRef={scrollRef} />
</GridItem>
<GridItem
position="relative"
bg="background.primary"
zIndex={1000}
width="100%"
>
<Box
w="100%"
display="flex"
justifyContent="center"
mx="auto"
hidden={menuState.isOpen}
>
<ChatInput
input={chatStore.input}
setInput={(value) => chatStore.setInput(value)}
handleSendMessage={chatStore.sendMessage}
isLoading={chatStore.isLoading}
/>
</Box>
</GridItem>
</Grid>
);
});
export default Chat;

View File

@@ -0,0 +1,157 @@
import React, { useEffect, useRef, useState } from "react";
import {
Box,
Button,
Grid,
GridItem,
useBreakpointValue,
} from "@chakra-ui/react";
import { observer } from "mobx-react-lite";
import chatStore from "../../stores/ClientChatStore";
import InputMenu from "./flyoutmenu/InputMenu";
import InputTextarea from "./ChatInputTextArea";
import SendButton from "./ChatInputSendButton";
import { useMaxWidth } from "../../layout/useMaxWidth";
import userOptionsStore from "../../stores/UserOptionsStore";
const ChatInput = observer(() => {
const inputRef = useRef<HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const maxWidth = useMaxWidth();
const [inputValue, setInputValue] = useState<string>("");
const [containerHeight, setContainerHeight] = useState(56);
const [containerBorderRadius, setContainerBorderRadius] = useState(9999);
const [shouldFollow, setShouldFollow] = useState<boolean>(
userOptionsStore.followModeEnabled,
);
const [couldFollow, setCouldFollow] = useState<boolean>(chatStore.isLoading);
const [inputWidth, setInputWidth] = useState<string>("50%");
useEffect(() => {
setShouldFollow(chatStore.isLoading && userOptionsStore.followModeEnabled);
setCouldFollow(chatStore.isLoading);
}, [chatStore.isLoading, userOptionsStore.followModeEnabled]);
useEffect(() => {
inputRef.current?.focus();
setInputValue(chatStore.input);
}, [chatStore.input]);
useEffect(() => {
if (containerRef.current) {
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
const newHeight = entry.target.clientHeight;
setContainerHeight(newHeight);
const newBorderRadius = Math.max(28 - (newHeight - 56) * 0.2, 16);
setContainerBorderRadius(newBorderRadius);
}
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
chatStore.sendMessage();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
chatStore.sendMessage();
}
};
const inputMaxWidth = useBreakpointValue(
{ base: "50rem", lg: "50rem", md: "80%", sm: "100vw" },
{ ssr: true },
);
const inputMinWidth = useBreakpointValue({ lg: "40rem" }, { ssr: true });
useEffect(() => {
setInputWidth("100%");
}, [inputMaxWidth, inputMinWidth]);
return (
<Box
width={inputWidth}
maxW={inputMaxWidth}
minWidth={inputMinWidth}
mx="auto"
p={2}
pl={2}
pb={`calc(env(safe-area-inset-bottom) + 16px)`}
bottom={0}
position="fixed"
zIndex={1000}
>
{couldFollow && (
<Box
position="absolute"
top={-8}
right={0}
zIndex={1001}
display="flex"
justifyContent="flex-end"
>
<Button
size="sm"
variant="ghost"
colorScheme="blue"
onClick={(_) => {
userOptionsStore.toggleFollowMode();
}}
isDisabled={!chatStore.isLoading}
>
{shouldFollow ? "Disable Follow Mode" : "Enable Follow Mode"}
</Button>
</Box>
)}
<Grid
ref={containerRef}
p={2}
bg="background.secondary"
borderRadius={`${containerBorderRadius}px`}
templateColumns="auto 1fr auto"
gap={2}
alignItems="center"
style={{
transition: "border-radius 0.2s ease",
}}
>
<GridItem>
<InputMenu
selectedModel={chatStore.model}
onSelectModel={chatStore.setModel}
isDisabled={chatStore.isLoading}
/>
</GridItem>
<GridItem>
<InputTextarea
inputRef={inputRef}
value={chatStore.input}
onChange={chatStore.setInput}
onKeyDown={handleKeyDown}
isLoading={chatStore.isLoading}
/>
</GridItem>
<GridItem>
<SendButton
isLoading={chatStore.isLoading}
isDisabled={chatStore.isLoading || !chatStore.input.trim()}
onClick={handleSubmit}
/>
</GridItem>
</Grid>
</Box>
);
});
export default ChatInput;

View File

@@ -0,0 +1,55 @@
import React from "react";
import { Button } from "@chakra-ui/react";
import clientChatStore from "../../stores/ClientChatStore";
import { CirclePause, Send } from "lucide-react";
import { motion } from "framer-motion";
interface SendButtonProps {
isLoading: boolean;
isDisabled: boolean;
onClick: (e: React.FormEvent) => void;
onStop?: () => void;
}
const SendButton: React.FC<SendButtonProps> = ({ onClick }) => {
const isDisabled =
clientChatStore.input.trim().length === 0 && !clientChatStore.isLoading;
return (
<Button
onClick={(e) =>
clientChatStore.isLoading
? clientChatStore.stopIncomingMessage()
: onClick(e)
}
bg="transparent"
color={
clientChatStore.input.trim().length <= 1 ? "brand.700" : "text.primary"
}
borderRadius="full"
p={2}
isDisabled={isDisabled}
_hover={{ bg: !isDisabled ? "rgba(255, 255, 255, 0.2)" : "inherit" }}
_active={{ bg: !isDisabled ? "rgba(255, 255, 255, 0.3)" : "inherit" }}
_focus={{ boxShadow: "none" }}
>
{clientChatStore.isLoading ? <MySpinner /> : <Send size={20} />}
</Button>
);
};
const MySpinner = ({ onClick }) => (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{
duration: 0.4,
ease: "easeInOut",
}}
>
<CirclePause color={"#F0F0F0"} size={24} onClick={onClick} />
</motion.div>
);
export default SendButton;

View File

@@ -0,0 +1,149 @@
import React, { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import {
Alert,
AlertIcon,
Box,
chakra,
HStack,
InputGroup,
} from "@chakra-ui/react";
import fileUploadStore from "../../stores/FileUploadStore";
import { UploadedItem } from "./Attachments";
import AutoResize from "react-textarea-autosize";
const AutoResizeTextArea = chakra(AutoResize);
interface InputTextAreaProps {
inputRef: React.RefObject<HTMLTextAreaElement>;
value: string;
onChange: (value: string) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
isLoading: boolean;
}
const InputTextArea: React.FC<InputTextAreaProps> = observer(
({ inputRef, value, onChange, onKeyDown, isLoading }) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const handleAttachmentClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
fileUploadStore.uploadFile(file, "/api/documents");
}
};
const handleRemoveUploadedItem = (url: string) => {
fileUploadStore.removeUploadedFile(url);
};
const [heightConstraint, setHeightConstraint] = useState<
number | undefined
>(10);
useEffect(() => {
if (value.length > 10) {
setHeightConstraint();
}
}, [value]);
return (
<Box
position="relative"
width="100%"
height={heightConstraint}
display="flex"
flexDirection="column"
>
{/* Attachments Section */}
{fileUploadStore.uploadResults.length > 0 && (
<HStack
spacing={2}
mb={2}
overflowX="auto"
css={{ "&::-webkit-scrollbar": { display: "none" } }}
// Ensure attachments wrap if needed
flexWrap="wrap"
>
{fileUploadStore.uploadResults.map((result) => (
<UploadedItem
key={result.url}
url={result.url}
name={result.name}
onRemove={() => handleRemoveUploadedItem(result.url)}
/>
))}
</HStack>
)}
{/* Input Area */}
<InputGroup position="relative">
<AutoResizeTextArea
fontFamily="Arial, sans-serif"
ref={inputRef}
value={value}
height={heightConstraint}
autoFocus
onChange={(e) => onChange(e.target.value)}
onKeyDown={onKeyDown}
p={2}
pr="8px"
pl="17px"
bg="rgba(255, 255, 255, 0.15)"
color="text.primary"
borderRadius="20px" // Set a consistent border radius
border="none"
placeholder="Free my mind..."
_placeholder={{ color: "gray.400" }}
_focus={{
outline: "none",
}}
disabled={isLoading}
minRows={1}
maxRows={12}
style={{
touchAction: "none",
resize: "none",
overflowY: "auto",
width: "100%",
transition: "height 0.2s ease-in-out",
}}
/>
{/*<InputRightElement*/}
{/* position="absolute"*/}
{/* right={0}*/}
{/* top={0}*/}
{/* bottom={0}*/}
{/* width="40px"*/}
{/* height="100%"*/}
{/* display="flex"*/}
{/* alignItems="center"*/}
{/* justifyContent="center"*/}
{/*>*/}
{/*<EnableSearchButton />*/}
{/*</InputRightElement>*/}
</InputGroup>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
{fileUploadStore.uploadError && (
<Alert status="error" mt={2}>
<AlertIcon />
{fileUploadStore.uploadError}
</Alert>
)}
</Box>
);
},
);
export default InputTextArea;

View File

@@ -0,0 +1,9 @@
import React from "react";
import CustomMarkdownRenderer from "./CustomMarkdownRenderer";
const ChatMessageContent = ({ content }) => {
return <CustomMarkdownRenderer markdown={content} />;
};
export default React.memo(ChatMessageContent);

View File

@@ -0,0 +1,56 @@
import React, { useEffect } from "react";
import { Box, Grid, GridItem } from "@chakra-ui/react";
import MessageBubble from "./MessageBubble";
import { observer } from "mobx-react-lite";
import chatStore from "../../stores/ClientChatStore";
import { useIsMobile } from "../contexts/MobileContext";
interface ChatMessagesProps {
scrollRef: React.RefObject<HTMLDivElement>;
}
const ChatMessages: React.FC<ChatMessagesProps> = observer(({ scrollRef }) => {
const isMobile = useIsMobile();
useEffect(() => {
(async () => {
await import("./math.css");
})();
}, []);
return (
<Box
pt={isMobile ? 24 : undefined}
overflowY={"scroll"}
overflowX={"hidden"}
>
<Grid
fontFamily="Arial, sans-serif"
templateColumns="1fr"
gap={2}
bg="transparent"
borderRadius="md"
boxShadow="md"
whiteSpace="pre-wrap"
>
{chatStore.messages.map((msg, index) => {
if (index < chatStore.messages.length - 1) {
return (
<GridItem key={index}>
<MessageBubble x scrollRef={scrollRef} msg={msg} />
</GridItem>
);
} else {
return (
<GridItem key={index} mb={isMobile ? 4 : undefined}>
<MessageBubble scrollRef={scrollRef} msg={msg} />
</GridItem>
);
}
})}
</Grid>
</Box>
);
});
export default ChatMessages;

View File

@@ -0,0 +1,22 @@
import React from "react";
import { renderCustomComponents } from "./renderCustomComponents";
import { renderCustomComponents as renderWelcomeHomeMarkdown } from "./RenderWelcomeHomeCustomComponents";
import { Box } from "@chakra-ui/react";
interface CustomMarkdownRendererProps {
markdown: string;
}
const CustomMarkdownRenderer: React.FC<CustomMarkdownRendererProps> = ({
markdown,
}) => {
return <div>{renderCustomComponents(markdown)}</div>;
};
export const WelcomeHomeMarkdownRenderer: React.FC<
CustomMarkdownRendererProps
> = ({ markdown }) => {
return <Box color="text.accent">{renderWelcomeHomeMarkdown(markdown)}</Box>;
};
export default CustomMarkdownRenderer;

View File

@@ -0,0 +1,49 @@
import React from "react";
import { IconButton } from "@chakra-ui/react";
import { Globe2Icon } from "lucide-react";
import clientChatStore from "../../stores/ClientChatStore";
import { observer } from "mobx-react-lite";
export const EnableSearchButton: React.FC<{ disabled: boolean }> = observer(
({ disabled }) => {
const onClick = () => {
if (clientChatStore.tools.includes("web-search")) {
clientChatStore.setTools([]);
} else {
clientChatStore.setTools(["web-search"]);
}
};
const isActive = clientChatStore.tools.includes("web-search");
return (
<IconButton
aria-label={isActive ? "Disable Search" : "Enable Search"}
title={isActive ? "Disable Search" : "Enable Search"}
bg="transparent"
color="text.tertiary"
icon={<Globe2Icon size={"1.3337rem"} />}
onClick={onClick}
isActive={isActive}
_active={{
bg: "transparent",
svg: {
stroke: "brand.100",
transition: "stroke 0.3s ease-in-out",
},
}}
_hover={{
bg: "transparent",
svg: {
stroke: "accent.secondary",
transition: "stroke 0.3s ease-in-out",
},
}}
variant="ghost"
size="sm"
isDisabled={disabled}
_focus={{ boxShadow: "none" }}
/>
);
},
);

View File

@@ -0,0 +1,135 @@
import React, { useRef } from "react";
import { observer } from "mobx-react-lite";
import {
Box,
Divider,
HStack,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Text,
useDisclosure,
useOutsideClick,
} from "@chakra-ui/react";
import { ChevronRight } from "lucide-react";
import { useIsMobile } from "../contexts/MobileContext";
const FlyoutSubMenu: React.FC<{
title: string;
flyoutMenuOptions: { name: string; value: string }[];
onClose: () => void;
handleSelect: (item) => Promise<void>;
isSelected?: (item) => boolean;
parentIsOpen: boolean;
}> = observer(
({
title,
flyoutMenuOptions,
onClose,
handleSelect,
isSelected,
parentIsOpen,
}) => {
const { isOpen, onOpen, onClose: onSubMenuClose } = useDisclosure();
const isMobile = useIsMobile();
const menuRef = new useRef();
useOutsideClick({
ref: menuRef,
enabled: !isMobile,
handler: () => {
onSubMenuClose();
},
});
return (
<Menu
placement="right-start"
isOpen={isOpen && parentIsOpen}
closeOnBlur={true}
onClose={() => {
onSubMenuClose();
}}
closeOnSelect={false}
>
<MenuButton
as={MenuItem}
onClick={onOpen}
ref={menuRef}
bg="background.tertiary"
color="text.primary"
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
>
<HStack width={"100%"} justifyContent={"space-between"}>
<Text>{title}</Text>
<ChevronRight size={"1rem"} />
</HStack>
</MenuButton>
<Portal>
<MenuList
key={title}
maxHeight={56}
overflowY="scroll"
visibility={"visible"}
minWidth="180px"
bg="background.tertiary"
boxShadow="lg"
transform="translateY(-50%)"
zIndex={9999}
position="absolute"
left="100%"
bottom={-10}
sx={{
"::-webkit-scrollbar": {
width: "8px",
},
"::-webkit-scrollbar-thumb": {
background: "background.primary",
borderRadius: "4px",
},
"::-webkit-scrollbar-track": {
background: "background.tertiary",
},
}}
>
{flyoutMenuOptions.map((item, index) => (
<Box key={"itemflybox" + index}>
<MenuItem
key={"itemfly" + index}
onClick={() => {
handleSelect(item);
onSubMenuClose();
onClose();
}}
bg={
isSelected(item)
? "background.secondary"
: "background.tertiary"
}
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
>
{item.name}
</MenuItem>
{index < flyoutMenuOptions.length - 1 && (
<Divider
key={item.name + "-divider"}
color="text.tertiary"
w={"100%"}
/>
)}
</Box>
))}
</MenuList>
</Portal>
</Menu>
);
},
);
export default FlyoutSubMenu;

View File

@@ -0,0 +1,88 @@
import React, { useState, useEffect } from "react";
import { Image, Box, Spinner, Text, Flex } from "@chakra-ui/react";
import { keyframes } from "@emotion/react";
const shimmer = keyframes`
0% { background-position: -100% 0; }
100% { background-position: 100% 0; }
`;
const ImageWithFallback = ({
alt,
src,
fallbackSrc = "/fallback.png",
...props
}) => {
const [isLoading, setIsLoading] = useState(true);
const [scrollPosition, setScrollPosition] = useState(0);
const isSlowLoadingSource = src.includes("text2image.seemueller.io");
const handleImageLoad = () => setIsLoading(false);
const handleImageError = () => {
setIsLoading(false);
props.onError?.();
};
useEffect(() => {
setIsLoading(true);
}, [src]);
useEffect(() => {
const handleScroll = () => {
const scrolled = window.scrollY;
setScrollPosition(scrolled);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
const parallaxOffset = scrollPosition * 0.2;
return (
<Box
position="relative"
w="full"
maxW="full"
borderRadius="md"
my={2}
overflow="hidden"
>
{isLoading && isSlowLoadingSource && (
<Flex
align="center"
justify="center"
direction="column"
w="full"
h="300px"
borderRadius="md"
bg="background.secondary"
backgroundImage="linear-gradient(90deg, rgba(51,51,51,0.2) 25%, rgba(34,34,34,0.4) 50%, rgba(51,51,51,0.2) 75%)"
backgroundSize="200% 100%"
animation={`${shimmer} 1.5s infinite`}
>
<Spinner size="xl" color="blue.500" mb={4} />
<Text fontSize="lg" color="gray.600">
Generating...
</Text>
</Flex>
)}
<Image
src={src}
alt={alt}
fallbackSrc={fallbackSrc}
onLoad={handleImageLoad}
onError={handleImageError}
display={isLoading ? "none" : "block"}
transform={`translateY(${parallaxOffset}px)`}
transition="transform 0.1s ease-out"
{...props}
/>
</Box>
);
};
export default ImageWithFallback;

View File

@@ -0,0 +1,51 @@
import React from "react";
import { observer } from "mobx-react-lite";
import clientChatStore from "../../stores/ClientChatStore";
export const IntermediateStepsComponent = observer(({ hidden }) => {
return (
<div hidden={hidden}>
{clientChatStore.intermediateSteps.map((step, index) => {
switch (step.kind) {
case "web-search": {
return <WebSearchResult key={index} data={step.data} />;
}
case "tool-result":
return <ToolResult key={index} data={step.data} />;
default:
return <GenericStep key={index} data={step.data} />;
}
})}
</div>
);
});
const WebSearchResult = () => {
return (
<div>
{/*{webResults?.map(r => <Box>*/}
{/* <Text>{r.title}</Text>*/}
{/* <Text>{r.url}</Text>*/}
{/* <Text>{r.snippet}</Text>*/}
{/*</Box>)}*/}
</div>
);
};
export const ToolResult = ({ data }) => {
return (
<div className="tool-result">
<h3>Tool Result</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export const GenericStep = ({ data }) => {
return (
<div className="generic-step">
<h3>Generic Step</h3>
<p>{data.description || "No additional information provided."}</p>
</div>
);
};

View File

@@ -0,0 +1,160 @@
import React, { useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";
import { Box, Flex, Text } from "@chakra-ui/react";
import MessageRenderer from "./ChatMessageContent";
import { observer } from "mobx-react-lite";
import { IntermediateStepsComponent } from "./IntermediateStepsComponent";
import MessageEditor from "./MessageEditorComponent";
import UserMessageTools from "./UserMessageTools";
import clientChatStore from "../../stores/ClientChatStore";
import UserOptionsStore from "../../stores/UserOptionsStore";
const MotionBox = motion(Box);
const LoadingDots = () => (
<Flex>
{[0, 1, 2].map((i) => (
<MotionBox
key={i}
width="8px"
height="8px"
borderRadius="50%"
backgroundColor="text.primary"
margin="0 4px"
animate={{
scale: [1, 1.2, 1],
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 1,
repeat: Infinity,
delay: i * 0.2,
}}
/>
))}
</Flex>
);
function renderMessage(msg: any) {
if (msg.role === "user") {
return (
<Text as="p" fontSize="sm" lineHeight="short" color="text.primary">
{msg.content}
</Text>
);
}
return <MessageRenderer content={msg.content} />;
}
const MessageBubble = observer(({ msg, scrollRef }) => {
const [isEditing, setIsEditing] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const isUser = msg.role === "user";
const senderName = isUser ? "You" : "Geoff's AI";
const isLoading = !msg.content || !(msg.content.trim().length > 0);
const messageRef = useRef();
const handleEdit = () => {
setIsEditing(true);
};
const handleCancelEdit = () => {
setIsEditing(false);
};
useEffect(() => {
if (
clientChatStore.messages.length > 0 &&
clientChatStore.isLoading &&
UserOptionsStore.followModeEnabled
) {
console.log(
`${clientChatStore.messages.length}/${clientChatStore.isLoading}/${UserOptionsStore.followModeEnabled}`,
);
scrollRef.current?.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "auto",
});
}
});
return (
<Flex
flexDirection="column"
alignItems={isUser ? "flex-end" : "flex-start"}
role="listitem"
flex={0}
aria-label={`Message from ${senderName}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Text
fontSize="xs"
color="text.tertiary"
textAlign={isUser ? "right" : "left"}
alignSelf={isUser ? "flex-end" : "flex-start"}
mb={1}
>
{senderName}
</Text>
<MotionBox
minW={{ base: "99%", sm: "99%", lg: isUser ? "55%" : "60%" }}
maxW={{ base: "99%", sm: "99%", lg: isUser ? "65%" : "65%" }}
p={3}
borderRadius="1.5em"
bg={isUser ? "#0A84FF" : "#3A3A3C"}
color="text.primary"
textAlign="left"
boxShadow="0 2px 4px rgba(0, 0, 0, 0.1)"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
overflow="hidden"
wordBreak="break-word"
whiteSpace="pre-wrap"
>
<Flex justifyContent="space-between" alignItems="center">
<Box
flex="1"
overflowWrap="break-word"
whiteSpace="pre-wrap"
ref={messageRef}
sx={{
"pre, code": {
maxWidth: "100%",
whiteSpace: "pre-wrap",
overflowX: "auto",
},
}}
>
<IntermediateStepsComponent hidden={msg.role === "user"} />
{isEditing ? (
<MessageEditor message={msg} onCancel={handleCancelEdit} />
) : isLoading ? (
<LoadingDots />
) : (
renderMessage(msg)
)}
</Box>
{isUser && (
<Box
ml={2}
width="32px"
height="32px"
display="flex"
justifyContent="center"
alignItems="center"
>
{isHovered && !isEditing && (
<UserMessageTools message={msg} onEdit={handleEdit} />
)}
</Box>
)}
</Flex>
</MotionBox>
</Flex>
);
});
export default MessageBubble;

View File

@@ -0,0 +1,72 @@
import React, { KeyboardEvent, useState } from "react";
import { Box, Flex, IconButton, Textarea } from "@chakra-ui/react";
import { Check, X } from "lucide-react";
import { observer } from "mobx-react-lite";
import store, { type IMessage } from "../../stores/ClientChatStore";
interface MessageEditorProps {
message: IMessage;
onCancel: () => void;
}
const MessageEditor = observer(({ message, onCancel }: MessageEditorProps) => {
const [editedContent, setEditedContent] = useState(message.content);
const handleSave = () => {
const messageIndex = store.messages.indexOf(message);
if (messageIndex !== -1) {
store.editMessage(messageIndex, editedContent);
}
onCancel();
};
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
handleSave();
}
if (e.key === "Escape") {
e.preventDefault();
onCancel();
}
};
return (
<Box width="100%">
<Textarea
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
onKeyDown={handleKeyDown}
minHeight="100px"
bg="transparent"
border="1px solid"
borderColor="whiteAlpha.300"
_hover={{ borderColor: "whiteAlpha.400" }}
_focus={{ borderColor: "brand.100", boxShadow: "none" }}
resize="vertical"
color="text.primary"
/>
<Flex justify="flex-end" mt={2} gap={2}>
<IconButton
aria-label="Cancel edit"
icon={<X />}
onClick={onCancel}
size="sm"
variant="ghost"
color={"accent.danger"}
/>
<IconButton
aria-label="Save edit"
icon={<Check />}
onClick={handleSave}
size="sm"
variant="ghost"
color={"accent.confirm"}
/>
</Flex>
</Box>
);
});
export default MessageEditor;

View File

@@ -0,0 +1,156 @@
import React, { useCallback } from "react";
import {
Box,
Button,
Divider,
Flex,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { observer } from "mobx-react-lite";
import { ChevronDown, Copy, RefreshCcw, Settings } from "lucide-react";
import ClientChatStore from "../../stores/ClientChatStore";
import clientChatStore from "../../stores/ClientChatStore";
import FlyoutSubMenu from "./FlyoutSubMenu";
import { useIsMobile } from "../contexts/MobileContext";
import { getModelFamily, SUPPORTED_MODELS } from "./SupportedModels";
import { formatConversationMarkdown } from "./exportConversationAsMarkdown";
// Common styles for MenuButton and IconButton
export const MsM_commonButtonStyles = {
bg: "transparent",
color: "text.primary",
borderRadius: "full",
padding: 2,
border: "none",
_hover: { bg: "rgba(255, 255, 255, 0.2)" },
_active: { bg: "rgba(255, 255, 255, 0.3)" },
_focus: { boxShadow: "none" },
};
const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(
({ isDisabled }) => {
const isMobile = useIsMobile();
const { isOpen, onOpen, onClose } = useDisclosure();
const textModels = SUPPORTED_MODELS;
const handleCopyConversation = useCallback(() => {
navigator.clipboard
.writeText(formatConversationMarkdown(ClientChatStore.messages))
.then(() => {
window.alert(
"Conversation copied to clipboard. \n\nPaste it somewhere safe!",
);
onClose();
})
.catch((err) => {
console.error("Could not copy text to clipboard: ", err);
window.alert("Failed to copy conversation. Please try again.");
});
}, [onClose]);
async function selectModelFn({ name, value }) {
if (getModelFamily(value)) {
ClientChatStore.setModel(value);
}
}
function isSelectedModelFn({ name, value }) {
return ClientChatStore.model === value;
}
return (
<Menu
isOpen={isOpen}
onClose={onClose}
onOpen={onOpen}
closeOnSelect={false}
closeOnBlur={true}
>
{isMobile ? (
<MenuButton
as={IconButton}
bg="text.accent"
icon={<Settings size={20} />}
isDisabled={isDisabled}
aria-label="Settings"
_hover={{ bg: "rgba(255, 255, 255, 0.2)" }}
_focus={{ boxShadow: "none" }}
{...MsM_commonButtonStyles}
/>
) : (
<MenuButton
as={Button}
rightIcon={<ChevronDown size={16} />}
isDisabled={isDisabled}
variant="ghost"
display="flex"
justifyContent="space-between"
alignItems="center"
minW="auto"
{...MsM_commonButtonStyles}
>
<Text noOfLines={1} maxW="100px" fontSize="sm">
{ClientChatStore.model}
</Text>
</MenuButton>
)}
<MenuList
bg="background.tertiary"
border="none"
borderRadius="md"
boxShadow="lg"
minW={"10rem"}
>
<Divider color="text.tertiary" />
<FlyoutSubMenu
title="Text Models"
flyoutMenuOptions={textModels.map((m) => ({ name: m, value: m }))}
onClose={onClose}
parentIsOpen={isOpen}
handleSelect={selectModelFn}
isSelected={isSelectedModelFn}
/>
<Divider color="text.tertiary" />
{/*Export conversation button*/}
<MenuItem
bg="background.tertiary"
color="text.primary"
onClick={handleCopyConversation}
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
>
<Flex align="center">
<Copy size="16px" style={{ marginRight: "8px" }} />
<Box>Export</Box>
</Flex>
</MenuItem>
{/*New conversation button*/}
<MenuItem
bg="background.tertiary"
color="text.primary"
onClick={() => {
onClose();
clientChatStore.reset();
}}
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
>
<Flex align="center">
<RefreshCcw size="16px" style={{ marginRight: "8px" }} />
<Box>New</Box>
</Flex>
</MenuItem>
</MenuList>
</Menu>
);
},
);
export default InputMenu;

View File

@@ -0,0 +1,576 @@
import React from "react";
import {
Box,
Code,
Divider,
Heading,
Link,
List,
ListItem,
OrderedList,
Table,
Tbody,
Td,
Text,
Th,
Thead,
Tr,
useColorModeValue,
} from "@chakra-ui/react";
import { marked } from "marked";
import CodeBlock from "../code/CodeBlock";
import ImageWithFallback from "./ImageWithFallback";
import markedKatex from "marked-katex-extension";
import katex from "katex";
try {
if (localStorage) {
marked.use(
markedKatex({
nonStandard: false,
displayMode: true,
throwOnError: false,
strict: true,
colorIsTextColor: true,
errorColor: "red",
}),
);
}
} catch (_) {}
const MemoizedCodeBlock = React.memo(CodeBlock);
/**
* Utility to map heading depth to Chakra heading styles that
* roughly match typical markdown usage.
*/
const getHeadingProps = (depth: number) => {
switch (depth) {
case 1:
return { as: "h1", size: "xl", mt: 4, mb: 2 };
case 2:
return { as: "h2", size: "lg", mt: 3, mb: 2 };
case 3:
return { as: "h3", size: "md", mt: 2, mb: 1 };
case 4:
return { as: "h4", size: "sm", mt: 2, mb: 1 };
case 5:
return { as: "h5", size: "sm", mt: 2, mb: 1 };
case 6:
return { as: "h6", size: "xs", mt: 2, mb: 1 };
default:
return { as: `h${depth}`, size: "md", mt: 2, mb: 1 };
}
};
interface TableToken extends marked.Tokens.Table {
align: Array<"center" | "left" | "right" | null>;
header: (string | marked.Tokens.TableCell)[];
rows: (string | marked.Tokens.TableCell)[][];
}
const CustomHeading: React.FC<{ text: string; depth: number }> = ({
text,
depth,
}) => {
const headingProps = getHeadingProps(depth);
return (
<Heading {...headingProps} wordBreak="break-word" maxWidth="100%">
{text}
</Heading>
);
};
const CustomParagraph: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return (
<Text
as="p"
fontSize="sm"
color="text.accent"
lineHeight="short"
wordBreak="break-word"
maxWidth="100%"
>
{children}
</Text>
);
};
const CustomBlockquote: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return (
<Box
as="blockquote"
borderLeft="4px solid"
borderColor="gray.200"
fontStyle="italic"
color="gray.600"
pl={4}
maxWidth="100%"
wordBreak="break-word"
mb={2}
>
{children}
</Box>
);
};
const CustomCodeBlock: React.FC<{ code: string; language?: string }> = ({
code,
language,
}) => {
return (
<MemoizedCodeBlock
language={language}
code={code}
onRenderComplete={() => Promise.resolve()}
/>
);
};
const CustomHr: React.FC = () => <Divider my={4} />;
const CustomList: React.FC<{
ordered?: boolean;
start?: number;
children: React.ReactNode;
}> = ({ ordered, start, children }) => {
const commonStyles = {
fontSize: "sm",
wordBreak: "break-word" as const,
maxWidth: "100%" as const,
stylePosition: "outside" as const,
mb: 2,
pl: 4,
};
return ordered ? (
<OrderedList start={start} {...commonStyles}>
{children}
</OrderedList>
) : (
<List styleType="disc" {...commonStyles}>
{children}
</List>
);
};
const CustomListItem: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
return <ListItem mb={1}>{children}</ListItem>;
};
const CustomKatex: React.FC<{ math: string; displayMode: boolean }> = ({
math,
displayMode,
}) => {
const renderedMath = katex.renderToString(math, { displayMode });
return (
<Box
as="span"
display={displayMode ? "block" : "inline"}
p={displayMode ? 4 : 1}
my={displayMode ? 4 : 0}
borderRadius="md"
overflow="auto"
maxWidth="100%"
dangerouslySetInnerHTML={{ __html: renderedMath }}
/>
);
};
const CustomTable: React.FC<{
header: React.ReactNode[];
align: Array<"center" | "left" | "right" | null>;
rows: React.ReactNode[][];
}> = ({ header, align, rows }) => {
return (
<Table
variant="simple"
size="sm"
my={4}
borderRadius="md"
overflow="hidden"
>
<Thead bg="background.secondary">
<Tr>
{header.map((cell, i) => (
<Th
key={i}
textAlign={align[i] || "left"}
fontWeight="bold"
p={2}
minW={16}
wordBreak="break-word"
>
{cell}
</Th>
))}
</Tr>
</Thead>
<Tbody>
{rows.map((row, rIndex) => (
<Tr key={rIndex}>
{row.map((cell, cIndex) => (
<Td
key={cIndex}
textAlign={align[cIndex] || "left"}
p={2}
wordBreak="break-word"
>
{cell}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
);
};
const CustomHtmlBlock: React.FC<{ content: string }> = ({ content }) => {
return <Box dangerouslySetInnerHTML={{ __html: content }} mb={2} />;
};
const CustomText: React.FC<{ text: React.ReactNode }> = ({ text }) => {
return (
<Text
fontSize="sm"
lineHeight="short"
wordBreak="break-word"
maxWidth="100%"
as="span"
>
{text}
</Text>
);
};
interface CustomStrongProps {
children: React.ReactNode;
}
const CustomStrong: React.FC<CustomStrongProps> = ({ children }) => {
return <Text as="strong">{children}</Text>;
};
const CustomEm: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<Text
as="em"
fontStyle="italic"
lineHeight="short"
wordBreak="break-word"
display="inline"
>
{children}
</Text>
);
};
const CustomDel: React.FC<{ text: string }> = ({ text }) => {
return (
<Text
as="del"
textDecoration="line-through"
lineHeight="short"
wordBreak="break-word"
display="inline"
>
{text}
</Text>
);
};
const CustomCodeSpan: React.FC<{ code: string }> = ({ code }) => {
const bg = useColorModeValue("gray.100", "gray.800");
return (
<Code
fontSize="sm"
bg={bg}
overflowX="clip"
borderRadius="md"
wordBreak="break-word"
maxWidth="100%"
p={0.5}
>
{code}
</Code>
);
};
const CustomMath: React.FC<{ math: string; displayMode?: boolean }> = ({
math,
displayMode = false,
}) => {
return (
<Box
as="span"
display={displayMode ? "block" : "inline"}
p={displayMode ? 4 : 1}
my={displayMode ? 4 : 0}
borderRadius="md"
overflow="auto"
maxWidth="100%"
className={`math ${displayMode ? "math-display" : "math-inline"}`}
>
{math}
</Box>
);
};
const CustomLink: React.FC<{
href: string;
title?: string;
children: React.ReactNode;
}> = ({ href, title, children, ...props }) => {
return (
<Link
href={href}
title={title}
isExternal
sx={{
"& span": {
color: "text.link",
},
}}
maxWidth="100%"
color="teal.500"
wordBreak="break-word"
{...props}
>
{children}
</Link>
);
};
const CustomImage: React.FC<{ href: string; text: string; title?: string }> = ({
href,
text,
title,
}) => {
return (
<ImageWithFallback
src={href}
alt={text}
title={title}
maxW="100%"
width="auto"
height="auto"
my={2}
/>
);
};
/**
* A helper function that iterates through a list of Marked tokens
* and returns an array of React elements. This is the heart of the
* custom-rendering logic, used both top-level and for nested tokens.
*/
function parseTokens(tokens: marked.Token[]): JSX.Element[] {
const output: JSX.Element[] = [];
let blockquoteContent: JSX.Element[] = [];
tokens.forEach((token, i) => {
switch (token.type) {
case "heading":
output.push(
<CustomHeading key={i} text={token.text} depth={token.depth} />,
);
break;
case "paragraph": {
const parsedContent = token.tokens
? parseTokens(token.tokens)
: token.text;
if (blockquoteContent.length > 0) {
blockquoteContent.push(
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
);
} else {
output.push(
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
);
}
break;
}
case "br":
output.push(<br key={i} />);
break;
case "escape": {
break;
}
case "blockquote_start":
blockquoteContent = [];
break;
case "blockquote_end":
output.push(
<CustomBlockquote key={i}>
{parseTokens(blockquoteContent)}
</CustomBlockquote>,
);
blockquoteContent = [];
break;
case "blockquote": {
output.push(
<CustomBlockquote key={i}>
{token.tokens ? parseTokens(token.tokens) : null}
</CustomBlockquote>,
);
break;
}
case "math":
output.push(
<CustomMath key={i} math={(token as any).value} displayMode={true} />,
);
break;
case "inlineMath":
output.push(
<CustomMath
key={i}
math={(token as any).value}
displayMode={false}
/>,
);
break;
case "inlineKatex":
case "blockKatex": {
const katexToken = token as any;
output.push(
<CustomKatex
key={i}
math={katexToken.text}
displayMode={katexToken.displayMode}
/>,
);
break;
}
case "code":
output.push(
<CustomCodeBlock key={i} code={token.text} language={token.lang} />,
);
break;
case "hr":
output.push(<CustomHr key={i} />);
break;
case "list": {
const { ordered, start, items } = token;
const listItems = items.map((listItem, idx) => {
const nestedContent = parseTokens(listItem.tokens);
return <CustomListItem key={idx}>{nestedContent}</CustomListItem>;
});
output.push(
<CustomList key={i} ordered={ordered} start={start}>
{listItems}
</CustomList>,
);
break;
}
case "table": {
const tableToken = token as TableToken;
output.push(
<CustomTable
key={i}
header={tableToken.header.map((cell) =>
typeof cell === "string" ? cell : parseTokens(cell.tokens || []),
)}
align={tableToken.align}
rows={tableToken.rows.map((row) =>
row.map((cell) =>
typeof cell === "string"
? cell
: parseTokens(cell.tokens || []),
),
)}
/>,
);
break;
}
case "html":
output.push(<CustomHtmlBlock key={i} content={token.text} />);
break;
case "def":
case "space":
break;
case "strong":
output.push(
<CustomStrong key={i}>
{parseTokens(token.tokens || [])}
</CustomStrong>,
);
break;
case "em":
output.push(
<CustomEm key={i}>
{token.tokens ? parseTokens(token.tokens) : token.text}
</CustomEm>,
);
break;
case "codespan":
output.push(<CustomCodeSpan key={i} code={token.text} />);
break;
case "link":
output.push(
<CustomLink key={i} href={token.href} title={token.title}>
{token.tokens ? parseTokens(token.tokens) : token.text}
</CustomLink>,
);
break;
case "image":
output.push(
<CustomImage
key={i}
href={token.href}
title={token.title}
text={token.text}
/>,
);
break;
case "text": {
const parsedContent = token.tokens
? parseTokens(token.tokens)
: token.text;
if (blockquoteContent.length > 0) {
blockquoteContent.push(
<React.Fragment key={i}>{parsedContent}</React.Fragment>,
);
} else {
output.push(<CustomText key={i} text={parsedContent} />);
}
break;
}
default:
console.warn("Unhandled token type:", token.type, token);
}
});
return output;
}
export function renderCustomComponents(markdown: string): JSX.Element[] {
marked.setOptions({
breaks: true,
gfm: true,
silent: false,
async: true,
});
const tokens = marked.lexer(markdown);
return parseTokens(tokens);
}

View File

@@ -0,0 +1,574 @@
import React from "react";
import {
Box,
Code,
Divider,
Heading,
Link,
List,
ListItem,
OrderedList,
Table,
Tbody,
Td,
Text,
Th,
Thead,
Tr,
useColorModeValue,
} from "@chakra-ui/react";
import { marked } from "marked";
import CodeBlock from "../code/CodeBlock";
import ImageWithFallback from "./ImageWithFallback";
import markedKatex from "marked-katex-extension";
import katex from "katex";
try {
if (localStorage) {
marked.use(
markedKatex({
nonStandard: false,
displayMode: true,
throwOnError: false,
strict: true,
colorIsTextColor: true,
errorColor: "red",
}),
);
}
} catch (_) {}
const MemoizedCodeBlock = React.memo(CodeBlock);
const getHeadingProps = (depth: number) => {
switch (depth) {
case 1:
return { as: "h1", size: "xl", mt: 4, mb: 2 };
case 2:
return { as: "h2", size: "lg", mt: 3, mb: 2 };
case 3:
return { as: "h3", size: "md", mt: 2, mb: 1 };
case 4:
return { as: "h4", size: "sm", mt: 2, mb: 1 };
case 5:
return { as: "h5", size: "sm", mt: 2, mb: 1 };
case 6:
return { as: "h6", size: "xs", mt: 2, mb: 1 };
default:
return { as: `h${depth}`, size: "md", mt: 2, mb: 1 };
}
};
interface TableToken extends marked.Tokens.Table {
align: Array<"center" | "left" | "right" | null>;
header: (string | marked.Tokens.TableCell)[];
rows: (string | marked.Tokens.TableCell)[][];
}
const CustomHeading: React.FC<{ text: string; depth: number }> = ({
text,
depth,
}) => {
const headingProps = getHeadingProps(depth);
return (
<Heading
{...headingProps}
wordBreak="break-word"
maxWidth="100%"
color="text.accent"
>
{text}
</Heading>
);
};
const CustomParagraph: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return (
<Text
as="p"
fontSize="sm"
lineHeight="short"
wordBreak="break-word"
maxWidth="100%"
>
{children}
</Text>
);
};
const CustomBlockquote: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return (
<Box
as="blockquote"
borderLeft="4px solid"
borderColor="gray.200"
fontStyle="italic"
color="gray.600"
pl={4}
maxWidth="100%"
wordBreak="break-word"
mb={2}
>
{children}
</Box>
);
};
const CustomCodeBlock: React.FC<{ code: string; language?: string }> = ({
code,
language,
}) => {
return (
<MemoizedCodeBlock
language={language}
code={code}
onRenderComplete={() => Promise.resolve()}
/>
);
};
const CustomHr: React.FC = () => <Divider my={4} />;
const CustomList: React.FC<{
ordered?: boolean;
start?: number;
children: React.ReactNode;
}> = ({ ordered, start, children }) => {
const commonStyles = {
fontSize: "sm",
wordBreak: "break-word" as const,
maxWidth: "100%" as const,
stylePosition: "outside" as const,
mb: 2,
pl: 4,
};
return ordered ? (
<OrderedList start={start} {...commonStyles}>
{children}
</OrderedList>
) : (
<List styleType="disc" {...commonStyles}>
{children}
</List>
);
};
const CustomListItem: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
return <ListItem mb={1}>{children}</ListItem>;
};
const CustomKatex: React.FC<{ math: string; displayMode: boolean }> = ({
math,
displayMode,
}) => {
const renderedMath = katex.renderToString(math, { displayMode });
return (
<Box
as="span"
display={displayMode ? "block" : "inline"}
// bg={bg}
p={displayMode ? 4 : 1}
my={displayMode ? 4 : 0}
borderRadius="md"
overflow="auto"
maxWidth="100%"
dangerouslySetInnerHTML={{ __html: renderedMath }}
/>
);
};
const CustomTable: React.FC<{
header: React.ReactNode[];
align: Array<"center" | "left" | "right" | null>;
rows: React.ReactNode[][];
}> = ({ header, align, rows }) => {
return (
<Table
variant="simple"
size="sm"
my={4}
borderRadius="md"
overflow="hidden"
>
<Thead bg="background.secondary">
<Tr>
{header.map((cell, i) => (
<Th
key={i}
textAlign={align[i] || "left"}
fontWeight="bold"
p={2}
minW={16}
wordBreak="break-word"
>
{cell}
</Th>
))}
</Tr>
</Thead>
<Tbody>
{rows.map((row, rIndex) => (
<Tr key={rIndex}>
{row.map((cell, cIndex) => (
<Td
key={cIndex}
textAlign={align[cIndex] || "left"}
p={2}
wordBreak="break-word"
>
{cell}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
);
};
const CustomHtmlBlock: React.FC<{ content: string }> = ({ content }) => {
return <Box dangerouslySetInnerHTML={{ __html: content }} mb={2} />;
};
const CustomText: React.FC<{ text: React.ReactNode }> = ({ text }) => {
return (
<Text
fontSize="sm"
lineHeight="short"
color="text.accent"
wordBreak="break-word"
maxWidth="100%"
as="span"
>
{text}
</Text>
);
};
interface CustomStrongProps {
children: React.ReactNode;
}
const CustomStrong: React.FC<CustomStrongProps> = ({ children }) => {
return <Text as="strong">{children}</Text>;
};
const CustomEm: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<Text
as="em"
fontStyle="italic"
lineHeight="short"
wordBreak="break-word"
display="inline"
>
{children}
</Text>
);
};
const CustomDel: React.FC<{ text: string }> = ({ text }) => {
return (
<Text
as="del"
textDecoration="line-through"
lineHeight="short"
wordBreak="break-word"
display="inline"
>
{text}
</Text>
);
};
const CustomCodeSpan: React.FC<{ code: string }> = ({ code }) => {
const bg = useColorModeValue("gray.100", "gray.800");
return (
<Code
fontSize="sm"
bg={bg}
overflowX="clip"
borderRadius="md"
wordBreak="break-word"
maxWidth="100%"
p={0.5}
>
{code}
</Code>
);
};
const CustomMath: React.FC<{ math: string; displayMode?: boolean }> = ({
math,
displayMode = false,
}) => {
return (
<Box
as="span"
display={displayMode ? "block" : "inline"}
p={displayMode ? 4 : 1}
my={displayMode ? 4 : 0}
borderRadius="md"
overflow="auto"
maxWidth="100%"
className={`math ${displayMode ? "math-display" : "math-inline"}`}
>
{math}
</Box>
);
};
const CustomLink: React.FC<{
href: string;
title?: string;
children: React.ReactNode;
}> = ({ href, title, children, ...props }) => {
return (
<Link
href={href}
title={title}
isExternal
sx={{
"& span": {
color: "text.link",
},
}}
maxWidth="100%"
color="teal.500"
wordBreak="break-word"
{...props}
>
{children}
</Link>
);
};
const CustomImage: React.FC<{ href: string; text: string; title?: string }> = ({
href,
text,
title,
}) => {
return (
<ImageWithFallback
src={href}
alt={text}
title={title}
maxW="100%"
width="auto"
height="auto"
my={2}
/>
);
};
function parseTokens(tokens: marked.Token[]): JSX.Element[] {
const output: JSX.Element[] = [];
let blockquoteContent: JSX.Element[] = [];
tokens.forEach((token, i) => {
switch (token.type) {
case "heading":
output.push(
<CustomHeading key={i} text={token.text} depth={token.depth} />,
);
break;
case "paragraph": {
const parsedContent = token.tokens
? parseTokens(token.tokens)
: token.text;
if (blockquoteContent.length > 0) {
blockquoteContent.push(
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
);
} else {
output.push(
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
);
}
break;
}
case "br":
output.push(<br key={i} />);
break;
case "escape": {
break;
}
case "blockquote_start":
blockquoteContent = [];
break;
case "blockquote_end":
output.push(
<CustomBlockquote key={i}>
{parseTokens(blockquoteContent)}
</CustomBlockquote>,
);
blockquoteContent = [];
break;
case "blockquote": {
output.push(
<CustomBlockquote key={i}>
{token.tokens ? parseTokens(token.tokens) : null}
</CustomBlockquote>,
);
break;
}
case "math":
output.push(
<CustomMath key={i} math={(token as any).value} displayMode={true} />,
);
break;
case "inlineMath":
output.push(
<CustomMath
key={i}
math={(token as any).value}
displayMode={false}
/>,
);
break;
case "inlineKatex":
case "blockKatex": {
const katexToken = token as any;
output.push(
<CustomKatex
key={i}
math={katexToken.text}
displayMode={katexToken.displayMode}
/>,
);
break;
}
case "code":
output.push(
<CustomCodeBlock key={i} code={token.text} language={token.lang} />,
);
break;
case "hr":
output.push(<CustomHr key={i} />);
break;
case "list": {
const { ordered, start, items } = token;
const listItems = items.map((listItem, idx) => {
const nestedContent = parseTokens(listItem.tokens);
return <CustomListItem key={idx}>{nestedContent}</CustomListItem>;
});
output.push(
<CustomList key={i} ordered={ordered} start={start}>
{listItems}
</CustomList>,
);
break;
}
case "table": {
const tableToken = token as TableToken;
output.push(
<CustomTable
key={i}
header={tableToken.header.map((cell) =>
typeof cell === "string" ? cell : parseTokens(cell.tokens || []),
)}
align={tableToken.align}
rows={tableToken.rows.map((row) =>
row.map((cell) =>
typeof cell === "string"
? cell
: parseTokens(cell.tokens || []),
),
)}
/>,
);
break;
}
case "html":
output.push(<CustomHtmlBlock key={i} content={token.text} />);
break;
case "def":
case "space":
break;
case "strong":
output.push(
<CustomStrong key={i}>
{parseTokens(token.tokens || [])}
</CustomStrong>,
);
break;
case "em":
output.push(
<CustomEm key={i}>
{token.tokens ? parseTokens(token.tokens) : token.text}
</CustomEm>,
);
break;
case "codespan":
output.push(<CustomCodeSpan key={i} code={token.text} />);
break;
case "link":
output.push(
<CustomLink key={i} href={token.href} title={token.title}>
{token.tokens ? parseTokens(token.tokens) : token.text}
</CustomLink>,
);
break;
case "image":
output.push(
<CustomImage
key={i}
href={token.href}
title={token.title}
text={token.text}
/>,
);
break;
case "text": {
const parsedContent = token.tokens
? parseTokens(token.tokens)
: token.text;
if (blockquoteContent.length > 0) {
blockquoteContent.push(
<React.Fragment key={i}>{parsedContent}</React.Fragment>,
);
} else {
output.push(<CustomText key={i} text={parsedContent} />);
}
break;
}
default:
console.warn("Unhandled token type:", token.type, token);
}
});
return output;
}
export function renderCustomComponents(markdown: string): JSX.Element[] {
marked.setOptions({
breaks: true,
gfm: true,
silent: false,
async: true,
});
const tokens = marked.lexer(markdown);
return parseTokens(tokens);
}

View File

@@ -0,0 +1,88 @@
const SUPPORTED_MODELS_GROUPS = {
openai: [
// "o1-preview",
// "o1-mini",
// "gpt-4o",
// "gpt-3.5-turbo"
],
groq: [
// "mixtral-8x7b-32768",
// "deepseek-r1-distill-llama-70b",
"meta-llama/llama-4-scout-17b-16e-instruct",
"gemma2-9b-it",
"mistral-saba-24b",
// "qwen-2.5-32b",
"llama-3.3-70b-versatile",
// "llama-3.3-70b-versatile"
// "llama-3.1-70b-versatile",
// "llama-3.3-70b-versatile"
],
cerebras: ["llama-3.3-70b"],
claude: [
// "claude-3-5-sonnet-20241022",
// "claude-3-opus-20240229"
],
fireworks: [
// "llama-v3p1-405b-instruct",
// "llama-v3p1-70b-instruct",
// "llama-v3p2-90b-vision-instruct",
// "mixtral-8x22b-instruct",
// "mythomax-l2-13b",
// "yi-large"
],
google: [
// "gemini-2.0-flash-exp",
// "gemini-1.5-flash",
// "gemini-exp-1206",
// "gemini-1.5-pro"
],
xai: [
// "grok-beta",
// "grok-2",
// "grok-2-1212",
// "grok-2-latest",
// "grok-beta"
],
cloudflareAI: [
"llama-3.2-3b-instruct", // max_tokens
"llama-3-8b-instruct", // max_tokens
"llama-3.1-8b-instruct-fast", // max_tokens
"deepseek-math-7b-instruct",
"deepseek-coder-6.7b-instruct-awq",
"hermes-2-pro-mistral-7b",
"openhermes-2.5-mistral-7b-awq",
"mistral-7b-instruct-v0.2",
"neural-chat-7b-v3-1-awq",
"openchat-3.5-0106",
// "gemma-7b-it",
],
};
export type SupportedModel =
| keyof typeof SUPPORTED_MODELS_GROUPS
| (typeof SUPPORTED_MODELS_GROUPS)[keyof typeof SUPPORTED_MODELS_GROUPS][number];
export type ModelFamily = keyof typeof SUPPORTED_MODELS_GROUPS;
function getModelFamily(model: string): ModelFamily | undefined {
return Object.keys(SUPPORTED_MODELS_GROUPS)
.filter((family) => {
return SUPPORTED_MODELS_GROUPS[
family as keyof typeof SUPPORTED_MODELS_GROUPS
].includes(model.trim());
})
.at(0) as ModelFamily | undefined;
}
const SUPPORTED_MODELS = [
// ...SUPPORTED_MODELS_GROUPS.xai,
// ...SUPPORTED_MODELS_GROUPS.claude,
// ...SUPPORTED_MODELS_GROUPS.google,
...SUPPORTED_MODELS_GROUPS.groq,
...SUPPORTED_MODELS_GROUPS.fireworks,
// ...SUPPORTED_MODELS_GROUPS.openai,
...SUPPORTED_MODELS_GROUPS.cerebras,
...SUPPORTED_MODELS_GROUPS.cloudflareAI,
];
export { SUPPORTED_MODELS, SUPPORTED_MODELS_GROUPS, getModelFamily };

View File

@@ -0,0 +1,34 @@
import { observer } from "mobx-react-lite";
import { IconButton } from "@chakra-ui/react";
import { Edit2Icon } from "lucide-react";
const UserMessageTools = observer(({ disabled = false, message, onEdit }) => (
<IconButton
bg="transparent"
color="text.primary"
aria-label="Edit message"
title="Edit message"
icon={<Edit2Icon size={"1em"} />}
onClick={() => onEdit(message)}
_active={{
bg: "transparent",
svg: {
stroke: "brand.100",
transition: "stroke 0.3s ease-in-out",
},
}}
_hover={{
bg: "transparent",
svg: {
stroke: "accent.secondary",
transition: "stroke 0.3s ease-in-out",
},
}}
variant="ghost"
size="sm"
isDisabled={disabled}
_focus={{ boxShadow: "none" }}
/>
));
export default UserMessageTools;

View File

@@ -0,0 +1,33 @@
import DOMPurify from "isomorphic-dompurify";
function domPurify(dirty: string) {
return DOMPurify.sanitize(dirty, {
USE_PROFILES: { html: true },
ALLOWED_TAGS: [
"b",
"i",
"u",
"a",
"p",
"span",
"div",
"table",
"thead",
"tbody",
"tr",
"td",
"th",
"ul",
"ol",
"li",
"code",
"pre",
],
ALLOWED_ATTR: ["href", "src", "alt", "title", "class", "style"],
FORBID_TAGS: ["script", "iframe"],
KEEP_CONTENT: true,
SAFE_FOR_TEMPLATES: true,
});
}
export default domPurify;

View File

@@ -0,0 +1,18 @@
// Function to generate a Markdown representation of the current conversation
import { type IMessage } from "../../stores/ClientChatStore";
import { Instance } from "mobx-state-tree";
export function formatConversationMarkdown(
messages: Instance<typeof IMessage>[],
): string {
return messages
.map((message) => {
if (message.role === "user") {
return `**You**: ${message.content}`;
} else if (message.role === "assistant") {
return `**Geoff's AI**: ${message.content}`;
}
return "";
})
.join("\n\n");
}

View File

@@ -0,0 +1,127 @@
import React, { useRef } from "react";
import { observer } from "mobx-react-lite";
import {
Box,
Divider,
HStack,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { ChevronRight } from "lucide-react";
const FlyoutSubMenu: React.FC<{
title: string;
flyoutMenuOptions: { name: string; value: string }[];
onClose: () => void;
handleSelect: (item) => Promise<void>;
isSelected?: (item) => boolean;
parentIsOpen: boolean;
setMenuState?: (state) => void;
}> = observer(
({
title,
flyoutMenuOptions,
onClose,
handleSelect,
isSelected,
parentIsOpen,
setMenuState,
}) => {
const { isOpen, onOpen, onClose: onSubMenuClose } = useDisclosure();
const menuRef = new useRef();
return (
<Menu
placement="right-start"
isOpen={isOpen && parentIsOpen}
closeOnBlur={true}
lazyBehavior={"keepMounted"}
isLazy={true}
onClose={(e) => {
onSubMenuClose();
}}
closeOnSelect={false}
>
<MenuButton
as={MenuItem}
onClick={onOpen}
ref={menuRef}
bg="background.tertiary"
color="text.primary"
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
>
<HStack width={"100%"} justifyContent={"space-between"}>
<Text>{title}</Text>
<ChevronRight size={"1rem"} />
</HStack>
</MenuButton>
<Portal>
<MenuList
key={title}
maxHeight={56}
overflowY="scroll"
visibility={"visible"}
minWidth="180px"
bg="background.tertiary"
boxShadow="lg"
transform="translateY(-50%)"
zIndex={9999}
position="absolute"
left="100%"
bottom={-10}
sx={{
"::-webkit-scrollbar": {
width: "8px",
},
"::-webkit-scrollbar-thumb": {
background: "background.primary",
borderRadius: "4px",
},
"::-webkit-scrollbar-track": {
background: "background.tertiary",
},
}}
>
{flyoutMenuOptions.map((item, index) => (
<Box key={"itemflybox" + index}>
<MenuItem
key={"itemfly" + index}
onClick={() => {
onSubMenuClose();
onClose();
handleSelect(item);
}}
bg={
isSelected(item)
? "background.secondary"
: "background.tertiary"
}
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
>
{item.name}
</MenuItem>
{index < flyoutMenuOptions.length - 1 && (
<Divider
key={item.name + "-divider"}
color="text.tertiary"
w={"100%"}
/>
)}
</Box>
))}
</MenuList>
</Portal>
</Menu>
);
},
);
export default FlyoutSubMenu;

View File

@@ -0,0 +1,190 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
Box,
Button,
Divider,
Flex,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Text,
useDisclosure,
useOutsideClick,
} from "@chakra-ui/react";
import { observer } from "mobx-react-lite";
import { ChevronDown, Copy, RefreshCcw, Settings } from "lucide-react";
import ClientChatStore from "../../../stores/ClientChatStore";
import clientChatStore from "../../../stores/ClientChatStore";
import FlyoutSubMenu from "./FlyoutSubMenu";
import { useIsMobile } from "../../contexts/MobileContext";
import { useIsMobile as useIsMobileUserAgent } from "../../../layout/_IsMobileHook";
import { getModelFamily, SUPPORTED_MODELS } from "../SupportedModels";
import { formatConversationMarkdown } from "../exportConversationAsMarkdown";
export const MsM_commonButtonStyles = {
bg: "transparent",
color: "text.primary",
borderRadius: "full",
padding: 2,
border: "none",
_hover: { bg: "rgba(255, 255, 255, 0.2)" },
_active: { bg: "rgba(255, 255, 255, 0.3)" },
_focus: { boxShadow: "none" },
};
const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(
({ isDisabled }) => {
const isMobile = useIsMobile();
const isMobileUserAgent = useIsMobileUserAgent();
const {
isOpen,
onOpen,
onClose,
onToggle,
getDisclosureProps,
getButtonProps,
} = useDisclosure();
const [controlledOpen, setControlledOpen] = useState<boolean>(false);
useEffect(() => {
setControlledOpen(isOpen);
}, [isOpen]);
const textModels = SUPPORTED_MODELS;
const handleClose = useCallback(() => {
onClose();
}, [isOpen]);
const handleCopyConversation = useCallback(() => {
navigator.clipboard
.writeText(formatConversationMarkdown(ClientChatStore.messages))
.then(() => {
window.alert(
"Conversation copied to clipboard. \n\nPaste it somewhere safe!",
);
onClose();
})
.catch((err) => {
console.error("Could not copy text to clipboard: ", err);
window.alert("Failed to copy conversation. Please try again.");
});
}, [onClose]);
async function selectModelFn({ name, value }) {
if (getModelFamily(value)) {
ClientChatStore.setModel(value);
}
}
function isSelectedModelFn({ name, value }) {
return ClientChatStore.model === value;
}
const menuRef = useRef();
const [menuState, setMenuState] = useState();
useOutsideClick({
enabled: !isMobile && isOpen,
ref: menuRef,
handler: () => {
handleClose();
},
});
return (
<Menu
isOpen={controlledOpen}
onClose={onClose}
onOpen={onOpen}
autoSelect={false}
closeOnSelect={false}
closeOnBlur={isOpen && !isMobileUserAgent}
isLazy={true}
lazyBehavior={"unmount"}
>
{isMobile ? (
<MenuButton
as={IconButton}
bg="text.accent"
icon={<Settings size={20} />}
isDisabled={isDisabled}
aria-label="Settings"
_hover={{ bg: "rgba(255, 255, 255, 0.2)" }}
_focus={{ boxShadow: "none" }}
{...MsM_commonButtonStyles}
/>
) : (
<MenuButton
as={Button}
rightIcon={<ChevronDown size={16} />}
isDisabled={isDisabled}
variant="ghost"
display="flex"
justifyContent="space-between"
alignItems="center"
minW="auto"
{...MsM_commonButtonStyles}
>
<Text noOfLines={1} maxW="100px" fontSize="sm">
{ClientChatStore.model}
</Text>
</MenuButton>
)}
<MenuList
bg="background.tertiary"
border="none"
borderRadius="md"
boxShadow="lg"
minW={"10rem"}
ref={menuRef}
>
<FlyoutSubMenu
title="Text Models"
flyoutMenuOptions={textModels.map((m) => ({ name: m, value: m }))}
onClose={onClose}
parentIsOpen={isOpen}
setMenuState={setMenuState}
handleSelect={selectModelFn}
isSelected={isSelectedModelFn}
/>
<Divider color="text.tertiary" />
{/*Export conversation button*/}
<MenuItem
bg="background.tertiary"
color="text.primary"
onClick={handleCopyConversation}
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
>
<Flex align="center">
<Copy size="16px" style={{ marginRight: "8px" }} />
<Box>Export</Box>
</Flex>
</MenuItem>
{/*New conversation button*/}
<MenuItem
bg="background.tertiary"
color="text.primary"
onClick={() => {
clientChatStore.setActiveConversation("conversation:new");
onClose();
}}
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
>
<Flex align="center">
<RefreshCcw size="16px" style={{ marginRight: "8px" }} />
<Box>New</Box>
</Flex>
</MenuItem>
</MenuList>
</Menu>
);
},
);
export default InputMenu;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
import { visit } from "unist-util-visit";
export default function remarkImageGeneration() {
return (tree) => {
visit(tree, "code", (node, index, parent) => {
if (node.lang === "generation") {
try {
const data = JSON.parse(node.value);
parent.children[index] = {
type: "generation",
data: data,
};
} catch (error) {
console.error("Invalid JSON in image-generation block:", error);
}
}
});
};
}

View File

@@ -0,0 +1,69 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { buildCodeHighlighter } from "./CodeHighlighter";
interface CodeBlockProps {
language: string;
code: string;
onRenderComplete: () => void;
}
const highlighter = buildCodeHighlighter();
const CodeBlock: React.FC<CodeBlockProps> = ({
language,
code,
onRenderComplete,
}) => {
const [html, setHtml] = useState<string>("");
const [loading, setLoading] = useState<boolean>(true);
const highlightCode = useCallback(async () => {
try {
const highlighted = (await highlighter).codeToHtml(code, {
lang: language,
theme: "github-dark",
});
setHtml(highlighted);
} catch (error) {
console.error("Error highlighting code:", error);
setHtml(`<pre>${code}</pre>`);
} finally {
setLoading(false);
onRenderComplete();
}
}, [language, code, onRenderComplete]);
useEffect(() => {
highlightCode();
}, [highlightCode]);
if (loading) {
return (
<div
style={{
backgroundColor: "#24292e",
padding: "10px",
borderRadius: "1.5em",
}}
>
Loading code...
</div>
);
}
return (
<div
dangerouslySetInnerHTML={{ __html: html }}
style={{
transition: "none",
padding: 20,
backgroundColor: "#24292e",
overflowX: "auto",
borderRadius: ".37em",
fontSize: ".75rem",
}}
/>
);
};
export default React.memo(CodeBlock);

View File

@@ -0,0 +1,75 @@
import { createHighlighterCore } from "shiki";
export async function buildCodeHighlighter() {
const [
githubDark,
html,
javascript,
jsx,
typescript,
tsx,
go,
rust,
python,
java,
kotlin,
shell,
sql,
yaml,
toml,
markdown,
json,
xml,
zig,
wasm,
] = await Promise.all([
import("shiki/themes/github-dark.mjs"),
import("shiki/langs/html.mjs"),
import("shiki/langs/javascript.mjs"),
import("shiki/langs/jsx.mjs"),
import("shiki/langs/typescript.mjs"),
import("shiki/langs/tsx.mjs"),
import("shiki/langs/go.mjs"),
import("shiki/langs/rust.mjs"),
import("shiki/langs/python.mjs"),
import("shiki/langs/java.mjs"),
import("shiki/langs/kotlin.mjs"),
import("shiki/langs/shell.mjs"),
import("shiki/langs/sql.mjs"),
import("shiki/langs/yaml.mjs"),
import("shiki/langs/toml.mjs"),
import("shiki/langs/markdown.mjs"),
import("shiki/langs/json.mjs"),
import("shiki/langs/xml.mjs"),
import("shiki/langs/zig.mjs"),
import("shiki/wasm"),
]);
// Create the highlighter instance with the loaded themes and languages
const instance = await createHighlighterCore({
themes: [githubDark], // Set the Base_theme
langs: [
html,
javascript,
jsx,
typescript,
tsx,
go,
rust,
python,
java,
kotlin,
shell,
sql,
yaml,
toml,
markdown,
json,
xml,
zig,
],
loadWasm: wasm, // Ensure correct loading of WebAssembly
});
return instance;
}

View File

@@ -0,0 +1,169 @@
import React from "react";
import {
Alert,
AlertIcon,
Box,
Button,
HStack,
Input,
Link,
List,
ListItem,
} from "@chakra-ui/react";
import { MarkdownEditor } from "./MarkdownEditor";
import { Fragment, useState } from "react";
function ConnectComponent() {
const [formData, setFormData] = useState({
markdown: "",
email: "",
firstname: "",
lastname: "",
});
const [isSubmitted, setIsSubmitted] = useState(false);
const [isError, setIsError] = useState(false);
const [validationError, setValidationError] = useState("");
const handleChange = (field: string) => (value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setIsSubmitted(false);
setValidationError("");
};
const handleSubmitButton = async () => {
setValidationError("");
if (!formData.email || !formData.firstname || !formData.markdown) {
setValidationError("Please fill in all required fields.");
return;
}
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
if (response.ok) {
setIsSubmitted(true);
setIsError(false);
setFormData({
markdown: "",
email: "",
firstname: "",
lastname: "",
});
} else {
setIsError(true);
}
} catch (error) {
setIsError(true);
}
};
return (
<Fragment>
<List color="text.primary" mb={4}>
<ListItem>
Email:{" "}
<Link href="mailto:geoff@seemueller.io" color="teal.500">
geoff@seemueller.io
</Link>
</ListItem>
</List>
<Box w="100%">
<HStack spacing={4} mb={4}>
<Input
placeholder="First name *"
value={formData.firstname}
onChange={(e) => handleChange("firstname")(e.target.value)}
color="text.primary"
borderColor="text.primary"
/>
<Input
placeholder="Last name *"
value={formData.lastname}
onChange={(e) => handleChange("lastname")(e.target.value)}
color="text.primary"
borderColor="text.primary"
// bg="text.primary"
/>
</HStack>
<Input
placeholder="Email *"
value={formData.email}
onChange={(e) => handleChange("email")(e.target.value)}
mb={4}
borderColor="text.primary"
color="text.primary"
/>
<MarkdownEditor
onChange={handleChange("markdown")}
markdown={formData.markdown}
placeholder="Your Message..."
/>
</Box>
<Button
variant="outline"
// colorScheme="blackAlpha"
onClick={handleSubmitButton}
alignSelf="flex-end"
size="md"
mt={4}
mb={4}
float="right"
_hover={{
bg: "",
transform: "scale(1.05)",
}}
_active={{
bg: "gray.800",
transform: "scale(1)",
}}
>
SEND
</Button>
<Box mt={12}>
{isSubmitted && (
<Alert
status="success"
borderRadius="md"
color="text.primary"
bg="green.500"
>
<AlertIcon />
Message sent successfully!
</Alert>
)}
{isError && (
<Alert
status="error"
borderRadius="md"
color="text.primary"
bg="red.500"
>
<AlertIcon />
There was an error sending your message. Please try again.
</Alert>
)}
{validationError && (
<Alert
status="warning"
borderRadius="md"
color="background.primary"
bg="yellow.500"
>
<AlertIcon />
{validationError}
</Alert>
)}
</Box>
</Fragment>
);
}
export default ConnectComponent;

View File

@@ -0,0 +1,23 @@
import React from "react";
import { Box, Textarea } from "@chakra-ui/react";
export const MarkdownEditor = (props: {
placeholder: string;
markdown: string;
onChange: (p: any) => any;
}) => {
return (
<Box sx={styles}>
<link rel="stylesheet" href="/" media="print" onLoad="this.media='all'" />
<Textarea
value={props.markdown}
placeholder={props.placeholder}
onChange={(e) => props.onChange(e.target.value)}
width="100%"
minHeight="150px"
height="100%"
resize="none"
/>
</Box>
);
};

View File

@@ -0,0 +1,18 @@
import {
ChakraProvider,
cookieStorageManagerSSR,
localStorageManager,
} from "@chakra-ui/react";
export function Chakra({ cookies, children, theme }) {
const colorModeManager =
typeof cookies === "string"
? cookieStorageManagerSSR("color_state", cookies)
: localStorageManager;
return (
<ChakraProvider colorModeManager={colorModeManager} theme={theme}>
{children}
</ChakraProvider>
);
}

View File

@@ -0,0 +1,36 @@
import React, { createContext, useContext, useState, useEffect } from "react";
import { useMediaQuery } from "@chakra-ui/react";
// Create the context to provide mobile state
const MobileContext = createContext(false);
// Create a provider component to wrap your app
export const MobileProvider = ({ children }: { children: React.ReactNode }) => {
const [isMobile, setIsMobile] = useState(false);
const [isFallbackMobile] = useMediaQuery("(max-width: 768px)");
useEffect(() => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
const mobile =
/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
userAgent.toLowerCase(),
);
setIsMobile(mobile);
}, []);
// Provide the combined mobile state globally
const mobileState = isMobile || isFallbackMobile;
return (
<MobileContext.Provider value={mobileState}>
{children}
</MobileContext.Provider>
);
};
// Custom hook to use the mobile context in any component
export function useIsMobile() {
return useContext(MobileContext);
}
export default MobileContext;

View File

@@ -0,0 +1,52 @@
import React from "react";
import { Badge, Box, Flex, Heading, Image, Text } from "@chakra-ui/react";
function DemoCard({ icon, title, description, imageUrl, badge, onClick }) {
return (
<Box
bg="background.secondary"
borderRadius="md"
overflowY="hidden"
boxShadow="md"
transition="transform 0.2s"
_hover={{ transform: "scale(1.05)", cursor: "pointer" }}
color="text.primary"
onClick={onClick}
display="flex"
flexDirection="column"
minW={"12rem"}
maxW={"18rem"}
minH={"35rem"}
maxH={"20rem"}
>
{imageUrl && (
<Image
src={imageUrl}
alt={title}
objectFit="cover"
minH="16rem"
maxH="20rem"
width="100%"
/>
)}
<Flex direction="column" flex="1" p={4}>
<Box display="flex" alignItems="center" mb={2}>
{icon}
<Heading as="h4" size="md" ml={2}>
{title}
</Heading>
</Box>
<Text fontSize="sm" flex="1">
{description}
</Text>
</Flex>
{badge && (
<Box p={2}>
<Badge colorScheme={"teal"}>{badge}</Badge>
</Box>
)}
</Box>
);
}
export default DemoCard;

View File

@@ -0,0 +1,38 @@
import React from "react";
import { SimpleGrid } from "@chakra-ui/react";
import { Rocket, Shield } from "lucide-react";
import DemoCard from "./DemoCard";
function DemoComponent() {
return (
<SimpleGrid
columns={{ base: 1, sm: 1, lg: 2 }}
spacing={"7%"}
minH={"min-content"}
h={"100vh"}
>
<DemoCard
icon={<Rocket size={24} color="teal" />}
title="toak"
description="A tool for turning git repositories into markdown, without their secrets"
imageUrl="/code-tokenizer-md.jpg"
badge="npm"
onClick={() => {
window.open("https://github.com/seemueller-io/toak");
}}
/>
<DemoCard
icon={<Shield size={24} color="teal" />}
title="REHOBOAM"
description="Explore the latest in AI news around the world in real-time"
imageUrl="/rehoboam.png"
badge="APP"
onClick={() => {
window.open("https://rehoboam.seemueller.io");
}}
/>
</SimpleGrid>
);
}
export default DemoComponent;

View File

@@ -0,0 +1,124 @@
import React from "react";
import {
Box,
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
Textarea,
useToast,
VStack,
} from "@chakra-ui/react";
import { observer } from "mobx-react-lite";
import feedbackState from "../../stores/ClientFeedbackStore";
const FeedbackModal = observer(({ isOpen, onClose, zIndex }) => {
const toast = useToast();
const handleSubmitFeedback = async () => {
const success = await feedbackState.submitFeedback();
if (success) {
toast({
title: "Feedback Submitted",
description: "Thank you for your feedback!",
status: "success",
duration: 3000,
isClosable: true,
});
feedbackState.reset();
onClose();
} else if (feedbackState.error) {
if (!feedbackState.input.trim() || feedbackState.input.length > 500) {
return;
}
toast({
title: "Submission Failed",
description: feedbackState.error,
status: "error",
duration: 3000,
isClosable: true,
});
}
};
const handleClose = () => {
feedbackState.reset();
onClose();
};
const charactersRemaining = 500 - (feedbackState.input?.length || 0);
return (
<Modal
isOpen={isOpen}
onClose={handleClose}
size="md"
motionPreset="slideInBottom"
zIndex={zIndex}
>
<ModalOverlay />
<ModalContent bg="gray.800" color="text.primary">
<ModalHeader textAlign="center">Feedback</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4} align="stretch">
<Text fontSize="md" textAlign="center">
Your thoughts help me improve. Let me know what you think!
</Text>
<Box position="relative">
<Textarea
placeholder="Type your feedback here..."
value={feedbackState.input}
onChange={(e) => feedbackState.setInput(e.target.value)}
bg="gray.700"
color="white"
minHeight="120px"
resize="vertical"
/>
<Text
position="absolute"
bottom="2"
right="2"
fontSize="xs"
color={charactersRemaining < 50 ? "orange.300" : "gray.400"}
>
{charactersRemaining} characters remaining
</Text>
</Box>
{feedbackState.error && (
<Text color="red.500" fontSize="sm">
{feedbackState.error}
</Text>
)}
</VStack>
</ModalBody>
<ModalFooter>
<Button
onClick={handleSubmitFeedback}
isLoading={feedbackState.isLoading}
colorScheme="teal"
mr={3}
disabled={feedbackState.isLoading || !feedbackState.input.trim()}
>
Submit
</Button>
<Button variant="outline" onClick={handleClose} colorScheme="gray">
Cancel
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
});
export default FeedbackModal;

View File

@@ -0,0 +1,35 @@
import React from "react";
import { Box } from "@chakra-ui/react";
const TealDogecoinIcon = (props) => (
<Box
as="svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
boxSize={props.boxSize || "1em"}
{...props}
>
<path
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
fill="#008080"
/>
<path
d="M12 21.6797C17.3459 21.6797 21.6797 17.3459 21.6797 12C21.6797 6.65405 17.3459 2.32031 12 2.32031C6.65405 2.32031 2.32031 6.65405 2.32031 12C2.32031 17.3459 6.65405 21.6797 12 21.6797Z"
fill="#009999"
/>
<path
d="M12 21.4757C17.2333 21.4757 21.4758 17.2333 21.4758 12C21.4758 6.76666 17.2333 2.52423 12 2.52423C6.76672 2.52423 2.52429 6.76666 2.52429 12C2.52429 17.2333 6.76672 21.4757 12 21.4757Z"
fill="#00CCCC"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.5001 9.54053C16.4888 6.92891 13.9888 6.44507 13.9888 6.44507H6.85606L6.88358 9.10523H8.30406V15.0454H6.85596V17.7007H13.7913C15.4628 17.7007 16.8026 16.0211 16.8026 16.0211C18.9482 12.9758 17.5 9.54053 17.5 9.54053H17.5001ZM13.8285 14.2314C13.8285 14.2314 13.2845 15.0163 12.6927 15.0163H11.5087L11.4806 9.11173H13.0001C13.0001 9.11173 13.7041 9.25894 14.1959 10.6521C14.1959 10.6521 14.848 12.6468 13.8285 14.2314Z"
fill="white"
fill-opacity="0.8"
/>
</Box>
);
export default TealDogecoinIcon;

View File

@@ -0,0 +1,23 @@
import React from "react";
import { Box, VStack } from "@chakra-ui/react";
import Markdown from "react-markdown";
import { webComponents } from "../react-markdown/WebComponents";
function LegalDoc({ text }) {
return (
<Box maxWidth="800px" margin="0 auto">
<VStack spacing={6} align="stretch">
<Box
color="text.primary"
wordBreak="break-word"
whiteSpace="pre-wrap"
spacing={4}
>
<Markdown components={webComponents}>{text}</Markdown>
</Box>
</VStack>
</Box>
);
}
export default LegalDoc;

View File

@@ -0,0 +1,123 @@
import React from "react";
import {
Box,
Divider,
Heading,
Link,
List,
ListItem,
OrderedList,
Text,
UnorderedList,
} from "@chakra-ui/react";
import ImageWithFallback from "../chat/ImageWithFallback";
import { MdCheckCircle } from "react-icons/md";
export const webComponents = {
p: ({ children }) => (
<Text fontSize="sm" lineHeight="short">
{children}
</Text>
),
strong: ({ children }) => <strong>{children}</strong>,
h1: ({ children }) => (
<Heading as="h1" size="xl" mt={4} mb={2}>
{children}
</Heading>
),
h2: ({ children }) => (
<Heading as="h2" size="lg" mt={3} mb={2}>
{children}
</Heading>
),
h3: ({ children }) => (
<Heading as="h3" size="md" mt={2} mb={1}>
{children}
</Heading>
),
h4: ({ children }) => (
<Heading as="h4" size="sm" mt={2} mb={1}>
{children}
</Heading>
),
ul: ({ children }) => (
<UnorderedList
// pl={3}
// mb={2}
fontSize="sm"
// stylePosition="inside" // Keep bullets inside the text flow
>
{children}
</UnorderedList>
),
ol: ({ children }) => (
<OrderedList fontSize="sm" spacing={2}>
{children}
</OrderedList>
),
li: ({ children, ...rest }) => {
const filteredChildren = React.Children.toArray(children)
.filter((child) => !(typeof child === "string" && child.trim() === "\n"))
.map((child, index, array) => {
// if (typeof child === 'string' && index === array.length - 1 && /\n/.test(child)) {
// return '\n';
// }
return child;
});
return <ListItem {...rest}>{filteredChildren}</ListItem>;
},
pre: ({ children }) => (
<Box
as="pre"
whiteSpace="pre-wrap"
bg="background.secondary"
borderRadius="md"
fontSize="sm"
>
{children}
</Box>
),
blockquote: ({ children }) => (
<Box
as="blockquote"
borderLeft="4px solid"
borderColor="gray.200"
fontStyle="italic"
color="gray.600"
pl={4}
>
{children}
</Box>
),
hr: () => <Divider my={4} />,
a: ({ href, children }) => (
<Link
color="teal.500"
href={href}
isExternal
maxWidth="100%"
wordBreak="break-word"
>
{children}
</Link>
),
img: ({ alt, src }) => <ImageWithFallback alt={alt} src={src} />,
icon_list: ({ children }) => (
<List spacing={3}>
{React.Children.map(children, (child) => (
<ListItem>
<Box
as={MdCheckCircle}
color="green.500"
mr={2}
display="inline-block"
/>
{child}
</ListItem>
))}
</List>
),
};

View File

@@ -0,0 +1,62 @@
import React, { useCallback, useMemo } from "react";
import { Box, Flex, useMediaQuery } from "@chakra-ui/react";
import { resumeData } from "../../static-data/resume_data";
import SectionContent from "./SectionContent";
import SectionButton from "./SectionButton";
const sections = ["professionalSummary", "skills", "experience", "education"];
export default function ResumeComponent() {
const [activeSection, setActiveSection] = React.useState(
"professionalSummary",
);
const [isMobile] = useMediaQuery("(max-width: 1243px)");
const handleSectionClick = useCallback((section) => {
setActiveSection(section);
}, []);
const capitalizeFirstLetter = useCallback((word) => {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}, []);
const sectionButtons = useMemo(
() =>
sections.map((section) => (
<SectionButton
key={section}
onClick={() => handleSectionClick(section)}
activeSection={activeSection}
section={section}
mobile={isMobile}
callbackfn={capitalizeFirstLetter}
/>
)),
[activeSection, isMobile, handleSectionClick, capitalizeFirstLetter],
);
return (
<Box p={"unset"}>
<Flex
direction={isMobile ? "column" : "row"}
mb={8}
wrap="nowrap"
gap={isMobile ? 2 : 4}
minWidth="0"
>
{sectionButtons}
</Flex>
<Box
bg="background.secondary"
color="text.primary"
borderRadius="md"
boxShadow="md"
borderWidth={1}
borderColor="brand.300"
minHeight="300px"
>
<SectionContent activeSection={activeSection} resumeData={resumeData} />
</Box>
</Box>
);
}

View File

@@ -0,0 +1,32 @@
import React from "react";
import { Button } from "@chakra-ui/react";
import { ChevronRight } from "lucide-react";
function SectionButton(props: {
onClick: () => void;
activeSection: string;
section: string;
mobile: boolean;
callbackfn: (word) => string;
}) {
return (
<Button
mt={1}
onClick={props.onClick}
variant={props.activeSection === props.section ? "solid" : "outline"}
colorScheme="brand"
rightIcon={<ChevronRight size={16} />}
size="md"
width={props.mobile ? "100%" : "auto"}
>
{props.section
.replace(/([A-Z])/g, " $1")
.trim()
.split(" ")
.map(props.callbackfn)
.join(" ")}
</Button>
);
}
export default SectionButton;

View File

@@ -0,0 +1,98 @@
import React from "react";
import {
Box,
Grid,
GridItem,
Heading,
ListItem,
Text,
UnorderedList,
VStack,
} from "@chakra-ui/react";
const fontSize = "md";
const ProfessionalSummary = ({ professionalSummary }) => (
<Box>
<Grid
templateColumns="1fr"
gap={4}
maxW={["100%", "100%", "100%"]}
mx="auto"
className="about-container"
>
<GridItem
colSpan={1}
maxW={["100%", "100%", "container.md"]}
justifySelf="center"
minH={"100%"}
>
<Grid templateColumns="1fr" gap={4} overflowY={"auto"}>
<GridItem>
<Text fontSize="md">{professionalSummary}</Text>
</GridItem>
</Grid>
</GridItem>
</Grid>
</Box>
);
const Skills = ({ skills }) => (
<VStack align={"baseline"} spacing={6} mb={4}>
<UnorderedList spacing={2} mb={0}>
<Box>
{skills?.map((skill, index) => (
<ListItem p={1} key={index}>
{skill}
</ListItem>
))}
</Box>
</UnorderedList>
</VStack>
);
const Experience = ({ experience }) => (
<VStack align="start" spacing={6} mb={4}>
{experience?.map((job, index) => (
<Box key={index} width="100%">
<Heading as="h3" size="md" mb={2}>
{job.title}
</Heading>
<Text fontWeight="bold">{job.company}</Text>
<Text color="gray.500" mb={2}>
{job.timeline}
</Text>
<Text>{job.description}</Text>
</Box>
))}
</VStack>
);
const Education = ({ education }) => (
<UnorderedList spacing={2} mb={4}>
{education?.map((edu, index) => <ListItem key={index}>{edu}</ListItem>)}
</UnorderedList>
);
const SectionContent = ({ activeSection, resumeData }) => {
const components = {
professionalSummary: ProfessionalSummary,
skills: Skills,
experience: Experience,
education: Education,
};
const ActiveComponent = components[activeSection];
return (
<Box p={4} minHeight="300px" width="100%">
{ActiveComponent ? (
<ActiveComponent {...resumeData} />
) : (
<Text>Select a section to view details.</Text>
)}
</Box>
);
};
export default SectionContent;

View File

@@ -0,0 +1,64 @@
// ServicesComponent.js
import React, { useCallback, useMemo } from "react";
import { Box, Flex, useMediaQuery } from "@chakra-ui/react";
import { servicesData } from "../../static-data/services_data";
import SectionButton from "../resume/SectionButton";
import ServicesSectionContent from "./ServicesComponentSection";
const sections = ["servicesOverview", "offerings"];
export default function ServicesComponent() {
const [activeSection, setActiveSection] = React.useState("servicesOverview");
const [isMobile] = useMediaQuery("(max-width: 1243px)");
const handleSectionClick = useCallback((section) => {
setActiveSection(section);
}, []);
const capitalizeFirstLetter = useCallback((word) => {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}, []);
const sectionButtons = useMemo(
() =>
sections.map((section) => (
<SectionButton
key={section}
onClick={() => handleSectionClick(section)}
activeSection={activeSection}
section={section}
mobile={isMobile}
callbackfn={capitalizeFirstLetter}
/>
)),
[activeSection, isMobile, handleSectionClick, capitalizeFirstLetter],
);
return (
<Box p={"unset"}>
<Flex
direction={isMobile ? "column" : "row"}
mb={8}
wrap="nowrap"
gap={isMobile ? 2 : 4}
minWidth="0" // Ensures flex items can shrink if needed
>
{sectionButtons}
</Flex>
<Box
bg="background.secondary"
color="text.primary"
borderRadius="md"
boxShadow="md"
borderWidth={1}
borderColor="brand.300"
minHeight="300px"
>
<ServicesSectionContent
activeSection={activeSection}
data={servicesData}
/>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,40 @@
import React from "react";
import { Box, Heading, Text, VStack } from "@chakra-ui/react";
const ServicesOverview = ({ servicesOverview }) => (
<Text fontSize="md">{servicesOverview}</Text>
);
const Offerings = ({ offerings }) => (
<VStack align="start" spacing={6} mb={4}>
{offerings.map((service, index) => (
<Box key={index}>
<Heading as="h3" size="md" mb={2}>
{service.title}
</Heading>
<Text mb={4}>{service.description}</Text>
</Box>
))}
</VStack>
);
const ServicesSectionContent = ({ activeSection, data }) => {
const components = {
servicesOverview: ServicesOverview,
offerings: Offerings,
};
const ActiveComponent = components[activeSection];
return (
<Box p={4} minHeight="300px" width="100%">
{ActiveComponent ? (
<ActiveComponent {...data} />
) : (
<Text>Select a section to view details.</Text>
)}
</Box>
);
};
export default ServicesSectionContent;

View File

@@ -0,0 +1,29 @@
import React from "react";
import { IconButton } from "@chakra-ui/react";
import { Github } from "lucide-react";
import { toolbarButtonZIndex } from "./Toolbar";
export default function GithubButton() {
return (
<IconButton
as="a"
href="https://github.com/geoffsee"
target="_blank"
aria-label="GitHub"
icon={<Github />}
size="md"
bg="transparent"
stroke="text.accent"
color="text.accent"
_hover={{
bg: "transparent",
svg: {
stroke: "accent.secondary",
transition: "stroke 0.3s ease-in-out",
},
}}
title="GitHub"
zIndex={toolbarButtonZIndex}
/>
);
}

View File

@@ -0,0 +1,41 @@
import React from "react";
import { IconButton, useDisclosure } from "@chakra-ui/react";
import { LucideHeart } from "lucide-react";
import { toolbarButtonZIndex } from "./Toolbar";
import SupportThisSiteModal from "./SupportThisSiteModal";
export default function SupportThisSiteButton() {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<IconButton
as="a"
aria-label="Support"
icon={<LucideHeart />}
cursor="pointer"
onClick={onOpen}
size="md"
stroke="text.accent"
bg="transparent"
_hover={{
bg: "transparent",
svg: {
stroke: "accent.danger",
transition: "stroke 0.3s ease-in-out",
},
}}
title="Support"
variant="ghost"
zIndex={toolbarButtonZIndex}
sx={{
svg: {
stroke: "text.accent",
strokeWidth: "2px",
transition: "stroke 0.2s ease-in-out",
},
}}
/>
<SupportThisSiteModal isOpen={isOpen} onClose={onClose} />
</>
);
}

View File

@@ -0,0 +1,225 @@
import React from "react";
import {
Box,
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
useClipboard,
useToast,
VStack,
} from "@chakra-ui/react";
import { QRCodeCanvas } from "qrcode.react";
import { FaBitcoin, FaEthereum } from "react-icons/fa";
import { observer } from "mobx-react-lite";
import clientTransactionStore from "../../stores/ClientTransactionStore";
import DogecoinIcon from "../icons/DogecoinIcon";
const SupportThisSiteModal = observer(({ isOpen, onClose, zIndex }) => {
const { hasCopied, onCopy } = useClipboard(
clientTransactionStore.depositAddress || "",
);
const toast = useToast();
const handleCopy = () => {
if (clientTransactionStore.depositAddress) {
onCopy();
toast({
title: "Address Copied!",
description: "Thank you for your support!",
status: "success",
duration: 3000,
isClosable: true,
});
}
};
const handleConfirmAmount = async () => {
try {
await clientTransactionStore.prepareTransaction();
toast({
title: "Success",
description: `Use your wallet app (Coinbase, ...ect) to send the selected asset to the provided address.`,
status: "success",
duration: 6000,
isClosable: true,
});
} catch (error) {
toast({
title: "Transaction Failed",
description: "There was an issue preparing your transaction.",
status: "error",
duration: 3000,
isClosable: true,
});
}
};
const donationMethods = [
{
name: "Ethereum",
icon: FaEthereum,
},
{
name: "Bitcoin",
icon: FaBitcoin,
},
{
name: "Dogecoin",
icon: DogecoinIcon,
},
];
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size="md"
motionPreset="slideInBottom"
zIndex={zIndex}
>
<ModalOverlay />
<ModalContent bg="gray.800" color="text.primary">
<ModalHeader textAlign="center" mb={2}>
Support
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={8} align="center">
<Text fontSize="md" textAlign="center">
Your contributions are fuel for magic.
</Text>
<Tabs
align="center"
variant="soft-rounded"
colorScheme="teal"
isFitted
>
<TabList mb={2} w={"20%"}>
{donationMethods.map((method) => (
<Tab
p={4}
key={method.name}
onClick={() => {
clientTransactionStore.setSelectedMethod(method.name);
}}
>
<Box p={1} w={"fit-content"}>
<method.icon />{" "}
</Box>
{method.name}
</Tab>
))}
</TabList>
<TabPanels>
{donationMethods.map((method) => (
<TabPanel key={method.name}>
{!clientTransactionStore.userConfirmed ? (
<VStack spacing={4}>
<Text>Enter your information:</Text>
<Input
placeholder="Your name"
value={
clientTransactionStore.donerId as string | undefined
}
onChange={(e) =>
clientTransactionStore.setDonerId(e.target.value)
}
type="text"
bg="gray.700"
color="white"
w="100%"
/>
<Text>Enter the amount you wish to donate:</Text>
<Input
placeholder="Enter amount"
value={
clientTransactionStore.amount as number | undefined
}
onChange={(e) =>
clientTransactionStore.setAmount(e.target.value)
}
type="number"
bg="gray.700"
color="white"
w="100%"
/>
<Button
onClick={handleConfirmAmount}
size="md"
colorScheme="teal"
>
Confirm Amount
</Button>
</VStack>
) : (
<>
<Box
bg="white"
p={2}
borderRadius="lg"
mb={4}
w={"min-content"}
>
<QRCodeCanvas
value={
clientTransactionStore.depositAddress as string
}
size={180}
/>
</Box>
<Box
bg="gray.700"
p={4}
borderRadius="md"
wordBreak="unset"
w="100%"
textAlign="center"
mb={4}
>
<Text fontWeight="bold" fontSize="xs">
{clientTransactionStore.depositAddress}
</Text>
</Box>
<Button
onClick={handleCopy}
size="md"
colorScheme="teal"
mb={4}
>
{hasCopied ? "Address Copied!" : "Copy Address"}
</Button>
<Text fontSize="md" fontWeight="bold">
Transaction ID: {clientTransactionStore.txId}
</Text>
</>
)}
</TabPanel>
))}
</TabPanels>
</Tabs>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="outline" mr={3} onClick={onClose} colorScheme="gray">
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
});
export default SupportThisSiteModal;

View File

@@ -0,0 +1,25 @@
import React from "react";
import { Flex } from "@chakra-ui/react";
import SupportThisSiteButton from "./SupportThisSiteButton";
import GithubButton from "./GithubButton";
import BuiltWithButton from "../BuiltWithButton";
const toolbarButtonZIndex = 901;
export { toolbarButtonZIndex };
function ToolBar({ isMobile }) {
return (
<Flex
direction={isMobile ? "row" : "column"}
alignItems={isMobile ? "flex-start" : "flex-end"}
pb={4}
>
<SupportThisSiteButton />
<GithubButton />
<BuiltWithButton />
</Flex>
);
}
export default ToolBar;

13
src/layout/Content.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { Flex } from "@chakra-ui/react";
import React from "react";
import { useIsMobile } from "../components/contexts/MobileContext";
function Content({ children }) {
const isMobile = useIsMobile();
return (
<Flex flexDirection="column" w="100%" h="100vh" p={!isMobile ? 4 : 1}>
{children}
</Flex>
);
}
export default Content;

48
src/layout/Hero.tsx Normal file
View File

@@ -0,0 +1,48 @@
import React from "react";
import { Box, Heading, Text } from "@chakra-ui/react";
import { usePageContext } from "../renderer/usePageContext";
import Routes from "../renderer/routes";
import { useIsMobile } from "../components/contexts/MobileContext";
export default function Hero() {
const pageContext = usePageContext();
const isMobile = useIsMobile();
return (
<Box p={2}>
<Box>
<Heading
textAlign={isMobile ? "left" : "right"}
minWidth="90px"
maxWidth={"220px"}
color="text.accent"
as="h3"
letterSpacing={"tight"}
size="lg"
>
{Routes[normalizePath(pageContext.urlPathname)]?.heroLabel}
</Heading>
</Box>
<Text
isTruncated
maxWidth="100%"
whiteSpace="nowrap"
letterSpacing={"tight"}
color="text.accent"
textAlign={isMobile ? "left" : "right"}
overflow="hidden"
>
{new Date().toLocaleDateString()}
</Text>
</Box>
);
}
const normalizePath = (path) => {
if (!path) return "/";
if (path.length > 1 && path.endsWith("/")) {
path = path.slice(0, -1);
}
return path.toLowerCase();
};

21
src/layout/Layout.css Normal file
View File

@@ -0,0 +1,21 @@
/* CSS for people who'd rather be coding */
* {
box-sizing: border-box; /* Because guessing sizes is for amateurs */
}
a {
text-decoration: none;
color: #fffff0; /* White, like the light at the end of a dark terminal */
}
a:hover {
color: #c0c0c0; /* Light gray, like the reflections of your code */
}
/* Media query, because even I have standards */
@media (max-width: 600px) {
body {
font-size: 14px; /* For ants */
}
}

52
src/layout/Layout.tsx Normal file
View File

@@ -0,0 +1,52 @@
import React, { useEffect, useState } from "react";
import { PageContextProvider } from "../renderer/usePageContext";
import { MobileProvider } from "../components/contexts/MobileContext";
import LayoutComponent from "./LayoutComponent";
import userOptionsStore from "../stores/UserOptionsStore";
import { observer } from "mobx-react-lite";
import { Chakra } from "../components/contexts/ChakraContext";
import { getTheme } from "./theme/color-themes";
export { Layout };
const Layout = observer(({ pageContext, children }) => {
const [activeTheme, setActiveTheme] = useState<string>("darknight");
useEffect(() => {
if (userOptionsStore.theme !== activeTheme) {
setActiveTheme(userOptionsStore.theme);
}
}, [userOptionsStore.theme]);
try {
if (pageContext?.headersOriginal) {
const headers = new Headers(pageContext.headersOriginal);
const cookies = headers.get("cookie");
const userPreferencesCookie = cookies
?.split("; ")
.find((row) => row.startsWith("user_preferences="))
?.split("=")[1];
try {
const { theme: receivedTheme } = JSON.parse(
atob(userPreferencesCookie ?? "{}"),
);
setActiveTheme(receivedTheme);
} catch (e) {}
}
} catch (e) {}
return (
<React.StrictMode>
<PageContextProvider pageContext={pageContext}>
<MobileProvider>
<Chakra theme={getTheme(activeTheme)}>
<LayoutComponent>{children}</LayoutComponent>
</Chakra>
</MobileProvider>
</PageContextProvider>
</React.StrictMode>
);
});

View File

@@ -0,0 +1,35 @@
import React from "react";
import { Grid, GridItem } from "@chakra-ui/react";
import Navigation from "./Navigation";
import Routes from "../renderer/routes";
import Hero from "./Hero";
import Content from "./Content";
import { useIsMobile } from "../components/contexts/MobileContext";
export default function LayoutComponent({ children }) {
const isMobile = useIsMobile();
return (
<Grid
templateAreas={
isMobile
? `"nav"
"main"`
: `"nav main"`
}
gridTemplateRows={isMobile ? "auto 1fr" : "1fr"}
gridTemplateColumns={isMobile ? "1fr" : "auto 1fr"}
minHeight="100vh"
gap="1"
>
<GridItem area={"nav"} hidden={false}>
<Navigation routeRegistry={Routes}>
<Hero />
</Navigation>
</GridItem>
<GridItem area={"main"}>
<Content>{children}</Content>
</GridItem>
</Grid>
);
}

43
src/layout/NavItem.tsx Normal file
View File

@@ -0,0 +1,43 @@
import { Box } from "@chakra-ui/react";
import React from "react";
function NavItem({ path, children, color, onClick, as, cursor }) {
return (
<Box
as={as ?? "a"}
href={path}
mb={2}
cursor={cursor}
// ml={5}
mr={2}
color={color ?? "text.accent"}
letterSpacing="normal"
display="block"
position="relative"
textAlign="right"
onClick={onClick}
_after={{
content: '""',
position: "absolute",
width: "100%",
height: "2px",
bottom: "0",
left: "0",
bg: "accent.secondary",
transform: "scaleX(0)",
transformOrigin: "right",
transition: "transform 0.3s ease-in-out",
}}
_hover={{
color: "tertiary.tertiary",
_after: {
transform: "scaleX(1)",
},
}}
>
{children}
</Box>
);
}
export default NavItem;

132
src/layout/Navigation.tsx Normal file
View File

@@ -0,0 +1,132 @@
import React, { useEffect } from "react";
import { observer } from "mobx-react-lite";
import {
Box,
Collapse,
Grid,
GridItem,
useBreakpointValue,
} from "@chakra-ui/react";
import { MenuIcon } from "lucide-react";
import Sidebar from "./Sidebar";
import NavItem from "./NavItem";
import menuState from "../stores/AppMenuStore";
import { usePageContext } from "../renderer/usePageContext";
import { useIsMobile } from "../components/contexts/MobileContext";
import { getTheme } from "./theme/color-themes";
import userOptionsStore from "../stores/UserOptionsStore";
const Navigation = observer(({ children, routeRegistry }) => {
const isMobile = useIsMobile();
const pageContext = usePageContext();
const currentPath = pageContext.urlPathname || "/";
const getTopValue = () => {
if (!isMobile) return undefined;
if (currentPath === "/") return 12;
return 0;
};
const variant = useBreakpointValue(
{
base: "outline",
md: "solid",
},
{
fallback: "md",
},
);
useEffect(() => {
menuState.closeMenu();
}, [variant]);
return (
<Grid templateColumns="1fr" templateRows="auto 1fr">
<GridItem
p={4}
position="fixed"
top={0}
left={0}
zIndex={1100}
width={isMobile ? "20%" : "100%"}
hidden={!isMobile}
>
<Grid templateColumns="auto 1fr" alignItems="center">
<GridItem>
<MenuIcon
cursor="pointer"
w={6}
h={6}
stroke={getTheme(userOptionsStore.theme).colors.text.accent}
onClick={() => {
switch (menuState.isOpen) {
case true:
menuState.closeMenu();
break;
case false:
menuState.openMenu();
break;
}
}}
/>
</GridItem>
<GridItem>{children}</GridItem>
</Grid>
</GridItem>
<GridItem>
<Collapse
hidden={isMobile && !menuState.isOpen}
in={(isMobile && menuState.isOpen) || !isMobile}
animateOpacity
>
<Grid
as="nav"
templateColumns="1fr"
width="100%"
h={isMobile ? "100vh" : "100vh"}
top={getTopValue()}
position={"relative"}
bg={"transparent"}
zIndex={1000}
gap={4}
p={isMobile ? 4 : 0}
>
<GridItem>{!isMobile && children}</GridItem>
<GridItem>
<Sidebar>
{Object.keys(routeRegistry)
.filter((p) => !routeRegistry[p].hideNav)
.map((path) => (
<NavItem key={path} path={path}>
{routeRegistry[path].sidebarLabel}
</NavItem>
))}
</Sidebar>
</GridItem>
</Grid>
</Collapse>
</GridItem>
{isMobile && (
<Box
position="fixed"
top="0"
left="0"
right="0"
height={menuState.isOpen ? "100vh" : "auto"}
pointerEvents="none"
zIndex={900}
>
<Box
height="100%"
transition="all 0.3s"
opacity={menuState.isOpen ? 1 : 0}
/>
</Box>
)}
</Grid>
);
});
export default Navigation;

141
src/layout/Sidebar.tsx Normal file
View File

@@ -0,0 +1,141 @@
import React, { useState } from "react";
import { Box, Flex, VStack } from "@chakra-ui/react";
import NavItem from "./NavItem";
import ToolBar from "../components/toolbar/Toolbar";
import { useIsMobile } from "../components/contexts/MobileContext";
import FeedbackModal from "../components/feedback/FeedbackModal";
import { ThemeSelectionOptions } from "../components/ThemeSelection";
function LowerSidebarContainer({ children, isMobile, ...props }) {
const bottom = isMobile ? undefined : "6rem";
const position = isMobile ? "relative" : "absolute";
return (
<Box width="100%" m={0.99} position={position} bottom={bottom} {...props}>
{children}
</Box>
);
}
function Sidebar({ children: navLinks }) {
const isMobile = useIsMobile();
return (
<SidebarContainer isMobile={isMobile}>
<VStack
spacing={6}
alignItems={isMobile ? "flex-start" : "flex-end"}
letterSpacing="tighter"
width="100%"
height="100%"
>
{navLinks}
<Box
alignItems={isMobile ? "flex-start" : "flex-end"}
bg="background.primary"
zIndex={1000}
width="100%"
fontSize={"x-small"}
>
<LowerSidebarContainer isMobile={isMobile}>
<ToolBar isMobile={isMobile} />
<RegulatoryItems isMobile={isMobile} />
<ThemeSelectionOptions />
</LowerSidebarContainer>
</Box>
</VStack>
{!isMobile && <BreathingVerticalDivider />}
</SidebarContainer>
);
}
function RegulatoryItems({ isMobile }) {
const [isFeedbackModalOpen, setFeedbackModalOpen] = useState(false);
const openFeedbackModal = () => setFeedbackModalOpen(true);
const closeFeedbackModal = () => setFeedbackModalOpen(false);
return (
<>
<VStack alignItems={isMobile ? "flex-start" : "flex-end"} spacing={1}>
<NavItem
color="text.tertiary"
as={"span"}
path=""
cursor={"pointer"}
onClick={() => {
window.open("https://seemueller.ai");
}}
>
seemueller.ai
</NavItem>
<NavItem
color="text.tertiary"
as={"span"}
path=""
cursor={"pointer"}
onClick={openFeedbackModal}
>
Feedback
</NavItem>
<NavItem color="text.tertiary" path="/privacy-policy">
Privacy Policy
</NavItem>
<NavItem color="text.tertiary" path="/terms-of-service">
Terms of Service
</NavItem>
</VStack>
{/* Feedback Modal */}
<FeedbackModal
isOpen={isFeedbackModalOpen}
onClose={closeFeedbackModal}
/>
</>
);
}
function SidebarContainer({ children, isMobile }) {
return (
<Flex
mt={isMobile ? 28 : undefined}
position="relative"
height="100vh"
width="100%"
>
{children}
</Flex>
);
}
function BreathingVerticalDivider() {
return (
<Box
position="absolute"
h="150%"
right={0}
bottom={0}
width="2px"
background="text.secondary"
animation="breathing 3s ease-in-out infinite"
>
<style>
{`
@keyframes breathing {
0%, 100% {
opacity: 0.7;
transform: scaleY(1);
}
50% {
opacity: 1;
transform: scaleY(1.2);
}
}
`}
</style>
</Box>
);
}
export default Sidebar;

View File

@@ -0,0 +1,19 @@
import { useEffect, useState } from "react";
import { useMediaQuery } from "@chakra-ui/react";
// Only use this when it is necessary to style responsively outside a MobileProvider.
export function useIsMobile() {
const [isMobile, setIsMobile] = useState(false);
const [isFallbackMobile] = useMediaQuery("(max-width: 768px)");
useEffect(() => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
const mobile =
/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
userAgent.toLowerCase(),
);
setIsMobile(mobile);
}, []);
return isMobile || isFallbackMobile;
}

View File

@@ -0,0 +1,239 @@
import { extendTheme } from "@chakra-ui/react";
const fonts = {
body: "monospace",
heading: "monospace",
};
const styles = {
global: {
body: {
fontFamily: fonts.body,
bg: "background.primary",
color: "text.primary",
margin: 0,
overflow: "hidden",
},
html: {
overflow: "hidden",
},
"::selection": {
backgroundColor: "accent.secondary",
color: "background.primary",
},
},
};
const components = {
Button: {
baseStyle: {
fontWeight: "bold",
borderRadius: "md", // Slightly rounded corners
},
variants: {
solid: {
bg: "accent.primary",
color: "background.primary",
_hover: {
bg: "accent.primary",
color: "background.primary",
},
},
outline: {
borderColor: "accent.primary",
color: "text.primary",
_hover: {
bg: "accent.primary",
color: "background.primary",
},
},
ghost: {
color: "text.primary",
_hover: {
bg: "background.secondary",
},
},
},
},
Link: {
baseStyle: {
color: "accent.secondary",
_hover: {
color: "accent.primary",
textDecoration: "none",
},
},
},
Heading: {
baseStyle: {
color: "text.primary",
letterSpacing: "tight",
},
sizes: {
"4xl": { fontSize: ["6xl", null, "7xl"], lineHeight: 1 },
"3xl": { fontSize: ["5xl", null, "6xl"], lineHeight: 1.2 },
"2xl": { fontSize: ["4xl", null, "5xl"] },
xl: { fontSize: ["3xl", null, "4xl"] },
lg: { fontSize: ["2xl", null, "3xl"] },
md: { fontSize: "xl" },
sm: { fontSize: "md" },
xs: { fontSize: "sm" },
},
},
Text: {
baseStyle: {
color: "text.primary",
},
variants: {
secondary: {
color: "text.secondary",
},
accent: {
color: "text.accent",
},
},
},
Input: {
variants: {
filled: {
field: {
bg: "background.secondary",
_hover: {
bg: "background.tertiary",
},
_focus: {
bg: "background.tertiary",
borderColor: "accent.primary",
},
},
},
},
},
MDXEditor: {
baseStyle: (props) => ({
border: "1px solid",
borderColor: "text.primary",
borderRadius: "lg",
height: "sm",
bg: "background.primary",
color: "text.primary",
boxShadow: "md",
// p: 4,
".mdxeditor-toolbar": {
border: "1px solid",
borderColor: "text.primary",
borderRadius: "xl",
bg: "background.primary",
m: 2,
p: 2,
// mb: 3,
// p: 3,
"& button": {
border: "none",
borderRadius: "md",
cursor: "pointer",
px: 3,
py: 1,
mr: 2,
transition: "all 0.3s ease",
_hover: {
bg: "text.secondary",
},
_active: {
bg: "background.primary",
color: "text.primary",
},
'&[data-state="on"]': {
bg: "transparent",
fill: "text.primary",
stroke: "text.primary",
boxShadow: "0 0 0 2px var(--text-primary)",
transform: "translateY(-1px)",
transition: "all 0.2s ease",
// border: '2px solid transparent', // No border needed for SVG
},
'&[data-state="off"]': {
bg: "transparent",
fill: "text.secondary",
stroke: "text.secondary",
opacity: 0.8,
transition: "all 0.2s ease",
},
},
},
"[aria-label='editable markdown']": {
color: "text.primary",
},
}),
},
CodeBlocks: {
baseStyle: (props) => ({
bg: "background.primary",
// color: 'text.primary',
}),
},
};
const Base_theme = extendTheme({
config: {
cssVarPrefix: "wgs",
initialColorMode: "dark",
useSystemColorMode: false,
},
fonts,
styles,
components,
letterSpacings: {
tighter: "-0.05em",
tight: "-0.025em",
normal: "0",
wide: "0.025em",
wider: "0.05em",
widest: "0.1em",
},
space: {
px: "1px",
0.5: "0.125rem",
1: "0.25rem",
1.5: "0.375rem",
2: "0.5rem",
2.5: "0.625rem",
3: "0.75rem",
3.5: "0.875rem",
4: "1rem",
5: "1.25rem",
6: "1.5rem",
7: "1.75rem",
8: "2rem",
9: "2.25rem",
10: "2.5rem",
12: "3rem",
14: "3.5rem",
16: "4rem",
18: "4.5rem",
20: "5rem",
22: "5.5rem",
24: "6rem",
28: "7rem",
32: "8rem",
34: "8.5rem",
36: "9rem",
38: "9.5rem",
40: "10rem",
44: "11rem",
48: "12rem",
52: "13rem",
56: "14rem",
60: "15rem",
64: "16rem",
72: "18rem",
80: "20rem",
96: "24rem",
},
});
export default Base_theme;

View File

@@ -0,0 +1,32 @@
export default {
brand: {
900: "#21252b",
800: "#343a40",
750: "#495057",
700: "#525c65",
600: "#90ee90",
500: "#ffa07a",
400: "#e0e0e0",
300: "#ff69b4",
200: "#da70d6",
100: "#ffffff",
},
background: {
primary: "#21252b",
secondary: "#343a40",
tertiary: "#495057",
},
text: {
primary: "#e0e0e0",
secondary: "#c0c0c0",
tertiary: "#a9a9a9",
accent: "#87cefa",
link: "#87cefa",
},
accent: {
primary: "#90ee90",
secondary: "#ffa07a",
danger: "#ff69b4",
confirm: "#90ee90",
},
};

View File

@@ -0,0 +1,32 @@
export default {
brand: {
900: "#1E1E2E",
800: "#302D41",
750: "#332E41",
700: "#575268",
600: "#6E6C7E",
500: "#988BA2",
400: "#C3BAC6",
300: "#D9E0EE",
200: "#F5E0DC",
100: "#FAE3B0",
},
background: {
primary: "#1E1E2E",
secondary: "#302D41",
tertiary: "#575268",
},
text: {
primary: "#D9E0EE",
secondary: "#C3BAC6",
tertiary: "#988BA2",
accent: "#F5E0DC",
link: "#96CDFB",
},
accent: {
primary: "#F5C2E7",
secondary: "#DDB6F2",
danger: "#F28FAD",
confirm: "#ABE9B3",
},
};

View File

@@ -0,0 +1,32 @@
export default {
brand: {
900: "#000000",
800: "#333333",
750: "#2B2B2B",
700: "#666666",
600: "#999999",
500: "#CCCCCC",
400: "#FFFFFF",
300: "#F0F0F0",
200: "#F8F9FA",
100: "#FFFFFF",
},
background: {
primary: "#000000",
secondary: "#222222",
tertiary: "#333333",
},
text: {
primary: "#F0F0F0",
secondary: "#CCCCCC",
tertiary: "#999999",
accent: "#FFFFFF",
link: "#0d9488",
},
accent: {
primary: "#FFFFFF",
secondary: "#c0c0c0",
danger: "#E53E3E",
confirm: "#00D26A",
},
};

View File

@@ -0,0 +1,40 @@
export default {
brand: {
colors: {
900: "#2C2E43",
800: "#3D4162",
750: "#4F5285",
700: "#6076AC",
600: "#7693D6",
500: "#8DAFF0",
400: "#A3C7FF",
300: "#B9E0FF",
200: "#CDF4FE",
100: "#E1FEFF",
},
},
background: {
primary: "linear-gradient(360deg, #15171C 100%, #353A47 100%)",
secondary: "#1B1F26",
tertiary: "#1E1E2E",
},
text: {
primary: "#f8f8f8",
secondary: "#3D4162",
tertiary: "#e5ebff",
accent: "#e6e6e6",
link: "aquamarine",
},
accent: {
primary: "#127c91",
secondary: "#39b4bf",
danger: "#E74C3C",
confirm: "#27AE60",
},
};

View File

@@ -0,0 +1,35 @@
export default {
brand: {
900: "#15171C",
800: "#1B1F26",
750: "#222731",
700: "#353A47",
600: "#535966",
500: "#747C88",
400: "#A0A4AC",
300: "#C6CBDC",
200: "#E6E9F0",
100: "#F3F4F8",
},
background: {
primary: "#15171C",
secondary: "#1B1F26",
tertiary: "#353A47",
},
text: {
primary: "#ffffff",
secondary: "#A0A4AC",
tertiary: "#747C88",
accent: "#E6E9F0",
link: "#96CDFB",
},
accent: {
primary: "#0095ff",
secondary: "#00acff",
danger: "#EA4D4D",
confirm: "#10CE8D",
},
};

View File

@@ -0,0 +1,50 @@
import { extendTheme } from "@chakra-ui/react";
import BaseTheme from "../base_theme";
import DarknightColors from "./Darknight";
import CapuchinColors from "./Capuchin";
import VsCodeColors from "./VsCode";
import OneDark from "./OneDark";
export function getColorThemes() {
return [
{ name: "darknight", colors: DarknightColors },
{ name: "onedark", colors: OneDark },
{ name: "capuchin", colors: CapuchinColors },
{ name: "vscode", colors: VsCodeColors },
];
}
const darknight = extendTheme({
...BaseTheme,
colors: DarknightColors,
});
const capuchin = extendTheme({
...BaseTheme,
colors: CapuchinColors,
});
const vsCode = extendTheme({
...BaseTheme,
colors: VsCodeColors,
});
const onedark = extendTheme({
...BaseTheme,
colors: OneDark,
});
export function getTheme(theme: string) {
switch (theme) {
case "onedark":
return onedark;
case "darknight":
return darknight;
case "capuchin":
return capuchin;
case "vscode":
return vsCode;
default:
return darknight;
}
}

33
src/layout/useMaxWidth.ts Normal file
View File

@@ -0,0 +1,33 @@
import { useState, useEffect } from "react";
import { useIsMobile } from "../components/contexts/MobileContext";
export const useMaxWidth = () => {
const isMobile = useIsMobile();
const [maxWidth, setMaxWidth] = useState("600px");
const calculateMaxWidth = () => {
if (isMobile) {
setMaxWidth("800px");
} else if (window.innerWidth < 1024) {
setMaxWidth("500px");
} else {
setMaxWidth("800px");
}
};
useEffect(() => {
calculateMaxWidth();
const handleResize = () => {
calculateMaxWidth();
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [isMobile]);
return maxWidth;
};

View File

@@ -0,0 +1,26 @@
import { useEffect, useState } from "react";
const usePageLoaded = (callback: () => void) => {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const handlePageLoad = () => {
setIsLoaded(true);
callback();
};
if (document.readyState === "complete") {
// Page is already fully loaded
handlePageLoad();
} else {
// Wait for the page to load
window.addEventListener("load", handlePageLoad);
}
return () => window.removeEventListener("load", handlePageLoad);
}, [callback]);
return isLoaded;
};
export default usePageLoaded;

6
src/models/Attachment.ts Normal file
View File

@@ -0,0 +1,6 @@
import { types } from "mobx-state-tree";
export default types.model("Attachment", {
content: types.string,
url: types.string,
});

View File

@@ -0,0 +1,7 @@
// models/IntermediateStep.ts
import { types } from "mobx-state-tree";
export default types.model("IntermediateStep", {
kind: types.string,
data: types.frozen(), // Allows storing any JSON-serializable data
});

15
src/models/Message.ts Normal file
View File

@@ -0,0 +1,15 @@
import { types } from "mobx-state-tree";
export default types
.model("Message", {
content: types.string,
role: types.enumeration(["user", "assistant"]),
})
.actions((self) => ({
setContent(newContent: string) {
self.content = newContent;
},
append(newContent: string) {
self.content += newContent;
},
}));

3
src/pages/+client.ts Normal file
View File

@@ -0,0 +1,3 @@
import UserOptionsStore from "../stores/UserOptionsStore";
UserOptionsStore.initialize();

24
src/pages/+data.ts Normal file
View File

@@ -0,0 +1,24 @@
// https://vike.dev/data
import Routes from "../../src/renderer/routes";
export { data };
export type Data = Awaited<ReturnType<typeof data>>;
import type { PageContextServer } from "vike/types";
const data = async (pageContext: PageContextServer) => {
const getTitle = (path) => {
return Routes[normalizePath(path)]?.heroLabel || "";
};
const normalizePath = (path) => {
if (!path) return "/";
if (path.length > 1 && path.endsWith("/")) {
path = path.slice(0, -1);
}
return path.toLowerCase();
};
return {
// The page's <title>
title: getTitle(pageContext.urlOriginal),
};
};

View File

@@ -0,0 +1,40 @@
import { usePageContext } from "../../renderer/usePageContext";
import { Center, Text } from "@chakra-ui/react";
export { Page };
function Page() {
const pageContext = usePageContext();
let msg: string;
const { abortReason, abortStatusCode } = pageContext;
if (abortReason?.notAdmin) {
msg = "You cannot access this page because you aren't an administrator.";
} else if (typeof abortReason === "string") {
msg = abortReason;
} else if (abortStatusCode === 403) {
msg =
"You cannot access this page because you don't have enough privileges.";
} else if (abortStatusCode === 401) {
msg =
"You cannot access this page because you aren't logged in. Please log in.";
} else {
msg = pageContext.is404
? "This page doesn't exist."
: "Something went wrong. Try again (later).";
}
return (
<Center height="100vh">
<Text>{msg}</Text>
</Center>
);
}
declare global {
namespace Vike {
interface PageContext {
abortReason?: string | { notAdmin: true };
}
}
}

View File

@@ -0,0 +1,24 @@
import React from "react";
import { Box, VStack } from "@chakra-ui/react";
import ConnectComponent from "../../components/connect/ConnectComponent";
import { Fragment } from "react";
import { useIsMobile } from "../../components/contexts/MobileContext";
export default function ConnectPage() {
const isMobile = useIsMobile();
return (
<Fragment>
<VStack
minH={"100%"}
maxW={"40rem"}
align="start"
h={!isMobile ? "90vh" : "70vh"}
>
<Box maxW={"710px"} p={2} overflowY={"auto"} mt={isMobile ? 24 : 0}>
<ConnectComponent />
</Box>
</VStack>
</Fragment>
);
}

26
src/pages/index/+Page.tsx Normal file
View File

@@ -0,0 +1,26 @@
import React, { useEffect } from "react";
import { Stack } from "@chakra-ui/react";
import Chat from "../../components/chat/Chat";
import clientChatStore from "../../stores/ClientChatStore";
import { getModelFamily } from "../../components/chat/SupportedModels";
// renders for path: "/"
export default function IndexPage() {
useEffect(() => {
try {
let model = localStorage.getItem("recentModel");
if (getModelFamily(model as string)) {
clientChatStore.setModel(model as string);
}
} catch (_) {
console.log("using default model");
}
}, []);
return (
<Stack direction="column" height="100%" width="100%" spacing={0}>
<Chat height="100%" width="100%" />
</Stack>
);
}

View File

@@ -0,0 +1,27 @@
import React, { Fragment } from "react";
import { Box, VStack } from "@chakra-ui/react";
import PrivacyPolicy from "../../components/legal/LegalDoc";
import privacy_policy from "./privacy_policy";
import { useIsMobile } from "../../components/contexts/MobileContext";
export default function Page() {
const isMobile = useIsMobile();
return (
<Fragment>
<VStack
width={"100%"}
align={"center"}
height={!isMobile ? "100%" : "100%"}
overflowX={"auto"}
>
<Box
overflowY={isMobile ? "scroll" : undefined}
maxH={!isMobile ? "70vh" : "89vh"}
mt={isMobile ? 24 : undefined}
>
<PrivacyPolicy text={privacy_policy} />
</Box>
</VStack>
</Fragment>
);
}

View File

@@ -0,0 +1,117 @@
const privacyPolicyUpdateDate = new Date().toISOString().split("T")[0];
export default `
### Privacy Policy
_Last Updated: ${privacyPolicyUpdateDate}_
This Privacy Policy describes how **geoff.seemueller.io LC** ("**we**", "**us**", or "**our**") collects, uses, and shares personal information when you visit or interact with **geoff.seemueller.io** (the "**Site**"). By accessing or using our Site, you agree to the collection and use of information in accordance with this policy.
---
### 1. Information We Collect
**a. Automatically Collected Information**
When you visit our Site, we automatically collect certain information about your device and interaction with the Site:
- **IP Address**: We collect your IP address to understand where our visitors are coming from and to enhance security.
- **Cookies and UUID**: We assign a unique identifier (UUID) to your device, stored in a cookie that expires in 30 years. This helps us recognize you on subsequent visits.
- **User Agent**: Information about your browser type, version, and operating system.
- **Referer**: The URL of the website that referred you to our Site.
- **Performance Metrics**: Page load time, DNS time, page download time, redirect response time, TCP connect time, server response time, DOM interactive time, and content loaded time.
- **Screen and Browser Information**: Screen resolution, viewport size, screen colors, document encoding, user language, and other similar data.
**b. Information Provided by You**
While we do not require you to provide personal information, any data you voluntarily submit through contact forms or other interactive features will be collected and processed in accordance with this Privacy Policy.
---
### 2. How We Use Your Information
We use the collected information for the following purposes:
- **Analytics and Performance Monitoring**: To analyze usage patterns and improve the functionality and user experience of our Site.
- **Security and Fraud Prevention**: To protect our Site and users from malicious activities.
- **Compliance with Legal Obligations**: To comply with applicable laws, regulations, and legal processes.
- **Personalization**: To remember your preferences and personalize your experience on our Site.
---
### 3. Cookies and Similar Technologies
We use cookies to enhance your browsing experience:
- **What Are Cookies?** Cookies are small text files stored on your device when you visit a website.
- **Purpose of Cookies**: We use a cookie to store your UUID, which helps us recognize your device on future visits.
- **Cookie Duration**: Our cookies expire after 30 years unless deleted by you.
- **Managing Cookies**: You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, some features of our Site may not function properly without cookies.
---
### 4. Sharing Your Information
We do not sell, trade, or rent your personal information to third parties.
---
### 5. Data Retention
We retain your personal information only for as long as necessary to fulfill the purposes outlined in this Privacy Policy:
- **UUID and Cookies**: Stored for up to 30 years unless you delete the cookie.
- **Log Data**: Retained for analytical and security purposes for a reasonable period or as required by law.
---
### 6. Your Rights and Choices
Depending on your jurisdiction, you may have the following rights regarding your personal information:
- **Access**: Request access to the personal data we hold about you.
- **Correction**: Request correction of inaccurate personal data.
- **Deletion**: Request deletion of your personal data.
- **Restriction**: Request restriction of processing your personal data.
- **Objection**: Object to the processing of your personal data.
- **Data Portability**: Request the transfer of your personal data to another party.
To exercise these rights, please contact us at **support@seemueller.io**. We may need to verify your identity before fulfilling your request.
---
### 7. Security Measures
We implement reasonable security measures to protect your personal information from unauthorized access, alteration, disclosure, or destruction. However, no method of transmission over the internet or electronic storage is 100% secure.
---
### 8. International Data Transfers
Your information may be transferred to and maintained on servers located outside of your state, province, country, or other governmental jurisdiction where data protection laws may differ. By providing your information, you consent to such transfers.
---
### 9. Children's Privacy
Our Site is not intended for individuals under the age of 16. We do not knowingly collect personal information from children under 16. If you believe we have collected such information, please contact us to delete it.
---
### 10. Third-Party Links
Our Site may contain links to external websites that are not operated by us. We have no control over and assume no responsibility for the content or privacy practices of these sites.
---
### 11. Changes to This Privacy Policy
We may update this Privacy Policy from time to time:
- **Notification**: Changes will be posted on this page with an updated "Last Updated" date.
- **Material Changes**: If significant changes are made, we will provide more prominent notice.
---
### 12. Contact Us
If you have any questions or concerns about this Privacy Policy or our data practices, please contact us:
- **Email**: support@seemueller.io
`;

View File

@@ -0,0 +1,27 @@
import React, { Fragment } from "react";
import { Box, VStack } from "@chakra-ui/react";
import TermsOfService from "../../components/legal/LegalDoc";
import terms_of_service from "./terms_of_service";
import { useIsMobile } from "../../components/contexts/MobileContext";
export default function Page() {
const isMobile = useIsMobile();
return (
<Fragment>
<VStack
width={"100%"}
align={"center"}
height={!isMobile ? "100%" : "100%"}
overflowX={"auto"}
>
<Box
overflowY={isMobile ? "scroll" : undefined}
maxH={!isMobile ? "70vh" : "89vh"}
mt={isMobile ? 24 : undefined}
>
<TermsOfService text={terms_of_service} />
</Box>
</VStack>
</Fragment>
);
}

View File

@@ -0,0 +1,107 @@
const tosUpdateDate = new Date().toISOString().split("T")[0];
export default `
### Terms of Service
_Last Updated: ${tosUpdateDate}_
Welcome to **geoff.seemueller.io** (the "Site"), operated by **geoff.seemueller.io LC** ("**we**", "**us**", or "**our**"). By accessing or using our Site, you agree to be bound by these Terms of Service ("Terms"). If you do not agree with these Terms, please do not use the Site.
---
### 1. Acceptance of Terms
By accessing or using the Site, you acknowledge that you have read, understood, and agree to be bound by these Terms and all applicable laws and regulations.
---
### 2. Use of the Site
**a. Eligibility**
You must be at least 16 years old to use this Site. By using the Site, you represent and warrant that you meet this requirement.
**b. Permitted Use**
You agree to use the Site solely for lawful purposes and in a way that does not infringe the rights of others or restrict or inhibit their use and enjoyment of the Site.
**c. Prohibited Activities**
You agree not to:
- Upload or transmit any harmful or malicious code.
- Interfere with the security or integrity of the Site.
- Use any automated means to access the Site without our permission.
- Attempt to gain unauthorized access to any portion of the Site.
---
### 3. Intellectual Property Rights
All content on the Site—including text, graphics, logos, images, and software—is the property of **geoff.seemueller.io LC** or its content suppliers and is protected by intellectual property laws. Unauthorized use of any materials on the Site may violate copyright, trademark, and other laws.
---
### 4. User Content
**a. Submissions**
Any content you submit to the Site, such as comments or feedback, is your responsibility. By submitting content, you grant us a non-exclusive, royalty-free, worldwide license to use, reproduce, modify, and display such content.
**b. Content Standards**
Your submitted content must not be unlawful, threatening, defamatory, obscene, or otherwise objectionable.
---
### 5. Disclaimer of Warranties
The Site is provided on an "as is" and "as available" basis. We make no warranties, express or implied, regarding the Site's operation or the information, content, or materials included.
---
### 6. Limitation of Liability
In no event shall **geoff.seemueller.io LC** or its affiliates be liable for any indirect, incidental, special, consequential, or punitive damages arising out of or related to your use of the Site.
---
### 7. Indemnification
You agree to indemnify, defend, and hold harmless **geoff.seemueller.io LC** and its affiliates from any claims, liabilities, damages, losses, or expenses arising from your use of the Site or violation of these Terms.
---
### 8. Modifications to the Terms
We reserve the right to modify these Terms at any time:
- **Notification**: Changes will be posted on this page with an updated "Last Updated" date.
- **Continued Use**: Your continued use of the Site after any changes signifies your acceptance of the new Terms.
---
### 9. Governing Law
These Terms are governed by and construed in accordance with the laws of the jurisdiction in which **geoff.seemueller.io LC** operates, without regard to its conflict of law principles.
---
### 10. Severability
If any provision of these Terms is found to be invalid or unenforceable, the remaining provisions shall remain in full force and effect.
---
### 11. Entire Agreement
These Terms constitute the entire agreement between you and **geoff.seemueller.io LC** regarding your use of the Site and supersede all prior agreements.
---
### 12. Contact Information
If you have any questions or concerns about these Terms, please contact us:
- **Email**: support@seemueller.io
`;

6
src/renderer/+config.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { Config } from "vike/types";
// https://vike.dev/config
export default {
passToClient: ["pageProps", "urlPathname"],
} satisfies Config;

View File

@@ -0,0 +1,16 @@
// https://vike.dev/onRenderClient
export { onRenderClient };
import React from "react";
import { hydrateRoot } from "react-dom/client";
import { Layout } from "../layout/Layout";
async function onRenderClient(pageContext) {
const { Page, pageProps } = pageContext;
hydrateRoot(
document.getElementById("page-view"),
<Layout pageContext={pageContext}>
<Page {...pageProps} />
</Layout>,
);
}

View File

@@ -0,0 +1,56 @@
import React from "react";
// https://vike.dev/onRenderHtml
export { onRenderHtml };
import { renderToStream } from "react-streaming/server";
import { escapeInject } from "vike/server";
import { Layout } from "../layout/Layout";
import type { OnRenderHtmlAsync } from "vike/types";
const onRenderHtml: OnRenderHtmlAsync = async (
pageContext,
): ReturnType<OnRenderHtmlAsync> => {
const { Page, pageProps } = pageContext;
const page = (
<Layout pageContext={pageContext}>
<Page {...pageProps} />
</Layout>
);
let ua;
try {
ua = pageContext.headers["user-agent"];
} catch (e) {
ua = "";
}
const res = escapeInject`<!DOCTYPE html>
<html data-theme="dark" lang="en">
<head>
<title>Geoff Seemueller</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<meta name="description" content="Maker Site">
<script>
window.ga_tid = "resume-site";
window.ga_api = "/api/metrics";
</script>
<script src="/cfga.min.js" async></script>
</head>
<body>
<div id="page-view">${await renderToStream(page, { userAgent: ua })}</div>
</body>
</html>`;
return {
documentHtml: res,
pageContext: {
// enableEagerStreaming: true
},
};
};

18
src/renderer/routes.ts Normal file
View File

@@ -0,0 +1,18 @@
export default {
"/": { sidebarLabel: "Home", heroLabel: "g.s" },
// "/about": { sidebarLabel: "About", heroLabel: "About Me" },
// "/resume": { sidebarLabel: "Resume", heroLabel: "resume" },
// "/demo": { sidebarLabel: "Demo", heroLabel: "Demos" },
// "/services": { sidebarLabel: "Services", heroLabel: "services" },
"/connect": { sidebarLabel: "Connect", heroLabel: "connect" },
"/privacy-policy": {
sidebarLabel: "",
heroLabel: "privacy policy",
hideNav: true,
},
"/terms-of-service": {
sidebarLabel: "",
heroLabel: "terms of service",
hideNav: true,
},
};

18
src/renderer/types.ts Normal file
View File

@@ -0,0 +1,18 @@
// renderer/types.ts
export type { PageProps };
type Page = (pageProps: PageProps) => React.ReactElement;
type PageProps = Record<string, unknown>;
declare global {
namespace Vike {
interface PageContext {
Page: Page;
pageProps?: PageProps;
fetch?: typeof fetch;
// Add your environment bindings here
env: import("../../workers/site/env");
}
}
}

View File

@@ -0,0 +1,22 @@
import React, { useContext } from "react";
import type { PageContext } from "vike/types";
export { PageContextProvider };
export { usePageContext };
const Context = React.createContext<PageContext>(undefined as any);
function PageContextProvider({
pageContext,
children,
}: {
pageContext: PageContext;
children: React.ReactNode;
}) {
return <Context.Provider value={pageContext}>{children}</Context.Provider>;
}
function usePageContext() {
const pageContext = useContext(Context);
return pageContext;
}

View File

@@ -0,0 +1,50 @@
export const resumeData = {
professionalSummary:
"Software engineer and DoD veteran with 10+ years of diverse experiences. " +
"Expertise in cloud technologies, DevOps practices, and full-stack development. " +
"Solid track record of leading high-stakes projects and teams in both military and civilian sectors.",
skills: [
"AI: Retrieval Augmented Generation, Meta, Mistral, OpenAI, Anthropic",
"Cloud: Cloudflare, Google GKE, Amazon AWS Lambda, PCF/TAS, TrueNAS",
"DEVOPS: Pulumi, Helm, Docker",
"CI/CD: Github Actions, Gitlab CI",
"Languages: Typescript, Javascript, Rust, Python, Java, Go",
"Databases: Durable Objects, Postgres, MySQL, Snowflake, Elasticsearch",
"Other: Isolate Compute, WASM Toolchains, Microservice Architectures, GraphQL",
],
experience: [
{
title: "Senior Software Engineer",
company: "Orbis Operations LLC",
timeline: "Mar 2022 - Aug 2024",
description:
"Architected and delivered AI products with a focus in national security.",
},
{
title: "Software Engineer",
company: "Global Analytics Platform",
timeline: "Jan 2020 - Mar 2022",
description:
"Spearheaded development of mission-critical data analytics systems.",
},
{
title: "Software Engineer",
company: "Force Nexus",
timeline: "Sep 2018 - Sep 2020",
description:
"Developed robust, real-time communication systems for special operations.",
},
{
title: "U.S. Army Ranger",
company: "1st Battalion, 75th Ranger Regiment",
timeline: "Jul 2014 - Sep 2018",
description:
"Conducted technical, high-risk operations in diverse environments.",
},
],
education: [
"Ranger Assessment and Selection Program, U.S. Army",
"Basic Leaders Course, U.S. Army",
"(In progress) BAS Computer Science",
],
};

View File

@@ -0,0 +1,37 @@
export const servicesData = {
servicesOverview: `I bring over a decade of experience in software engineering and special operations to deliver effective AI solutions, cloud infrastructure, and mission-critical applications. My background combines technical expertise with a disciplined approach to problem-solving, resulting in scalable and secure solutions for complex business challenges.
Working with my partners, I offer a comprehensive skill set to address demanding technological needs. We focus on practical, high-performance solutions rather than hype. Our approach emphasizes robust engineering and real-world effectiveness.
If you're facing difficult technical challenges and need reliable, efficient solutions, let's discuss how I can help.`,
offerings: [
{
title: "AI Integration and Development Services",
description:
"Leverage advanced AI technologies like Retrieval Augmented Generation to develop custom AI solutions. Expertise with platforms such as OpenAI, Meta, Mistral, and Anthropic ensures state-of-the-art AI integration tailored to your business needs.",
},
{
title: "Cloud Infrastructure and DevOps Consulting",
description:
"Provide comprehensive cloud solutions using platforms like Google GKE, AWS Lambda, and Cloudflare. Implement DevOps best practices with tools like Pulumi, Helm, and Docker to streamline your development pipeline and enhance scalability.",
},
{
title: "Full-Stack Development Services",
description:
"Offer full-stack development expertise across multiple languages including TypeScript, Rust, Python, Java, and Go. Build robust, high-performance applications that meet your specific requirements.",
},
{
title: "Data Analytics and Visualization Solutions",
description:
"Develop mission-critical data analytics systems to unlock valuable insights. Utilize databases like PostgreSQL, MySQL, Snowflake, and Elasticsearch for efficient data management and retrieval.",
},
{
title: "Real-Time Communication Systems",
description:
"Design and implement robust real-time communication systems, drawing from experience in developing solutions for special operations forces. Ensure secure, reliable, and efficient communication channels for your organization.",
},
{
title: "Technical Leadership and Project Management",
description:
"Provide leadership in high-stakes projects, guiding teams through complex technical challenges. Combine military discipline with technical expertise to deliver projects on time and within scope.",
},
],
};

View File

@@ -0,0 +1,25 @@
export const welcome_home_text = `
# welcome!
---
Please enjoy [responsibly](https://en.wikipedia.org/wiki/Guerilla_Open_Access_Manifesto)
<br/>
Checkout my [resume](https://wellfound.com/u/geoff-seemueller)
<br/>
`;
export const welcome_home_tip = `
<!--[ai.seemueller.io](https://ai.seemueller.io)-->
<br/>
<!--[khaos.seemueller.io](https://khaos.seemueller.io) -->
<!--<br/>-->
<!--[rehoboam.seemueller.io](https://rehoboam.seemueller.io)-->
<!--<br/>-->
<!--[khaos.seemueller.io](https://khaos.seemueller.io)-->
`;

View File

@@ -0,0 +1,21 @@
import { types } from "mobx-state-tree";
const AppMenuStateModel = types
.model("AppMenuState", {
isOpen: types.optional(types.boolean, false),
})
.actions((self) => ({
openMenu() {
self.isOpen = true;
},
closeMenu() {
self.isOpen = false;
},
toggleMenu() {
self.isOpen = !self.isOpen;
},
}));
const menuState = AppMenuStateModel.create();
export default menuState;

View File

@@ -0,0 +1,326 @@
import { applySnapshot, flow, Instance, types } from "mobx-state-tree";
import Message from "../models/Message";
import Attachment from "../models/Attachment";
import IntermediateStep from "../models/IntermediateStep";
import { UserOptionsStore } from "./index";
const ClientChatStore = types
.model("ClientChatStore", {
messages: types.optional(types.array(Message), []),
input: types.optional(types.string, ""),
isLoading: types.optional(types.boolean, false),
model: types.optional(types.string, "llama-3.3-70b-versatile"),
imageModel: types.optional(types.string, "black-forest-labs/flux-1.1-pro"),
attachments: types.optional(types.array(Attachment), []),
tools: types.optional(types.array(types.string), []),
intermediateSteps: types.array(IntermediateStep),
})
.views((self) => ({
get getModel() {
return self.model;
},
}))
.actions((self) => ({
cleanup() {
if (self.eventSource) {
self.eventSource.close();
self.eventSource = null;
}
},
sendMessage: flow(function* () {
if (!self.input.trim() || self.isLoading) return;
self.cleanup();
yield self.setFollowModeEnabled(true);
self.setIsLoading(true);
const userMessage = Message.create({
content: self.input,
role: "user" as const,
});
self.addMessage(userMessage);
self.setInput("");
try {
const payload = {
messages: self.messages.slice(),
model: self.model,
attachments: self.attachments.slice(),
tools: self.tools.slice(),
};
yield new Promise((resolve) => setTimeout(resolve, 500));
self.addMessage(Message.create({ content: "", role: "assistant" }));
const response = yield fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (response.status === 429) {
self.updateLastMessage(
`Too many requests in the given time. Please wait a few moments and try again.`,
);
self.cleanup();
return;
}
if (response.status > 200) {
self.updateLastMessage(`Error! Something went wrong, try again.`);
self.cleanup();
return;
}
const { streamUrl } = yield response.json();
self.eventSource = new EventSource(streamUrl);
self.eventSource.onmessage = async (event) => {
try {
const dataString = event.data;
const parsedData = JSON.parse(dataString);
if (parsedData.type === "error") {
self.updateLastMessage(`${parsedData.error}`);
self.cleanup();
self.setIsLoading(false);
return;
}
if (
parsedData.type === "chat" &&
parsedData.data.choices[0]?.finish_reason === "stop"
) {
self.appendToLastMessage(
parsedData.data.choices[0]?.delta?.content || "",
);
self.cleanup();
self.setIsLoading(false);
return;
}
if (parsedData.type === "chat") {
self.appendToLastMessage(
parsedData.data.choices[0]?.delta?.content || "",
);
} else {
self.handleIntermediateSteps(parsedData);
}
} catch (error) {
console.error("Error processing stream:", error);
}
};
self.eventSource.onerror = (e) => {
self.cleanup();
};
} catch (error) {
console.error("Error in sendMessage:", error);
if (
!self.messages.length ||
self.messages[self.messages.length - 1].role !== "assistant"
) {
self.addMessage({
content: "Sorry, there was an error.",
role: "assistant",
});
} else {
self.updateLastMessage("Sorry, there was an error.");
}
self.cleanup();
self.setIsLoading(false);
} finally {
}
}),
setFollowModeEnabled: flow(function* (isEnabled: boolean) {
yield UserOptionsStore.setFollowModeEnabled(isEnabled);
}),
setInput(value: string) {
self.input = value;
},
setModel(value: string) {
self.model = value;
try {
localStorage.setItem("recentModel", value);
} catch (error) {}
},
setImageModel(value: string) {
self.imageModel = value;
},
addMessage(message: Instance<typeof Message>) {
self.messages.push(message);
},
editMessage: flow(function* (index: number, content: string) {
yield self.setFollowModeEnabled(true);
if (index >= 0 && index < self.messages.length) {
self.messages[index].setContent(content);
self.messages.splice(index + 1);
self.setIsLoading(true);
yield new Promise((resolve) => setTimeout(resolve, 500));
self.addMessage(Message.create({ content: "", role: "assistant" }));
try {
const payload = {
messages: self.messages.slice(),
model: self.model,
attachments: self.attachments.slice(),
tools: self.tools.slice(),
};
const response = yield fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (response.status === 429) {
self.updateLastMessage(
`Too many requests in the given time. Please wait a few moments and try again.`,
);
self.cleanup();
return;
}
if (response.status > 200) {
self.updateLastMessage(`Error! Something went wrong, try again.`);
self.cleanup();
return;
}
const { streamUrl } = yield response.json();
self.eventSource = new EventSource(streamUrl);
self.eventSource.onmessage = (event) => {
try {
const dataString = event.data;
const parsedData = JSON.parse(dataString);
if (parsedData.type === "error") {
self.updateLastMessage(`${parsedData.error}`);
self.cleanup();
self.setIsLoading(false);
return;
}
if (
parsedData.type === "chat" &&
parsedData.data.choices[0]?.finish_reason === "stop"
) {
self.cleanup();
self.setIsLoading(false);
return;
}
if (parsedData.type === "chat") {
self.appendToLastMessage(
parsedData.data.choices[0]?.delta?.content || "",
);
} else {
self.handleIntermediateSteps(parsedData);
}
} catch (error) {
console.error("Error processing stream:", error);
} finally {
}
};
self.eventSource.onerror = (e) => {
console.log("EventSource encountered an error", JSON.stringify(e));
};
} catch (error) {
console.error("Error in editMessage:", error);
self.addMessage({
content: "Sorry, there was an error.",
role: "assistant",
});
self.cleanup();
} finally {
}
}
}),
getIsLoading() {
return self.isLoading;
},
reset() {
applySnapshot(self, {});
},
addAttachment(attachment: Instance<typeof Attachment>) {
self.attachments.push(attachment);
if (self.attachments.length > 0) {
if (!self.tools.includes("user-attachments")) {
self.tools.push("user-attachments");
}
}
},
addIntermediateStep(stepData) {
return;
},
handleIntermediateSteps(eventData: any) {
try {
self.appendToLastMessage(eventData?.data.trim() + "\n");
self.addIntermediateStep({
kind: eventData.type,
data: eventData.data,
});
} catch (e) {}
},
removeMessagesAfter(index: number) {
if (index >= 0 && index < self.messages.length) {
self.messages.splice(index + 1);
}
},
removeAttachment(url: string) {
const f =
self.attachments.filter((attachment) => attachment.url !== url) ?? [];
self.attachments.clear();
self.attachments.push(...f);
if (self.attachments.length === 0) {
const remainingTools = self.tools.filter(
(tool) => tool !== "user-attachments",
);
self.tools.clear();
self.tools.push(...remainingTools);
}
},
setTools(tools: string[]) {
self.tools.clear();
self.tools.push(...tools);
},
getTools() {
return self.tools.slice();
},
updateLastMessage(content: string) {
if (self.messages.length > 0) {
self.messages[self.messages.length - 1].content = content;
}
},
appendToLastMessage(content: string) {
if (self.messages.length > 0) {
self.messages[self.messages.length - 1].content += content;
}
},
setIsLoading(value: boolean) {
self.isLoading = value;
},
stopIncomingMessage() {
if (self.eventSource) {
self.eventSource.close();
self.eventSource = null;
}
self.setIsLoading(false);
},
}));
export type IMessage = Instance<typeof Message>;
export type IClientChatStore = Instance<typeof this>;
export default ClientChatStore.create();

View File

@@ -0,0 +1,90 @@
import { types, flow } from "mobx-state-tree";
const ClientFeedbackStore = types
.model("ClientFeedbackStore", {
input: types.optional(types.string, ""),
isLoading: types.optional(types.boolean, false),
isSubmitted: types.optional(types.boolean, false),
error: types.optional(types.string, ""),
})
.actions((self) => {
const setError = (error) => {
self.error = error;
};
const setInput = (value) => {
self.input = value;
if (self.error) {
setError("");
}
};
const reset = () => {
self.input = "";
self.isLoading = false;
self.isSubmitted = false;
self.error = "";
};
const validateInput = () => {
if (!self.input.trim()) {
setError("Feedback cannot be empty.");
return false;
}
if (self.input.length > 500) {
setError("Feedback cannot exceed 500 characters.");
return false;
}
setError("");
return true;
};
const submitFeedback = flow(function* () {
if (!validateInput()) {
return false;
}
self.isLoading = true;
try {
const response = yield fetch("/api/feedback", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ feedback: self.input }),
});
if (!response.ok) {
throw new Error(`Error: ${response.status} - ${response.statusText}`);
}
self.isSubmitted = true;
self.input = "";
return true;
} catch (error) {
console.error(error);
setError(
error.message || "An error occurred while submitting feedback.",
);
return false;
} finally {
self.isLoading = false;
}
});
return {
setInput,
setError,
reset,
validateInput,
submitFeedback,
};
});
const feedbackStore = ClientFeedbackStore.create();
export default feedbackStore;

View File

@@ -0,0 +1,76 @@
import { types, flow } from "mobx-state-tree";
const ClientTransactionStore = types
.model("ClientTransactionStore", {
selectedMethod: types.string,
depositAddress: types.maybeNull(types.string),
amount: types.optional(types.string, ""),
donerId: types.optional(types.string, ""),
userConfirmed: types.optional(types.boolean, false),
txId: types.optional(types.string, ""),
})
.actions((self) => ({
setSelectedMethod(method: string) {
self.selectedMethod = method;
self.userConfirmed = false;
},
setAmount(value: string) {
self.amount = value;
},
setDonerId(value: string) {
self.donerId = value;
},
confirmUser() {
self.userConfirmed = true;
},
setTransactionId(txId: string) {
self.txId = txId;
},
setDepositAddress(address: string) {
self.depositAddress = address;
},
resetTransaction() {
self.txId = "";
self.depositAddress = null;
self.userConfirmed = false;
},
prepareTransaction: flow(function* () {
if (!self.amount || !self.donerId || parseInt(self.amount) <= 0) {
throw new Error("Invalid donation data");
}
const currency = self.selectedMethod.toLowerCase();
try {
const response = yield fetch("/api/tx", {
method: "POST",
body: ["PREPARE_TX", self.donerId, currency, self.amount]
.join(",")
.trim(),
});
if (!response.ok) throw new Error("Failed to prepare transaction");
const txData = yield response.json();
let finalDepositAddress = txData.depositAddress;
if (currency === "ethereum") {
finalDepositAddress = "0x" + finalDepositAddress;
}
self.setTransactionId(txData.txKey);
self.setDepositAddress(finalDepositAddress);
self.confirmUser();
} catch (error) {
console.error("Transaction preparation failed:", error);
throw error;
}
}),
}));
export default ClientTransactionStore.create({
selectedMethod: "Ethereum",
depositAddress: null,
amount: "",
donerId: "",
userConfirmed: false,
transactionId: "",
});

View File

@@ -0,0 +1,67 @@
import { types, flow } from "mobx-state-tree";
import clientChatStore from "./ClientChatStore";
import Attachment from "../models/Attachment";
const FileUploadStore = types
.model("FileUploadStore", {
isUploading: types.optional(types.boolean, false),
uploadError: types.maybeNull(types.string),
uploadedFiles: types.array(types.string),
uploadResults: types.array(types.frozen()),
})
.actions((self) => ({
uploadFile: flow(function* (file: File, endpoint: string) {
if (!endpoint) {
self.uploadError = "Endpoint URL is required.";
return;
}
self.isUploading = true;
self.uploadError = null;
const formData = new FormData();
formData.append("file", file);
try {
const response = yield fetch(endpoint, {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Upload failed with status: ${response.status}`);
}
const result = yield response.json();
self.uploadResults.push(result);
if (result.url) {
self.uploadedFiles.push(result.url);
clientChatStore.addAttachment(
Attachment.create({
content: `${file.name}\n~~~${result?.extractedText}\n`,
url: result.url,
}),
);
} else {
throw new Error("No URL returned from the server.");
}
} catch (error: any) {
self.uploadError = error.message;
} finally {
self.isUploading = false;
}
}),
removeUploadedFile(url: string) {
clientChatStore.removeAttachment(url);
const index = self.uploadedFiles.findIndex(
(uploadedUrl) => uploadedUrl === url,
);
if (index !== -1) {
self.uploadedFiles.splice(index, 1);
self.uploadResults.splice(index, 1);
}
},
}));
export default FileUploadStore.create();

View File

@@ -0,0 +1,100 @@
import { flow, types } from "mobx-state-tree";
import ClientChatStore from "./ClientChatStore";
import { runInAction } from "mobx";
import Cookies from "js-cookie";
const UserOptionsStore = types
.model("UserOptionsStore", {
followModeEnabled: types.optional(types.boolean, false),
theme: types.optional(types.string, "darknight"),
text_model: types.optional(types.string, "llama-3.3-70b-versatile"),
})
.actions((self) => ({
getFollowModeEnabled: flow(function* () {
return self.followModeEnabled;
}),
storeUserOptions() {
const userOptionsCookie = document.cookie
.split(";")
.find((row) => row.startsWith("user_preferences"));
console.log(document.cookie.split(";"));
const newUserOptions = JSON.stringify({
theme: self.theme,
text_model: self.text_model,
});
const encodedUserPreferences = btoa(newUserOptions);
const oldUserOptions = userOptionsCookie
? atob(userOptionsCookie.split("=")[1])
: null;
runInAction(() => {
Cookies.set("user_preferences", encodedUserPreferences);
});
},
initialize: flow(function* () {
const userPreferencesCoookie = document.cookie
.split(";")
.find((row) => row.startsWith("user_preferences"));
if (!userPreferencesCoookie) {
console.log("No user preferences cookie found, creating one");
self.storeUserOptions();
}
if (userPreferencesCoookie) {
const userPreferences = JSON.parse(
atob(userPreferencesCoookie.split("=")[1]),
);
self.theme = userPreferences.theme;
self.text_model = userPreferences.text_model;
}
window.addEventListener("scroll", async () => {
if (ClientChatStore.isLoading && self.followModeEnabled) {
console.log("scrolling");
await self.setFollowModeEnabled(false);
}
});
window.addEventListener("wheel", async () => {
if (ClientChatStore.isLoading && self.followModeEnabled) {
console.log("wheel");
await self.setFollowModeEnabled(false);
}
});
window.addEventListener("touchmove", async () => {
console.log("touchmove");
if (ClientChatStore.isLoading && self.followModeEnabled) {
await self.setFollowModeEnabled(false);
}
});
window.addEventListener("mousedown", async () => {
if (ClientChatStore.isLoading && self.followModeEnabled) {
await self.setFollowModeEnabled(false);
}
});
}),
deleteCookie() {
document.cookie = "user_preferences=; max-age=; path=/;";
},
setFollowModeEnabled: flow(function* (followMode: boolean) {
self.followModeEnabled = followMode;
}),
toggleFollowMode: flow(function* () {
self.followModeEnabled = !self.followModeEnabled;
}),
selectTheme: flow(function* (theme: string) {
self.theme = theme;
self.storeUserOptions();
}),
}));
const userOptionsStore = UserOptionsStore.create();
export default userOptionsStore;

15
src/stores/index.ts Normal file
View File

@@ -0,0 +1,15 @@
import AppMenuStore from "./AppMenuStore";
import ClientChatStore from "./ClientChatStore";
import ClientFeedbackStore from "./ClientFeedbackStore";
import ClientTransactionStore from "./ClientTransactionStore";
import FileUploadStore from "./FileUploadStore";
import UserOptionsStore from "./UserOptionsStore";
export {
AppMenuStore,
ClientChatStore,
ClientFeedbackStore,
ClientTransactionStore,
FileUploadStore,
UserOptionsStore,
};