mirror of
https://github.com/DustinBrett/daedalOS.git
synced 2025-12-06 00:20:05 +01:00
Added AI via Prompt API & WebLLM
This commit is contained in:
parent
8e35a879a7
commit
983fc5f6fc
|
|
@ -7,7 +7,7 @@ import { useProcesses } from "contexts/process";
|
|||
import { loadFiles } from "utils/functions";
|
||||
import { useLinkHandler } from "hooks/useLinkHandler";
|
||||
|
||||
type MarkedOptions = {
|
||||
export type MarkedOptions = {
|
||||
headerIds: boolean;
|
||||
mangle: boolean;
|
||||
};
|
||||
|
|
|
|||
36
components/system/Taskbar/AI/AIButton.tsx
Normal file
36
components/system/Taskbar/AI/AIButton.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import {
|
||||
AI_STAGE,
|
||||
AI_TITLE,
|
||||
WINDOW_ID,
|
||||
} from "components/system/Taskbar/AI/constants";
|
||||
import { AIIcon } from "components/system/Taskbar/AI/icons";
|
||||
import StyledAIButton from "components/system/Taskbar/AI/StyledAIButton";
|
||||
import { DIV_BUTTON_PROPS } from "utils/constants";
|
||||
import { label } from "utils/functions";
|
||||
import useTaskbarContextMenu from "components/system/Taskbar/useTaskbarContextMenu";
|
||||
import { useSession } from "contexts/session";
|
||||
|
||||
type AIButtonProps = {
|
||||
aiVisible: boolean;
|
||||
toggleAI: () => void;
|
||||
};
|
||||
|
||||
const AIButton: FC<AIButtonProps> = ({ aiVisible, toggleAI }) => {
|
||||
const { removeFromStack } = useSession();
|
||||
|
||||
return (
|
||||
<StyledAIButton
|
||||
onClick={() => {
|
||||
toggleAI();
|
||||
if (aiVisible) removeFromStack(WINDOW_ID);
|
||||
}}
|
||||
{...DIV_BUTTON_PROPS}
|
||||
{...label(`${AI_TITLE} (${AI_STAGE})`)}
|
||||
{...useTaskbarContextMenu()}
|
||||
>
|
||||
<AIIcon />
|
||||
</StyledAIButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIButton;
|
||||
417
components/system/Taskbar/AI/AIChat.tsx
Normal file
417
components/system/Taskbar/AI/AIChat.tsx
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
import { useTheme } from "styled-components";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
formatWebLlmProgress,
|
||||
speakMessage,
|
||||
} from "components/system/Taskbar/AI/functions";
|
||||
import {
|
||||
AIIcon,
|
||||
ChatIcon,
|
||||
CopyIcon,
|
||||
EditIcon,
|
||||
PersonIcon,
|
||||
SendFilledIcon,
|
||||
SendIcon,
|
||||
SpeakIcon,
|
||||
StopIcon,
|
||||
WarningIcon,
|
||||
} from "components/system/Taskbar/AI/icons";
|
||||
import useAITransition from "components/system/Taskbar/AI/useAITransition";
|
||||
import {
|
||||
AI_STAGE,
|
||||
AI_TITLE,
|
||||
AI_WORKER,
|
||||
DEFAULT_CONVO_STYLE,
|
||||
WINDOW_ID,
|
||||
} from "components/system/Taskbar/AI/constants";
|
||||
import StyledAIChat from "components/system/Taskbar/AI/StyledAIChat";
|
||||
import { CloseIcon } from "components/system/Window/Titlebar/WindowActionIcons";
|
||||
import Button from "styles/common/Button";
|
||||
import { label, viewWidth } from "utils/functions";
|
||||
import { PREVENT_SCROLL } from "utils/constants";
|
||||
import {
|
||||
type MessageTypes,
|
||||
type ConvoStyles,
|
||||
type Message,
|
||||
type WorkerResponse,
|
||||
type WebLlmProgress,
|
||||
type AIResponse,
|
||||
} from "components/system/Taskbar/AI/types";
|
||||
import useWorker from "hooks/useWorker";
|
||||
import useFocusable from "components/system/Window/useFocusable";
|
||||
import { useSession } from "contexts/session";
|
||||
import { useWindowAI } from "hooks/useWindowAI";
|
||||
|
||||
type AIChatProps = {
|
||||
toggleAI: () => void;
|
||||
};
|
||||
|
||||
const AIChat: FC<AIChatProps> = ({ toggleAI }) => {
|
||||
const {
|
||||
colors: { taskbar: taskbarColor },
|
||||
sizes: { taskbar: taskbarSize },
|
||||
} = useTheme();
|
||||
const getFullWidth = useCallback(
|
||||
() => Math.min(taskbarSize.ai.chatWidth, viewWidth()),
|
||||
[taskbarSize.ai.chatWidth]
|
||||
);
|
||||
const [fullWidth, setFullWidth] = useState(getFullWidth);
|
||||
const aiTransition = useAITransition(fullWidth);
|
||||
const [convoStyle, setConvoStyle] = useState(DEFAULT_CONVO_STYLE);
|
||||
const [primaryColor, secondaryColor, tertiaryColor] =
|
||||
taskbarColor.ai[convoStyle];
|
||||
const [promptText, setPromptText] = useState("");
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
const typing = promptText.length > 0;
|
||||
const [conversation, setConversation] = useState<Message[]>([]);
|
||||
const addMessage = useCallback(
|
||||
(
|
||||
text: string | undefined,
|
||||
type: MessageTypes,
|
||||
formattedText?: string
|
||||
): void => {
|
||||
if (text) {
|
||||
setConversation((prevMessages) => [
|
||||
...prevMessages,
|
||||
{ formattedText: formattedText || text, text, type },
|
||||
]);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
const addUserPrompt = useCallback(() => {
|
||||
if (promptText) {
|
||||
addMessage(promptText, "user");
|
||||
(textAreaRef.current as HTMLTextAreaElement).value = "";
|
||||
setPromptText("");
|
||||
}
|
||||
}, [addMessage, promptText]);
|
||||
const lastAiMessageIndex = useMemo(
|
||||
() =>
|
||||
conversation.length -
|
||||
[...conversation].reverse().findIndex(({ type }) => type === "ai") -
|
||||
1,
|
||||
[conversation]
|
||||
);
|
||||
const [responding, setResponding] = useState(false);
|
||||
const [canceling, setCanceling] = useState(false);
|
||||
const [failedSession, setFailedSession] = useState(false);
|
||||
const sessionIdRef = useRef<number>(0);
|
||||
const hasWindowAI = useWindowAI();
|
||||
const aiWorker = useWorker<void>(AI_WORKER);
|
||||
const stopResponse = useCallback(() => {
|
||||
if (aiWorker.current && responding) {
|
||||
aiWorker.current.postMessage("cancel");
|
||||
setCanceling(true);
|
||||
}
|
||||
}, [aiWorker, responding]);
|
||||
const newTopic = useCallback(() => {
|
||||
stopResponse();
|
||||
sessionIdRef.current = 0;
|
||||
setConversation([]);
|
||||
setFailedSession(false);
|
||||
}, [stopResponse]);
|
||||
const changeConvoStyle = useCallback(
|
||||
(newConvoStyle: ConvoStyles) => {
|
||||
if (convoStyle !== newConvoStyle) {
|
||||
newTopic();
|
||||
setConvoStyle(newConvoStyle);
|
||||
textAreaRef.current?.focus(PREVENT_SCROLL);
|
||||
}
|
||||
},
|
||||
[convoStyle, newTopic]
|
||||
);
|
||||
const [containerElement, setContainerElement] =
|
||||
useState<HTMLElement | null>();
|
||||
const { removeFromStack } = useSession();
|
||||
const { zIndex, ...focusableProps } = useFocusable(
|
||||
WINDOW_ID,
|
||||
undefined,
|
||||
containerElement
|
||||
);
|
||||
const scrollbarVisible = useMemo(
|
||||
() =>
|
||||
conversation.length > 0 &&
|
||||
sectionRef.current instanceof HTMLElement &&
|
||||
sectionRef.current.scrollHeight > sectionRef.current.clientHeight,
|
||||
[conversation.length]
|
||||
);
|
||||
const [copiedIndex, setCopiedIndex] = useState(-1);
|
||||
const [progressMessage, setProgressMessage] = useState<string>("");
|
||||
const autoSizeText = useCallback(() => {
|
||||
const textArea = textAreaRef.current as HTMLTextAreaElement;
|
||||
|
||||
textArea.style.height = "auto";
|
||||
textArea.style.height = `${textArea.scrollHeight}px`;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
textAreaRef.current?.focus(PREVENT_SCROLL);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const updateFullWidth = (): void => setFullWidth(getFullWidth);
|
||||
|
||||
window.addEventListener("resize", updateFullWidth);
|
||||
|
||||
return () => window.removeEventListener("resize", updateFullWidth);
|
||||
}, [getFullWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversation.length > 0 || failedSession) {
|
||||
requestAnimationFrame(() =>
|
||||
sectionRef.current?.scrollTo({
|
||||
behavior: "smooth",
|
||||
top: sectionRef.current.scrollHeight,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [conversation, failedSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
aiWorker.current &&
|
||||
conversation.length > 0 &&
|
||||
conversation[conversation.length - 1].type === "user"
|
||||
) {
|
||||
const { text } = conversation[conversation.length - 1];
|
||||
|
||||
setResponding(true);
|
||||
|
||||
sessionIdRef.current ||= Date.now();
|
||||
|
||||
aiWorker.current.postMessage({
|
||||
hasWindowAI,
|
||||
id: sessionIdRef.current,
|
||||
style: convoStyle,
|
||||
text,
|
||||
});
|
||||
}
|
||||
}, [aiWorker, conversation, convoStyle, hasWindowAI]);
|
||||
|
||||
useEffect(() => {
|
||||
const workerRef = aiWorker.current;
|
||||
const workerResponse = ({ data }: WorkerResponse): void => {
|
||||
const doneResponding = typeof data === "string" || "response" in data;
|
||||
|
||||
setResponding(!doneResponding);
|
||||
|
||||
if (data === "canceled") {
|
||||
setCanceling(false);
|
||||
} else if ((data as WebLlmProgress).progress) {
|
||||
const {
|
||||
progress: { text },
|
||||
} = data as WebLlmProgress;
|
||||
|
||||
setProgressMessage(formatWebLlmProgress(text));
|
||||
} else if ((data as AIResponse).response) {
|
||||
const { formattedResponse, response } = data as AIResponse;
|
||||
|
||||
addMessage(response, "ai", formattedResponse);
|
||||
} else if ((data as AIResponse).response === "") {
|
||||
setFailedSession(true);
|
||||
}
|
||||
};
|
||||
|
||||
workerRef?.addEventListener("message", workerResponse);
|
||||
|
||||
return () => workerRef?.removeEventListener("message", workerResponse);
|
||||
}, [addMessage, aiWorker]);
|
||||
|
||||
return (
|
||||
<StyledAIChat
|
||||
ref={setContainerElement}
|
||||
$primaryColor={primaryColor}
|
||||
$scrollbarVisible={scrollbarVisible}
|
||||
$secondaryColor={secondaryColor}
|
||||
$tertiaryColor={tertiaryColor}
|
||||
$typing={typing}
|
||||
$width={fullWidth}
|
||||
$zIndex={zIndex}
|
||||
{...aiTransition}
|
||||
{...focusableProps}
|
||||
>
|
||||
<div className="header">
|
||||
<header>
|
||||
{`${AI_TITLE} (${AI_STAGE})`}
|
||||
<nav>
|
||||
<Button
|
||||
className="close"
|
||||
onClick={() => {
|
||||
toggleAI();
|
||||
removeFromStack(WINDOW_ID);
|
||||
}}
|
||||
{...label("Close")}
|
||||
>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
<section ref={sectionRef}>
|
||||
<div className="convo-header">
|
||||
<div className="title">
|
||||
<AIIcon /> {AI_TITLE}
|
||||
</div>
|
||||
<div className="convo-style">
|
||||
Choose a conversation style
|
||||
<div className="buttons">
|
||||
<button
|
||||
className={convoStyle === "creative" ? "selected" : ""}
|
||||
onClick={() => changeConvoStyle("creative")}
|
||||
type="button"
|
||||
{...label("Start an original and imaginative chat")}
|
||||
>
|
||||
<h4>More</h4>
|
||||
<h2>Creative</h2>
|
||||
</button>
|
||||
<button
|
||||
className={convoStyle === "balanced" ? "selected" : ""}
|
||||
onClick={() => changeConvoStyle("balanced")}
|
||||
type="button"
|
||||
{...label("For everyday, informed chats")}
|
||||
>
|
||||
<h4>More</h4>
|
||||
<h2>Balanced</h2>
|
||||
</button>
|
||||
<button
|
||||
className={convoStyle === "precise" ? "selected" : ""}
|
||||
onClick={() => changeConvoStyle("precise")}
|
||||
type="button"
|
||||
{...label("Start a concise chat, useful for fact-finding")}
|
||||
>
|
||||
<h4>More</h4>
|
||||
<h2>Precise</h2>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="conversation">
|
||||
{conversation.map(({ formattedText, type, text }, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={index} className={type}>
|
||||
{(index === 0 || conversation[index - 1].type !== type) && (
|
||||
<div className="avatar">
|
||||
{type === "user" ? <PersonIcon /> : <AIIcon />}
|
||||
{type === "user" ? "You" : "AI"}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: formattedText }}
|
||||
className="message"
|
||||
/>
|
||||
<div
|
||||
className={`controls${index === lastAiMessageIndex ? " last" : ""}${responding && index === conversation.length - 1 ? " hidden" : ""}`}
|
||||
>
|
||||
<button
|
||||
className="copy"
|
||||
onClick={() => {
|
||||
navigator.clipboard?.writeText(text);
|
||||
setCopiedIndex(index);
|
||||
setTimeout(() => setCopiedIndex(-1), 5000);
|
||||
}}
|
||||
type="button"
|
||||
{...label(copiedIndex === index ? "Copied" : "Copy")}
|
||||
>
|
||||
<CopyIcon />
|
||||
</button>
|
||||
{type === "user" && (
|
||||
<button
|
||||
className="edit"
|
||||
onClick={() => {
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.value = text;
|
||||
textAreaRef.current.focus(PREVENT_SCROLL);
|
||||
setPromptText(text);
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
{...label("Edit")}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
)}
|
||||
{"speechSynthesis" in window && type === "ai" && (
|
||||
<button
|
||||
className="speak"
|
||||
onClick={() => speakMessage(text)}
|
||||
type="button"
|
||||
{...label("Read aloud")}
|
||||
>
|
||||
<SpeakIcon />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{responding && (
|
||||
<div className="responding">
|
||||
<button
|
||||
className={`stop${canceling ? " canceling" : ""}`}
|
||||
disabled={Boolean(progressMessage) || canceling}
|
||||
onClick={stopResponse}
|
||||
type="button"
|
||||
>
|
||||
{!progressMessage && !canceling && <StopIcon />}
|
||||
{canceling ? "Canceling" : progressMessage || "Stop Responding"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{failedSession && (
|
||||
<div className="failed-session">
|
||||
<WarningIcon />
|
||||
It might be time to move onto a new topic.
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a onClick={newTopic}>Let's start over.</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
disabled={failedSession}
|
||||
onBlur={autoSizeText}
|
||||
onChange={(event) => {
|
||||
setPromptText(event.target.value);
|
||||
autoSizeText();
|
||||
}}
|
||||
onFocus={autoSizeText}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!canceling && !responding) addUserPrompt();
|
||||
}
|
||||
|
||||
autoSizeText();
|
||||
}}
|
||||
placeholder="Ask me anything..."
|
||||
/>
|
||||
<button
|
||||
className="new-topic"
|
||||
onClick={newTopic}
|
||||
type="button"
|
||||
{...label("New topic")}
|
||||
>
|
||||
<ChatIcon />
|
||||
</button>
|
||||
<button
|
||||
className="submit"
|
||||
disabled={canceling || responding}
|
||||
{...(typing && {
|
||||
onClick: addUserPrompt,
|
||||
})}
|
||||
type="button"
|
||||
{...(!canceling && typing ? label("Submit") : undefined)}
|
||||
>
|
||||
{!canceling && typing ? <SendFilledIcon /> : <SendIcon />}
|
||||
</button>
|
||||
</footer>
|
||||
</StyledAIChat>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIChat;
|
||||
26
components/system/Taskbar/AI/StyledAIButton.ts
Normal file
26
components/system/Taskbar/AI/StyledAIButton.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
const StyledAIButton = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: ${({ theme }) => theme.sizes.taskbar.ai.buttonWidth};
|
||||
|
||||
svg {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.colors.taskbar.hover};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: ${({ theme }) => theme.colors.taskbar.foreground};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledAIButton;
|
||||
490
components/system/Taskbar/AI/StyledAIChat.ts
Normal file
490
components/system/Taskbar/AI/StyledAIChat.ts
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
import { m as motion } from "framer-motion";
|
||||
import styled from "styled-components";
|
||||
import { TASKBAR_HEIGHT } from "utils/constants";
|
||||
|
||||
type StyledAIChatProps = {
|
||||
$primaryColor: string;
|
||||
$scrollbarVisible: boolean;
|
||||
$secondaryColor: string;
|
||||
$tertiaryColor: string;
|
||||
$typing: boolean;
|
||||
$width: number;
|
||||
$zIndex: number;
|
||||
};
|
||||
|
||||
const StyledAIChat = styled(motion.section)<StyledAIChatProps>`
|
||||
background-color: rgb(32, 32, 32);
|
||||
border-left: 1px solid rgb(104, 104, 104);
|
||||
bottom: ${TASKBAR_HEIGHT}px;
|
||||
color: rgb(200, 200, 200);
|
||||
font-size: 14px;
|
||||
height: calc(100% - ${TASKBAR_HEIGHT}px);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: ${({ $zIndex }) => $zIndex};
|
||||
|
||||
section {
|
||||
&::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-clip: content-box;
|
||||
background-color: rgb(77, 77, 77);
|
||||
border: 6px solid transparent;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgb(121, 121, 121);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 49px - 122px);
|
||||
min-width: ${({ $width }) => $width - 1}px;
|
||||
overflow: hidden auto;
|
||||
place-content: space-between;
|
||||
|
||||
.convo-header {
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
width: ${({ $scrollbarVisible }) =>
|
||||
$scrollbarVisible ? "100%" : "calc(100% + 13px)"};
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
|
||||
> svg {
|
||||
height: 34px;
|
||||
margin-right: 8px;
|
||||
margin-top: 3px;
|
||||
width: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.convo-style {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 17px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
|
||||
.buttons {
|
||||
border: 1px solid rgb(102, 102, 102);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
margin-bottom: 48px;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100px;
|
||||
min-width: 100px;
|
||||
padding: 7px 28px;
|
||||
place-items: center;
|
||||
|
||||
&.selected {
|
||||
background: ${({ $secondaryColor, $tertiaryColor }) =>
|
||||
`linear-gradient(135deg, ${$secondaryColor} 0%, ${$tertiaryColor} 100%)`};
|
||||
}
|
||||
}
|
||||
|
||||
h2,
|
||||
h4 {
|
||||
font-weight: 400;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 9.5px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation {
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 13.5px;
|
||||
gap: 13px;
|
||||
letter-spacing: 0.2px;
|
||||
padding: 16px;
|
||||
padding-bottom: 5px;
|
||||
|
||||
.user {
|
||||
.avatar {
|
||||
.person {
|
||||
background: ${({ $tertiaryColor }) => $tertiaryColor};
|
||||
border-radius: 50%;
|
||||
fill: rgb(255, 255, 255, 45%);
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
padding-bottom: 12px;
|
||||
place-items: center;
|
||||
|
||||
svg {
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
cursor: text;
|
||||
padding-left: 36px;
|
||||
user-select: text;
|
||||
white-space: pre-line;
|
||||
|
||||
* {
|
||||
cursor: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: rgb(26, 26, 26);
|
||||
border: 1px solid rgb(48, 48, 48);
|
||||
border-radius: 5px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.language-js,
|
||||
&.language-python {
|
||||
&::before {
|
||||
background-color: rgb(29, 29, 29);
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
display: flex;
|
||||
font-family: ${({ theme }) => theme.formats.systemFont};
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
height: 40px;
|
||||
left: -12px;
|
||||
padding: 0 12px;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
top: -12px;
|
||||
width: calc(100% + 24px);
|
||||
}
|
||||
}
|
||||
|
||||
&.language-js::before {
|
||||
content: "JavaScript";
|
||||
}
|
||||
|
||||
&.language-python::before {
|
||||
content: "Python";
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
+ .controls button {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
pre:last-child {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.responding {
|
||||
display: flex;
|
||||
margin-top: -6px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
width: calc(100% + 13px);
|
||||
|
||||
.stop {
|
||||
background-color: rgb(45, 45, 45);
|
||||
border: ${({ $primaryColor }) => `1px solid ${$primaryColor}`};
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.3px;
|
||||
min-height: 36px;
|
||||
padding: 8px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(50, 50, 50);
|
||||
}
|
||||
|
||||
&.canceling {
|
||||
background-color: rgb(42, 42, 42);
|
||||
}
|
||||
|
||||
.stop-icon {
|
||||
fill: ${({ $primaryColor }) => $primaryColor};
|
||||
height: 18px;
|
||||
margin-right: 6px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.failed-session {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
width: ${({ $scrollbarVisible }) =>
|
||||
$scrollbarVisible ? "100%" : "calc(100% + 13px)"};
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
border-bottom: 1px solid rgb(48, 48, 48);
|
||||
content: "";
|
||||
flex: 1 1 0%;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-inline-start: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: ${({ $primaryColor }) => $primaryColor};
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
fill: ${({ $primaryColor }) => $primaryColor};
|
||||
height: 18px;
|
||||
margin-right: 4px;
|
||||
margin-top: 2px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding-left: 36px;
|
||||
padding-top: 11px;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.copy,
|
||||
.edit,
|
||||
.speak {
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
height: 32px;
|
||||
visibility: hidden;
|
||||
width: 32px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(45, 45, 45);
|
||||
border: 1px solid rgb(65, 65, 65);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgb(42, 42, 42);
|
||||
}
|
||||
|
||||
.copy-icon,
|
||||
.edit-icon,
|
||||
.speak-icon {
|
||||
fill: #fff;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.last {
|
||||
.copy,
|
||||
.edit,
|
||||
.speak {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 49px;
|
||||
min-width: ${({ $width }) => $width - 1}px;
|
||||
padding: 14px 15px 16px;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 3px 1px;
|
||||
|
||||
nav {
|
||||
position: relative;
|
||||
right: -8px;
|
||||
top: -9px;
|
||||
|
||||
.close {
|
||||
border-radius: 5px;
|
||||
height: 36px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
width: 36px;
|
||||
|
||||
> svg {
|
||||
fill: rgb(241, 241, 241);
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(49, 49, 49);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 122px;
|
||||
min-width: ${({ $width }) => $width - 1}px;
|
||||
padding: 16px 14px;
|
||||
place-content: space-between;
|
||||
position: absolute;
|
||||
|
||||
.new-topic {
|
||||
background: ${({ $secondaryColor, $tertiaryColor }) =>
|
||||
`linear-gradient(135deg, ${$secondaryColor} 0%, ${$tertiaryColor} 100%)`};
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
transition: opacity 0.1s 0.1s ease-in-out;
|
||||
width: 40px;
|
||||
|
||||
> .chat {
|
||||
fill: #fff;
|
||||
height: 20px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 1px solid rgb(32, 32, 32);
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
height: 36px;
|
||||
position: relative;
|
||||
right: 5px;
|
||||
top: 49px;
|
||||
width: 36px;
|
||||
|
||||
.send {
|
||||
fill: ${({ $primaryColor, $typing }) =>
|
||||
$typing ? $primaryColor : "rgb(99, 99, 99)"};
|
||||
height: 20px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ $typing }) =>
|
||||
$typing ? "rgb(44, 44, 44)" : undefined};
|
||||
cursor: ${({ $typing }) => ($typing ? "pointer" : undefined)};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgb(38, 38, 38);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: rgb(31, 31, 31);
|
||||
border: 1px solid rgb(102, 102, 102);
|
||||
border-radius: 7px;
|
||||
bottom: 16px;
|
||||
color: #fff;
|
||||
font-family: ${({ theme }) => theme.formats.systemFont};
|
||||
font-size: 12.5px;
|
||||
letter-spacing: 0.6px;
|
||||
min-height: 90px;
|
||||
overflow: hidden;
|
||||
padding: 13px 44px 13px 15px;
|
||||
position: absolute;
|
||||
resize: none;
|
||||
right: 14px;
|
||||
transition:
|
||||
border-bottom 0.2s 0.2s ease-in-out,
|
||||
width 0.2s 0.2s ease-in-out;
|
||||
width: calc(100% - 80px);
|
||||
|
||||
&::placeholder {
|
||||
color: rgb(206, 206, 206);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-bottom: ${({ $primaryColor }) => `2px solid ${$primaryColor}`};
|
||||
width: ${({ $typing }) => ($typing ? "calc(100% - 28px)" : undefined)};
|
||||
|
||||
& + .new-topic {
|
||||
opacity: ${({ $typing }) => ($typing ? "0%" : "100%")};
|
||||
transition-delay: ${({ $typing }) => ($typing ? 0.2 : 0.4)}s;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:focus) + .new-topic {
|
||||
transition-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledAIChat;
|
||||
152
components/system/Taskbar/AI/ai.worker.ts
Normal file
152
components/system/Taskbar/AI/ai.worker.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import {
|
||||
type ChatCompletionMessageParam,
|
||||
type MLCEngine,
|
||||
} from "@mlc-ai/web-llm";
|
||||
import {
|
||||
type WorkerMessage,
|
||||
type Session,
|
||||
} from "components/system/Taskbar/AI/types";
|
||||
|
||||
const MARKED_LIBS = [
|
||||
"/Program Files/Marked/marked.min.js",
|
||||
"/Program Files/Marked/purify.min.js",
|
||||
];
|
||||
|
||||
const CONVO_STYLE_TEMPS = {
|
||||
balanced: {
|
||||
temperature: 0.5,
|
||||
topK: 3,
|
||||
},
|
||||
creative: {
|
||||
temperature: 0.8,
|
||||
topK: 5,
|
||||
},
|
||||
precise: {
|
||||
temperature: 0.2,
|
||||
topK: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const WEB_LLM_MODEL = "Llama-3.1-8B-Instruct-q4f32_1-MLC";
|
||||
const WEB_LLM_MODEL_CONFIG = {
|
||||
"Llama-3.1-8B-Instruct-q4f32_1-MLC": {
|
||||
frequency_penalty: 0,
|
||||
max_tokens: 4000,
|
||||
presence_penalty: 0,
|
||||
top_p: 0.9,
|
||||
},
|
||||
};
|
||||
const WEB_LLM_SYSTEM_PROMPT: ChatCompletionMessageParam = {
|
||||
content: "You are a helpful AI assistant.",
|
||||
role: "system",
|
||||
};
|
||||
|
||||
let cancel = false;
|
||||
let responding = false;
|
||||
|
||||
let sessionId = 0;
|
||||
let session: Session | ChatCompletionMessageParam[] | undefined;
|
||||
let engine: MLCEngine;
|
||||
|
||||
let markedLoaded = false;
|
||||
|
||||
globalThis.addEventListener(
|
||||
"message",
|
||||
async ({ data }: { data: WorkerMessage | "cancel" | "init" }) => {
|
||||
if (!data || data === "init") return;
|
||||
|
||||
if (data === "cancel") {
|
||||
if (responding) cancel = true;
|
||||
} else if (data.id && data.text && data.style) {
|
||||
responding = true;
|
||||
|
||||
if (sessionId !== data.id) {
|
||||
sessionId = data.id;
|
||||
|
||||
if (data.hasWindowAI) {
|
||||
(session as Session)?.destroy();
|
||||
|
||||
session = await globalThis.ai.createTextSession(
|
||||
CONVO_STYLE_TEMPS[data.style]
|
||||
);
|
||||
} else {
|
||||
session = [WEB_LLM_SYSTEM_PROMPT];
|
||||
|
||||
if (!engine) {
|
||||
const { CreateMLCEngine } = await import("@mlc-ai/web-llm");
|
||||
|
||||
if (!cancel) {
|
||||
engine = await CreateMLCEngine(WEB_LLM_MODEL, {
|
||||
initProgressCallback: (progress) =>
|
||||
globalThis.postMessage({ progress }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = "";
|
||||
let retry = 0;
|
||||
|
||||
try {
|
||||
while (retry++ < 3 && !response) {
|
||||
if (cancel) break;
|
||||
|
||||
try {
|
||||
if (data.hasWindowAI) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
response = (await (session as Session)?.prompt(data.text)) || "";
|
||||
} else {
|
||||
(session as ChatCompletionMessageParam[]).push({
|
||||
content: data.text,
|
||||
role: "user",
|
||||
});
|
||||
|
||||
const {
|
||||
choices: [{ message }],
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
} = await engine.chat.completions.create({
|
||||
logprobs: true,
|
||||
messages: session as ChatCompletionMessageParam[],
|
||||
temperature: CONVO_STYLE_TEMPS[data.style].temperature,
|
||||
top_logprobs: CONVO_STYLE_TEMPS[data.style].topK,
|
||||
...WEB_LLM_MODEL_CONFIG[WEB_LLM_MODEL],
|
||||
});
|
||||
|
||||
(session as ChatCompletionMessageParam[]).push(message);
|
||||
|
||||
response = message.content || "";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get prompt response.", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response) console.error("Failed retires to create response.");
|
||||
} catch (error) {
|
||||
console.error("Failed to create text session.", error);
|
||||
}
|
||||
|
||||
if (cancel) {
|
||||
cancel = false;
|
||||
globalThis.postMessage("canceled");
|
||||
} else {
|
||||
if (response && !markedLoaded) {
|
||||
globalThis.importScripts(...MARKED_LIBS);
|
||||
markedLoaded = true;
|
||||
}
|
||||
|
||||
globalThis.postMessage({
|
||||
formattedResponse: globalThis.marked.parse(response, {
|
||||
headerIds: false,
|
||||
mangle: false,
|
||||
}),
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
responding = false;
|
||||
}
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
15
components/system/Taskbar/AI/constants.ts
Normal file
15
components/system/Taskbar/AI/constants.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { type ConvoStyles } from "components/system/Taskbar/AI/types";
|
||||
|
||||
export const AI_TITLE = "Talos";
|
||||
|
||||
export const AI_STAGE = "alpha";
|
||||
|
||||
export const DEFAULT_CONVO_STYLE: ConvoStyles = "balanced";
|
||||
|
||||
export const AI_WORKER = (): Worker =>
|
||||
new Worker(
|
||||
new URL("components/system/Taskbar/AI/ai.worker", import.meta.url),
|
||||
{ name: "AI" }
|
||||
);
|
||||
|
||||
export const WINDOW_ID = "ai-chat-window";
|
||||
57
components/system/Taskbar/AI/functions.ts
Normal file
57
components/system/Taskbar/AI/functions.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
export const formatWebLlmProgress = (text: string): string => {
|
||||
if (text === "Start to fetch params") return "Fetching parameters";
|
||||
if (text.startsWith("Finish loading on WebGPU")) return "";
|
||||
|
||||
const [, progressCurrent, progressTotal] =
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
/\[(\d+)\/(\d+)\]/.exec(text) || [];
|
||||
|
||||
let progress = "";
|
||||
|
||||
if (typeof Number(progressTotal) === "number") {
|
||||
progress = `${progressCurrent || 0}/${progressTotal}`;
|
||||
}
|
||||
|
||||
if (text.startsWith("Loading model from cache")) {
|
||||
return `Loading${progress ? ` (${progress})` : ""}`;
|
||||
}
|
||||
|
||||
const [, percentComplete] = /(\d+)% completed/.exec(text) || [];
|
||||
const [, secsElapsed] = /(\d+) secs elapsed/.exec(text) || [];
|
||||
|
||||
if (typeof Number(percentComplete) === "number") {
|
||||
progress += `${progress ? ", " : ""}${percentComplete}%`;
|
||||
}
|
||||
|
||||
if (typeof Number(secsElapsed) === "number") {
|
||||
progress += `${progress ? ", " : ""}${secsElapsed}s`;
|
||||
}
|
||||
|
||||
if (text.startsWith("Loading GPU shader modules")) {
|
||||
return `Loading into GPU${progress ? ` (${progress})` : ""}`;
|
||||
}
|
||||
|
||||
const [, dataLoaded] = /(\d+)MB (fetched|loaded)/.exec(text) || [];
|
||||
|
||||
if (typeof Number(dataLoaded) === "number") {
|
||||
progress += `${progress ? ", " : ""}${dataLoaded}MB`;
|
||||
}
|
||||
|
||||
if (text.startsWith("Fetching param cache")) {
|
||||
return `Fetching${progress ? ` (${progress})` : ""}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
export const speakMessage = (text: string): void => {
|
||||
const [voice] = window.speechSynthesis.getVoices();
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
|
||||
utterance.voice = voice;
|
||||
utterance.pitch = 0.9;
|
||||
utterance.rate = 1.5;
|
||||
utterance.volume = 0.5;
|
||||
|
||||
window.speechSynthesis.speak(utterance);
|
||||
};
|
||||
123
components/system/Taskbar/AI/icons.tsx
Normal file
123
components/system/Taskbar/AI/icons.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { memo } from "react";
|
||||
|
||||
export const AIIcon = memo(() => (
|
||||
<svg viewBox="0 0 330 220" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M177.49 123.22c7.325 13.037 15.82 22.852 25.489 29.444 9.814 6.445 20.874 9.668 33.179 9.668 14.794 0 26.88-4.908 36.255-14.722 9.374-9.961 14.062-22.632 14.062-38.013 0-14.795-4.322-27.1-12.964-36.914-8.643-9.814-19.483-14.722-32.52-14.722-11.865 0-22.632 4.908-32.3 14.722-9.521 9.668-19.921 26.514-31.2 50.537m-26.807-23.51c-7.178-12.891-15.674-22.56-25.489-29.005-9.668-6.445-20.727-9.667-33.178-9.668-14.795 0-26.88 4.908-36.255 14.722-9.375 9.668-14.063 22.193-14.063 37.573 0 14.796 4.321 27.1 12.964 36.915 8.643 9.814 19.482 14.721 32.52 14.721 11.865 0 22.558-4.834 32.08-14.502 9.668-9.668 20.141-26.587 31.42-50.757m15.601 40.21c-10.4 19.922-21.313 34.498-32.739 43.726-11.28 9.229-23.877 13.843-37.793 13.843-19.775 0-36.548-8.203-50.317-24.61C31.81 156.472 25 136.184 25 112.014c0-25.635 6.079-46.362 18.237-62.183 12.305-15.82 28.272-23.73 47.9-23.73 13.917 0 26.368 4.541 37.354 13.623 10.987 8.936 21.973 23.73 32.96 44.385 9.96-20.215 20.727-35.083 32.3-44.605 11.571-9.668 24.462-14.502 38.671-14.502 19.482 0 36.108 8.277 49.878 24.83 13.916 16.552 20.874 36.987 20.874 61.303 0 25.489-6.152 46.143-18.457 61.963-12.158 15.674-28.052 23.511-47.68 23.511-13.917 0-26.295-4.248-37.134-12.744-10.694-8.643-21.9-23.291-33.619-43.946"
|
||||
fill="url(#a)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
gradientUnits="objectBoundingBox"
|
||||
id="a"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="0" stopColor="#C02B3C">
|
||||
<animate
|
||||
attributeName="stop-color"
|
||||
dur="20s"
|
||||
repeatCount="indefinite"
|
||||
values="#C02B3C;#A86EDD;#0736C4;#52B471;#FFC800;#FF5F3D;#C02B3C;"
|
||||
/>
|
||||
</stop>
|
||||
<stop offset=".5" stopColor="#A86EDD">
|
||||
<animate
|
||||
attributeName="stop-color"
|
||||
dur="20s"
|
||||
repeatCount="indefinite"
|
||||
values="#A86EDD;#0736C4;#52B471;#FFC800;#FF5F3D;#C02B3C;#A86EDD;"
|
||||
/>
|
||||
</stop>
|
||||
<stop offset="1" stopColor="#0736C4">
|
||||
<animate
|
||||
attributeName="stop-color"
|
||||
dur="20s"
|
||||
repeatCount="indefinite"
|
||||
values="#0736C4;#52B471;#FFC800;#FF5F3D;#C02B3C;#A86EDD;#0736C4;"
|
||||
/>
|
||||
</stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const ChatIcon = memo(() => (
|
||||
<svg className="chat" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2c5.523 0 10 4.477 10 10 0 .263-.01.523-.03.78a6.518 6.518 0 0 0-1.474-1.05 8.5 8.5 0 1 0-15.923 4.407l.15.27-1.112 3.984 3.987-1.112.27.15a8.449 8.449 0 0 0 3.862 1.067c.281.54.636 1.036 1.05 1.474a9.96 9.96 0 0 1-5.368-1.082l-3.825 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm11 15.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5 .5.001 2.503a.5.5 0 1 1-1 0V18h-2.505a.5.5 0 0 1 0-1H17v-2.5a.5.5 0 1 1 1 0V17h2.497a.5.5 0 0 1 0 1H18Z" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const SendIcon = memo(() => (
|
||||
<svg className="send" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.694 12 2.299 3.27c-.236-.607.356-1.188.942-.981l.093.039 18 9a.75.75 0 0 1 .097 1.284l-.097.058-18 9c-.583.291-1.217-.245-1.065-.848l.03-.095L5.694 12 2.299 3.27 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const SendFilledIcon = memo(() => (
|
||||
<svg className="send" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m12.815 12.197-7.532 1.255a.5.5 0 0 0-.386.318L2.3 20.728c-.248.64.421 1.25 1.035.942l18-9a.75.75 0 0 0 0-1.341l-18-9c-.614-.307-1.283.303-1.035.942l2.598 6.958a.5.5 0 0 0 .386.318l7.532 1.255a.2.2 0 0 1 0 .395Z" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const PersonIcon = memo(() => (
|
||||
<svg
|
||||
className="person"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M17.755 14a2.249 2.249 0 0 1 2.248 2.25v.918a2.75 2.75 0 0 1-.512 1.598c-1.546 2.164-4.07 3.235-7.49 3.235-3.422 0-5.945-1.072-7.487-3.236a2.75 2.75 0 0 1-.51-1.596v-.92A2.249 2.249 0 0 1 6.253 14h11.502ZM12 2.005a5 5 0 1 1 0 10 5 5 0 0 1 0-10Z" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const CopyIcon = memo(() => (
|
||||
<svg
|
||||
className="copy-icon"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M5.503 4.627 5.5 6.75v10.504a3.25 3.25 0 0 0 3.25 3.25h8.616a2.251 2.251 0 0 1-2.122 1.5H8.75A4.75 4.75 0 0 1 4 17.254V6.75c0-.98.627-1.815 1.503-2.123ZM17.75 2A2.25 2.25 0 0 1 20 4.25v13a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-13A2.25 2.25 0 0 1 8.75 2h9Zm0 1.5h-9a.75.75 0 0 0-.75.75v13c0 .414.336.75.75.75h9a.75.75 0 0 0 .75-.75v-13a.75.75 0 0 0-.75-.75Z" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const EditIcon = memo(() => (
|
||||
<svg
|
||||
className="edit-icon"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M20.952 3.048a3.578 3.578 0 0 0-5.06 0L3.94 15a3.106 3.106 0 0 0-.825 1.476L2.02 21.078a.75.75 0 0 0 .904.903l4.601-1.096a3.106 3.106 0 0 0 1.477-.825L20.952 8.11a3.578 3.578 0 0 0 0-5.06Zm-4 1.06a2.078 2.078 0 1 1 2.94 2.94L19 7.939 16.06 5l.892-.891ZM15 6.062 17.94 9 7.94 19c-.21.21-.474.357-.763.426l-3.416.814.813-3.416c.069-.29.217-.554.427-.764L15 6.06Z" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const StopIcon = memo(() => (
|
||||
<svg
|
||||
className="stop-icon"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M4.75 3A1.75 1.75 0 0 0 3 4.75v14.5c0 .966.784 1.75 1.75 1.75h14.5A1.75 1.75 0 0 0 21 19.25V4.75A1.75 1.75 0 0 0 19.25 3H4.75Z" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const WarningIcon = memo(() => (
|
||||
<svg
|
||||
className="warning-icon"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M13 17a.999.999 0 1 0-1.998 0 .999.999 0 0 0 1.997 0Zm-.26-7.853a.75.75 0 0 0-1.493.103l.004 4.501.007.102a.75.75 0 0 0 1.493-.103l-.004-4.502-.007-.101Zm1.23-5.488c-.857-1.548-3.082-1.548-3.938 0L2.286 17.66c-.83 1.5.255 3.34 1.97 3.34h15.49c1.714 0 2.799-1.84 1.969-3.34L13.969 3.66Zm-2.626.726a.75.75 0 0 1 1.313 0l7.745 14.002a.75.75 0 0 1-.656 1.113H4.256a.75.75 0 0 1-.657-1.113l7.745-14.002Z" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const SpeakIcon = memo(() => (
|
||||
<svg
|
||||
className="speak-icon"
|
||||
viewBox="0 0 20 19"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M13 2.25c0-1.079-1.274-1.65-2.08-.934L6.427 5.309a.75.75 0 0 1-.498.19H2.25A2.25 2.25 0 0 0 0 7.749v4.497a2.25 2.25 0 0 0 2.25 2.25h3.68a.75.75 0 0 1 .498.19l4.491 3.994c.806.716 2.081.144 2.081-.934V2.25ZM7.425 6.43 11.5 2.807v14.382l-4.075-3.624a2.25 2.25 0 0 0-1.495-.569H2.25a.75.75 0 0 1-.75-.75V7.75A.75.75 0 0 1 2.25 7h3.68a2.25 2.25 0 0 0 1.495-.569Zm9.567-2.533a.75.75 0 0 1 1.049.157A9.959 9.959 0 0 1 20 10a9.96 9.96 0 0 1-1.96 5.946.75.75 0 0 1-1.205-.892A8.459 8.459 0 0 0 18.5 10a8.459 8.459 0 0 0-1.665-5.054.75.75 0 0 1 .157-1.049ZM15.143 6.37a.75.75 0 0 1 1.017.303c.536.99.84 2.125.84 3.328a6.973 6.973 0 0 1-.84 3.328.75.75 0 0 1-1.32-.714c.42-.777.66-1.666.66-2.614s-.24-1.837-.66-2.614a.75.75 0 0 1 .303-1.017Z" />
|
||||
</svg>
|
||||
));
|
||||
52
components/system/Taskbar/AI/types.ts
Normal file
52
components/system/Taskbar/AI/types.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { type MarkedOptions } from "components/apps/Marked/useMarked";
|
||||
|
||||
export type Session = {
|
||||
destroy: () => void;
|
||||
prompt: (message: string) => Promise<string>;
|
||||
};
|
||||
|
||||
declare global {
|
||||
/* eslint-disable vars-on-top, no-var */
|
||||
var ai: {
|
||||
canCreateTextSession: () => Promise<string>;
|
||||
createTextSession: (config?: {
|
||||
temperature?: number;
|
||||
topK?: number;
|
||||
}) => Promise<Session>;
|
||||
};
|
||||
var marked: {
|
||||
parse: (markdownString: string, options: MarkedOptions) => string;
|
||||
};
|
||||
/* eslint-enable vars-on-top, no-var */
|
||||
}
|
||||
|
||||
export type MessageTypes = "user" | "ai";
|
||||
|
||||
export type Message = {
|
||||
formattedText: string;
|
||||
text: string;
|
||||
type: MessageTypes;
|
||||
};
|
||||
|
||||
export type ConvoStyles = "balanced" | "creative" | "precise";
|
||||
|
||||
export type WorkerMessage = {
|
||||
hasWindowAI: boolean;
|
||||
id: number;
|
||||
style: ConvoStyles;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type AIResponse = { formattedResponse: string; response: string };
|
||||
|
||||
export type WebLlmProgress = {
|
||||
progress: {
|
||||
progress: number;
|
||||
text: string;
|
||||
timeElapsed: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkerResponse = {
|
||||
data: AIResponse | WebLlmProgress | "canceled";
|
||||
};
|
||||
28
components/system/Taskbar/AI/useAITransition.ts
Normal file
28
components/system/Taskbar/AI/useAITransition.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { type MotionProps } from "framer-motion";
|
||||
import { TRANSITIONS_IN_SECONDS } from "utils/constants";
|
||||
|
||||
const useAITransition = (width: number, widthOffset = 0.75): MotionProps => ({
|
||||
animate: "active",
|
||||
exit: {
|
||||
transition: {
|
||||
duration: TRANSITIONS_IN_SECONDS.TASKBAR_ITEM / 10,
|
||||
ease: "circIn",
|
||||
},
|
||||
width: `${width * widthOffset}px`,
|
||||
},
|
||||
initial: "initial",
|
||||
transition: {
|
||||
duration: TRANSITIONS_IN_SECONDS.TASKBAR_ITEM,
|
||||
ease: "circOut",
|
||||
},
|
||||
variants: {
|
||||
active: {
|
||||
width: `${width}px`,
|
||||
},
|
||||
initial: {
|
||||
width: `${width * widthOffset}px`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useAITransition;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
type StyledClockProps = {
|
||||
$hasAI: boolean;
|
||||
$width: number;
|
||||
};
|
||||
|
||||
|
|
@ -16,7 +17,8 @@ const StyledClock = styled.div<StyledClockProps>`
|
|||
place-content: center;
|
||||
place-items: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
right: ${({ theme, $hasAI }) =>
|
||||
$hasAI ? theme.sizes.taskbar.ai.buttonWidth : 0};
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
|
|
|
|||
|
|
@ -59,12 +59,18 @@ const easterEggOnClick: React.MouseEventHandler<HTMLElement> = async ({
|
|||
};
|
||||
|
||||
type ClockProps = {
|
||||
hasAI: boolean;
|
||||
setClockWidth: React.Dispatch<React.SetStateAction<number>>;
|
||||
toggleCalendar: () => void;
|
||||
width: number;
|
||||
};
|
||||
|
||||
const Clock: FC<ClockProps> = ({ setClockWidth, toggleCalendar, width }) => {
|
||||
const Clock: FC<ClockProps> = ({
|
||||
hasAI,
|
||||
setClockWidth,
|
||||
toggleCalendar,
|
||||
width,
|
||||
}) => {
|
||||
const [now, setNow] = useState<LocaleTimeDate>(
|
||||
Object.create(null) as LocaleTimeDate
|
||||
);
|
||||
|
|
@ -197,6 +203,7 @@ const Clock: FC<ClockProps> = ({ setClockWidth, toggleCalendar, width }) => {
|
|||
return (
|
||||
<StyledClock
|
||||
ref={supportsOffscreenCanvas ? clockCallbackRef : undefined}
|
||||
$hasAI={hasAI}
|
||||
$width={width}
|
||||
aria-label="Clock"
|
||||
onClick={onClockClick}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ import StyledTaskbar from "components/system/Taskbar/StyledTaskbar";
|
|||
import TaskbarEntries from "components/system/Taskbar/TaskbarEntries";
|
||||
import useTaskbarContextMenu from "components/system/Taskbar/useTaskbarContextMenu";
|
||||
import { CLOCK_CANVAS_BASE_WIDTH, FOCUSABLE_ELEMENT } from "utils/constants";
|
||||
import { useWindowAI } from "hooks/useWindowAI";
|
||||
import { useSession } from "contexts/session";
|
||||
|
||||
const AIButton = dynamic(() => import("components/system/Taskbar/AI/AIButton"));
|
||||
const AIChat = dynamic(() => import("components/system/Taskbar/AI/AIChat"));
|
||||
const Calendar = dynamic(() => import("components/system/Taskbar/Calendar"));
|
||||
const Search = dynamic(() => import("components/system/Taskbar/Search"));
|
||||
const StartMenu = dynamic(() => import("components/system/StartMenu"));
|
||||
|
|
@ -17,7 +21,10 @@ const Taskbar: FC = () => {
|
|||
const [startMenuVisible, setStartMenuVisible] = useState(false);
|
||||
const [searchVisible, setSearchVisible] = useState(false);
|
||||
const [calendarVisible, setCalendarVisible] = useState(false);
|
||||
const [aiVisible, setAIVisible] = useState(false);
|
||||
const [clockWidth, setClockWidth] = useState(CLOCK_CANVAS_BASE_WIDTH);
|
||||
const { aiEnabled } = useSession();
|
||||
const hasWindowAI = useWindowAI();
|
||||
const toggleStartMenu = useCallback(
|
||||
(showMenu?: boolean): void =>
|
||||
setStartMenuVisible((currentMenuState) => showMenu ?? !currentMenuState),
|
||||
|
|
@ -37,6 +44,11 @@ const Taskbar: FC = () => {
|
|||
),
|
||||
[]
|
||||
);
|
||||
const toggleAI = useCallback(
|
||||
(showAI?: boolean): void =>
|
||||
setAIVisible((currentAIState) => showAI ?? !currentAIState),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -57,13 +69,18 @@ const Taskbar: FC = () => {
|
|||
/>
|
||||
<TaskbarEntries clockWidth={clockWidth} />
|
||||
<Clock
|
||||
hasAI={hasWindowAI || aiEnabled}
|
||||
setClockWidth={setClockWidth}
|
||||
toggleCalendar={toggleCalendar}
|
||||
width={clockWidth}
|
||||
/>
|
||||
{(hasWindowAI || aiEnabled) && (
|
||||
<AIButton aiVisible={aiVisible} toggleAI={toggleAI} />
|
||||
)}
|
||||
</StyledTaskbar>
|
||||
<AnimatePresence initial={false} presenceAffectsLayout={false}>
|
||||
{calendarVisible && <Calendar toggleCalendar={toggleCalendar} />}
|
||||
{aiVisible && <AIChat toggleAI={toggleAI} />}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import { AI_TITLE } from "components/system/Taskbar/AI/constants";
|
||||
import { useMenu } from "contexts/menu";
|
||||
import {
|
||||
type ContextMenuCapture,
|
||||
|
|
@ -10,13 +11,17 @@ import { useViewport } from "contexts/viewport";
|
|||
import { useProcessesRef } from "hooks/useProcessesRef";
|
||||
import { MENU_SEPERATOR } from "utils/constants";
|
||||
import { toggleShowDesktop } from "utils/functions";
|
||||
import { useWebGPUCheck } from "hooks/useWebGPUCheck";
|
||||
import { useWindowAI } from "hooks/useWindowAI";
|
||||
|
||||
const useTaskbarContextMenu = (onStartButton = false): ContextMenuCapture => {
|
||||
const { contextMenu } = useMenu();
|
||||
const { minimize, open } = useProcesses();
|
||||
const { stackOrder } = useSession();
|
||||
const { aiEnabled, setAiEnabled, stackOrder } = useSession();
|
||||
const processesRef = useProcessesRef();
|
||||
const { fullscreenElement, toggleFullscreen } = useViewport();
|
||||
const hasWebGPU = useWebGPUCheck();
|
||||
const hasWindowAI = useWindowAI();
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
|
|
@ -62,19 +67,33 @@ const useTaskbarContextMenu = (onStartButton = false): ContextMenuCapture => {
|
|||
? "Exit full screen"
|
||||
: "Enter full screen",
|
||||
},
|
||||
MENU_SEPERATOR
|
||||
MENU_SEPERATOR,
|
||||
...(hasWebGPU && !hasWindowAI
|
||||
? [
|
||||
{
|
||||
action: () => setAiEnabled(!aiEnabled),
|
||||
checked: aiEnabled,
|
||||
label: `Show ${AI_TITLE} button`,
|
||||
},
|
||||
MENU_SEPERATOR,
|
||||
]
|
||||
: [])
|
||||
);
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
}),
|
||||
[
|
||||
aiEnabled,
|
||||
contextMenu,
|
||||
fullscreenElement,
|
||||
hasWebGPU,
|
||||
hasWindowAI,
|
||||
minimize,
|
||||
onStartButton,
|
||||
open,
|
||||
processesRef,
|
||||
setAiEnabled,
|
||||
stackOrder,
|
||||
toggleFullscreen,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export type IconPosition = {
|
|||
export type IconPositions = Record<string, IconPosition>;
|
||||
|
||||
export type SessionData = {
|
||||
aiEnabled: boolean;
|
||||
clockSource: ClockSource;
|
||||
cursor: string;
|
||||
iconPositions: IconPositions;
|
||||
|
|
@ -54,6 +55,7 @@ export type SessionContextState = SessionData & {
|
|||
prependToStack: (id: string) => void;
|
||||
removeFromStack: (id: string) => void;
|
||||
sessionLoaded: boolean;
|
||||
setAiEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setClockSource: React.Dispatch<React.SetStateAction<ClockSource>>;
|
||||
setCursor: React.Dispatch<React.SetStateAction<string>>;
|
||||
setForegroundId: React.Dispatch<React.SetStateAction<string>>;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const useSessionContextState = (): SessionContextState => {
|
|||
const [themeName, setThemeName] = useState(DEFAULT_THEME);
|
||||
const [clockSource, setClockSource] = useState(DEFAULT_CLOCK_SOURCE);
|
||||
const [cursor, setCursor] = useState("");
|
||||
const [aiEnabled, setAiEnabled] = useState(false);
|
||||
const [windowStates, setWindowStates] = useState(
|
||||
Object.create(null) as WindowStates
|
||||
);
|
||||
|
|
@ -186,6 +187,7 @@ const useSessionContextState = (): SessionContextState => {
|
|||
writeFile(
|
||||
SESSION_FILE,
|
||||
JSON.stringify({
|
||||
aiEnabled,
|
||||
clockSource,
|
||||
cursor,
|
||||
iconPositions,
|
||||
|
|
@ -211,6 +213,7 @@ const useSessionContextState = (): SessionContextState => {
|
|||
}
|
||||
}
|
||||
}, [
|
||||
aiEnabled,
|
||||
clockSource,
|
||||
cursor,
|
||||
haltSession,
|
||||
|
|
@ -247,6 +250,7 @@ const useSessionContextState = (): SessionContextState => {
|
|||
|
||||
if (session.clockSource) setClockSource(session.clockSource);
|
||||
if (session.cursor) setCursor(session.cursor);
|
||||
if (session.aiEnabled) setAiEnabled(session.aiEnabled);
|
||||
if (session.themeName) setThemeName(session.themeName);
|
||||
if (session.wallpaperImage) {
|
||||
setWallpaper(session.wallpaperImage, session.wallpaperFit);
|
||||
|
|
@ -338,6 +342,7 @@ const useSessionContextState = (): SessionContextState => {
|
|||
}, [deletePath, lstat, readFile, rootFs, setWallpaper]);
|
||||
|
||||
return {
|
||||
aiEnabled,
|
||||
clockSource,
|
||||
cursor,
|
||||
foregroundId,
|
||||
|
|
@ -347,6 +352,7 @@ const useSessionContextState = (): SessionContextState => {
|
|||
removeFromStack,
|
||||
runHistory,
|
||||
sessionLoaded,
|
||||
setAiEnabled,
|
||||
setClockSource,
|
||||
setCursor,
|
||||
setForegroundId,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ type NavigatorWithGPU = Navigator & {
|
|||
};
|
||||
};
|
||||
|
||||
let HAS_WEB_GPU = false;
|
||||
|
||||
const supportsWebGPU = async (): Promise<boolean> => {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
if (!("gpu" in navigator)) return false;
|
||||
|
|
@ -41,19 +43,22 @@ const supportsWebGPU = async (): Promise<boolean> => {
|
|||
requiredMaxComputeWorkgroupStorageSize >
|
||||
(adapter.limits.maxComputeWorkgroupStorageSize ?? 0);
|
||||
|
||||
if (!insufficientLimits) HAS_WEB_GPU = true;
|
||||
|
||||
return !insufficientLimits;
|
||||
};
|
||||
|
||||
export const useWebGPUCheck = (): boolean => {
|
||||
const [hasWebGPU, setHasWebGPU] = useState<boolean>(false);
|
||||
const checkWebGPU = useCallback(
|
||||
async () => setHasWebGPU(await supportsWebGPU()),
|
||||
[]
|
||||
);
|
||||
const [hasWebGPU, setHasWebGPU] = useState<boolean>(HAS_WEB_GPU);
|
||||
const checkWebGPU = useCallback(async () => {
|
||||
const sufficientLimits = await supportsWebGPU();
|
||||
|
||||
if (sufficientLimits) setHasWebGPU(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(checkWebGPU);
|
||||
}, [checkWebGPU]);
|
||||
if (!hasWebGPU) requestAnimationFrame(checkWebGPU);
|
||||
}, [checkWebGPU, hasWebGPU]);
|
||||
|
||||
return hasWebGPU;
|
||||
};
|
||||
|
|
|
|||
35
hooks/useWindowAI.ts
Normal file
35
hooks/useWindowAI.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
let HAS_WINDOW_AI = false;
|
||||
|
||||
const supportsAI = async (): Promise<boolean> => {
|
||||
if (typeof window === "undefined") return false;
|
||||
if (!("ai" in window)) return false;
|
||||
|
||||
try {
|
||||
if (!("canCreateTextSession" in window.ai)) return false;
|
||||
|
||||
const hasWindowAi = (await window.ai.canCreateTextSession()) !== "no";
|
||||
|
||||
if (hasWindowAi) HAS_WINDOW_AI = true;
|
||||
|
||||
return hasWindowAi;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const useWindowAI = (): boolean => {
|
||||
const [hasAI, setHasAI] = useState<boolean>(HAS_WINDOW_AI);
|
||||
const checkAI = useCallback(async () => {
|
||||
const hasWindowAi = await supportsAI();
|
||||
|
||||
if (hasWindowAi) setHasAI(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasAI) requestAnimationFrame(checkAI);
|
||||
}, [checkAI, hasAI]);
|
||||
|
||||
return hasAI;
|
||||
};
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
"*.{js,ts,tsx}": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mlc-ai/web-llm": "^0.2.58",
|
||||
"@monaco-editor/loader": "^1.4.0",
|
||||
"@panzoom/panzoom": "^4.5.1",
|
||||
"@prettier/plugin-xml": "^3.4.1",
|
||||
|
|
|
|||
7
public/Program Files/Marked/marked.min.js
vendored
7
public/Program Files/Marked/marked.min.js
vendored
File diff suppressed because one or more lines are too long
3
public/Program Files/Marked/purify.min.js
vendored
3
public/Program Files/Marked/purify.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -27,6 +27,15 @@ const colors = {
|
|||
taskbar: {
|
||||
active: "hsla(0, 0%, 20%, 70%)",
|
||||
activeForeground: "hsla(0, 0%, 40%, 70%)",
|
||||
ai: {
|
||||
balanced: ["rgb(112, 203, 255)", "rgb(40, 112, 234)", "rgb(0, 95, 184)"],
|
||||
creative: [
|
||||
"rgb(215, 167, 187)",
|
||||
"rgb(145, 72, 135)",
|
||||
"rgb(139, 37, 126)",
|
||||
],
|
||||
precise: ["rgb(167, 224, 235)", "rgb(0, 104, 128)", "rgb(0, 83, 102)"],
|
||||
},
|
||||
background: "hsla(0, 0%, 10%, 70%)",
|
||||
button: {
|
||||
color: "#FFF",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ const sizes = {
|
|||
size: 320,
|
||||
},
|
||||
taskbar: {
|
||||
ai: {
|
||||
buttonWidth: "40px",
|
||||
chatWidth: 415,
|
||||
},
|
||||
blur: "5px",
|
||||
button: {
|
||||
iconSize: "15px",
|
||||
|
|
|
|||
12
yarn.lock
12
yarn.lock
|
|
@ -746,6 +746,13 @@
|
|||
semver "^7.3.5"
|
||||
tar "^6.1.11"
|
||||
|
||||
"@mlc-ai/web-llm@^0.2.58":
|
||||
version "0.2.58"
|
||||
resolved "https://registry.yarnpkg.com/@mlc-ai/web-llm/-/web-llm-0.2.58.tgz#9cb4ed31ebde0573bd0a84d2baf9b5cfbc0799ea"
|
||||
integrity sha512-QWMwMC6E4JyfBSQ0rRu/Z1V+nicMycE0wXSnrCKebsTAg/Z7h8l5JXAES2v3Yls99qHZtmrKXwn0ZSHUccd7yw==
|
||||
dependencies:
|
||||
loglevel "^1.9.1"
|
||||
|
||||
"@monaco-editor/loader@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
|
||||
|
|
@ -5234,6 +5241,11 @@ log-update@^6.1.0:
|
|||
strip-ansi "^7.1.0"
|
||||
wrap-ansi "^9.0.0"
|
||||
|
||||
loglevel@^1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7"
|
||||
integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user