Commit
·
db78b1a
1
Parent(s):
9b72f0d
done
Browse files- .idea/php.xml +15 -0
- .idea/prettier.xml +2 -1
- src/App.tsx +5 -90
- src/chat/Chat.tsx +80 -20
- src/chat/ChatForm.tsx +20 -16
- src/chat/MessageContent.tsx +116 -0
- src/chat/MessageToolCall.tsx +57 -0
- src/layout/Header.tsx +36 -0
- src/textGeneration/TextGeneration.ts +203 -43
- src/textGeneration/types.ts +35 -1
- src/textGeneration/worker/textGenerationWorker.ts +12 -10
- src/theme/button/Button.tsx +2 -22
- src/theme/misc/Loader.tsx +22 -3
- src/utils/context/chatSettings/ChatSettingsContext.ts +2 -2
- src/utils/context/theme/ThemeContext.ts +1 -1
- src/utils/models.ts +47 -1
- src/utils/tools.ts +73 -3
- src/utils/webMcp.ts +57 -0
- tsconfig.app.json +1 -0
- vite.config.ts +9 -9
.idea/php.xml
CHANGED
|
@@ -10,9 +10,24 @@
|
|
| 10 |
<option name="highlightLevel" value="WARNING" />
|
| 11 |
<option name="transferred" value="true" />
|
| 12 |
</component>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
<component name="PhpStanOptionsConfiguration">
|
| 14 |
<option name="transferred" value="true" />
|
| 15 |
</component>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
<component name="PsalmOptionsConfiguration">
|
| 17 |
<option name="transferred" value="true" />
|
| 18 |
</component>
|
|
|
|
| 10 |
<option name="highlightLevel" value="WARNING" />
|
| 11 |
<option name="transferred" value="true" />
|
| 12 |
</component>
|
| 13 |
+
<component name="PhpCodeSniffer">
|
| 14 |
+
<phpcs_settings>
|
| 15 |
+
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="7f620b7f-b413-40f6-9b51-4ffe32f7487a" timeout="30000" />
|
| 16 |
+
</phpcs_settings>
|
| 17 |
+
</component>
|
| 18 |
+
<component name="PhpStan">
|
| 19 |
+
<PhpStan_settings>
|
| 20 |
+
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="7f620b7f-b413-40f6-9b51-4ffe32f7487a" timeout="60000" />
|
| 21 |
+
</PhpStan_settings>
|
| 22 |
+
</component>
|
| 23 |
<component name="PhpStanOptionsConfiguration">
|
| 24 |
<option name="transferred" value="true" />
|
| 25 |
</component>
|
| 26 |
+
<component name="Psalm">
|
| 27 |
+
<Psalm_settings>
|
| 28 |
+
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="7f620b7f-b413-40f6-9b51-4ffe32f7487a" timeout="60000" />
|
| 29 |
+
</Psalm_settings>
|
| 30 |
+
</component>
|
| 31 |
<component name="PsalmOptionsConfiguration">
|
| 32 |
<option name="transferred" value="true" />
|
| 33 |
</component>
|
.idea/prettier.xml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
<project version="4">
|
| 3 |
<component name="PrettierConfiguration">
|
| 4 |
-
<option name="myConfigurationMode" value="
|
|
|
|
| 5 |
</component>
|
| 6 |
</project>
|
|
|
|
| 1 |
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
<project version="4">
|
| 3 |
<component name="PrettierConfiguration">
|
| 4 |
+
<option name="myConfigurationMode" value="MANUAL" />
|
| 5 |
+
<option name="myRunOnSave" value="true" />
|
| 6 |
</component>
|
| 7 |
</project>
|
src/App.tsx
CHANGED
|
@@ -1,103 +1,18 @@
|
|
| 1 |
-
import { type Message } from "@huggingface/transformers";
|
| 2 |
import ChatSettingsContextProvider from "@utils/context/chatSettings/ChatSettingsContextProvider.tsx";
|
| 3 |
-
import { type FormEvent, useMemo, useState } from "react";
|
| 4 |
|
| 5 |
import Chat from "./chat/Chat.tsx";
|
| 6 |
-
import
|
| 7 |
import ThemeContextProvider from "./utils/context/theme/ThemeContextProvider.tsx";
|
| 8 |
|
| 9 |
export default function App() {
|
| 10 |
return (
|
| 11 |
<ThemeContextProvider>
|
| 12 |
<ChatSettingsContextProvider>
|
| 13 |
-
<
|
|
|
|
|
|
|
|
|
|
| 14 |
</ChatSettingsContextProvider>
|
| 15 |
</ThemeContextProvider>
|
| 16 |
);
|
| 17 |
}
|
| 18 |
-
|
| 19 |
-
/*export default function App() {
|
| 20 |
-
const [initialized, setInitialized] = useState<boolean>(false);
|
| 21 |
-
const [input, setInput] = useState<string>("");
|
| 22 |
-
const [messages, setMessages] = useState<Array<Message>>([
|
| 23 |
-
{
|
| 24 |
-
role: "system",
|
| 25 |
-
content: "You are a helpful assistant",
|
| 26 |
-
},
|
| 27 |
-
]);
|
| 28 |
-
const [isLoading, setIsLoading] = useState(false);
|
| 29 |
-
|
| 30 |
-
const textGeneration = useMemo(() => new TextGeneration(), []);
|
| 31 |
-
|
| 32 |
-
const init = async () => {
|
| 33 |
-
await textGeneration.initializeModel(console.log);
|
| 34 |
-
setInitialized(true);
|
| 35 |
-
};
|
| 36 |
-
|
| 37 |
-
const handleSubmit = async (e: FormEvent) => {
|
| 38 |
-
e.preventDefault();
|
| 39 |
-
if (!input.trim()) return;
|
| 40 |
-
|
| 41 |
-
setIsLoading(true);
|
| 42 |
-
const requestMessages = messages;
|
| 43 |
-
requestMessages.push({
|
| 44 |
-
role: "user",
|
| 45 |
-
content: input,
|
| 46 |
-
});
|
| 47 |
-
|
| 48 |
-
try {
|
| 49 |
-
const resp = await textGeneration.generateText(
|
| 50 |
-
requestMessages,
|
| 51 |
-
[],
|
| 52 |
-
console.log
|
| 53 |
-
);
|
| 54 |
-
console.log(resp);
|
| 55 |
-
requestMessages.push({
|
| 56 |
-
role: "assistant",
|
| 57 |
-
content: resp.response,
|
| 58 |
-
});
|
| 59 |
-
setMessages(requestMessages);
|
| 60 |
-
setInput("");
|
| 61 |
-
} catch (error) {
|
| 62 |
-
console.error("Error:", error);
|
| 63 |
-
//setOutput("Error generating text. Check console for details.");
|
| 64 |
-
} finally {
|
| 65 |
-
setIsLoading(false);
|
| 66 |
-
}
|
| 67 |
-
};
|
| 68 |
-
|
| 69 |
-
return (
|
| 70 |
-
<div className="mx-auto max-w-3xl">
|
| 71 |
-
<h1 className="mb-6 text-3xl font-bold text-gray-900">Text Generation</h1>
|
| 72 |
-
{initialized ? (
|
| 73 |
-
<form onSubmit={handleSubmit} className="space-y-4">
|
| 74 |
-
<textarea
|
| 75 |
-
value={input}
|
| 76 |
-
onChange={(e) => setInput(e.target.value)}
|
| 77 |
-
placeholder="Enter your prompt here..."
|
| 78 |
-
rows={5}
|
| 79 |
-
className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-transparent focus:ring-2 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-100"
|
| 80 |
-
disabled={isLoading}
|
| 81 |
-
/>
|
| 82 |
-
<button
|
| 83 |
-
type="submit"
|
| 84 |
-
disabled={isLoading}
|
| 85 |
-
className="rounded-lg bg-blue-600 px-6 py-3 font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
| 86 |
-
>
|
| 87 |
-
{isLoading ? "Generating..." : "Answer"}
|
| 88 |
-
</button>
|
| 89 |
-
</form>
|
| 90 |
-
) : (
|
| 91 |
-
<button
|
| 92 |
-
type="button"
|
| 93 |
-
onClick={init}
|
| 94 |
-
disabled={isLoading}
|
| 95 |
-
className="rounded-lg bg-blue-600 px-6 py-3 font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
| 96 |
-
>
|
| 97 |
-
initialize
|
| 98 |
-
</button>
|
| 99 |
-
)}
|
| 100 |
-
</div>
|
| 101 |
-
);
|
| 102 |
-
}
|
| 103 |
-
*/
|
|
|
|
|
|
|
| 1 |
import ChatSettingsContextProvider from "@utils/context/chatSettings/ChatSettingsContextProvider.tsx";
|
|
|
|
| 2 |
|
| 3 |
import Chat from "./chat/Chat.tsx";
|
| 4 |
+
import Header from "./layout/Header.tsx";
|
| 5 |
import ThemeContextProvider from "./utils/context/theme/ThemeContextProvider.tsx";
|
| 6 |
|
| 7 |
export default function App() {
|
| 8 |
return (
|
| 9 |
<ThemeContextProvider>
|
| 10 |
<ChatSettingsContextProvider>
|
| 11 |
+
<div className="mx-auto flex h-screen w-full max-w-4xl flex-col">
|
| 12 |
+
<Header className="" />
|
| 13 |
+
<Chat className="flex-grow" />
|
| 14 |
+
</div>
|
| 15 |
</ChatSettingsContextProvider>
|
| 16 |
</ThemeContextProvider>
|
| 17 |
);
|
| 18 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/chat/Chat.tsx
CHANGED
|
@@ -1,15 +1,22 @@
|
|
| 1 |
-
import { type Message } from "@huggingface/transformers";
|
| 2 |
import { Button, Card, Slider } from "@theme";
|
| 3 |
import { useChatSettings } from "@utils/context/chatSettings/useChatSettings.ts";
|
| 4 |
import { formatBytes } from "@utils/format.ts";
|
| 5 |
import { MODELS } from "@utils/models.ts";
|
| 6 |
import { TOOLS } from "@utils/tools.ts";
|
| 7 |
import { Settings } from "lucide-react";
|
| 8 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
import TextGeneration from "../textGeneration/TextGeneration.ts";
|
| 11 |
import cn from "../utils/classnames.ts";
|
| 12 |
import ChatForm from "./ChatForm.tsx";
|
|
|
|
|
|
|
| 13 |
|
| 14 |
enum State {
|
| 15 |
IDLE,
|
|
@@ -23,16 +30,25 @@ export default function Chat({ className = "" }: { className?: string }) {
|
|
| 23 |
const initializedModelKey = useRef<string>(null);
|
| 24 |
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
| 25 |
const [state, setState] = useState<State>(State.IDLE);
|
| 26 |
-
const [messages, setMessages] = useState<Array<Message>>([]);
|
| 27 |
const settingsKey = useRef<string>(null);
|
| 28 |
|
| 29 |
const generator = useMemo(() => new TextGeneration(), []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
useEffect(() => {
|
| 32 |
const newSettingsKey = JSON.stringify(settings);
|
| 33 |
if (!settings || settingsKey.current === newSettingsKey) return;
|
| 34 |
settingsKey.current = newSettingsKey;
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}, [settings]);
|
| 37 |
|
| 38 |
if (!settings) return;
|
|
@@ -54,18 +70,34 @@ export default function Chat({ className = "" }: { className?: string }) {
|
|
| 54 |
await initializeModel();
|
| 55 |
}
|
| 56 |
setState(State.GENERATING);
|
| 57 |
-
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
conversation,
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
};
|
| 70 |
|
| 71 |
const model = MODELS[settings.modelKey];
|
|
@@ -107,14 +139,42 @@ export default function Chat({ className = "" }: { className?: string }) {
|
|
| 107 |
</div>
|
| 108 |
</div>
|
| 109 |
) : (
|
| 110 |
-
<div>
|
| 111 |
-
{messages
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
</div>
|
| 115 |
)}
|
| 116 |
<Card className="mt-auto">
|
| 117 |
-
<ChatForm
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
</Card>
|
| 119 |
<div className="flex justify-between dark:border-gray-700">
|
| 120 |
<Button
|
|
|
|
|
|
|
| 1 |
import { Button, Card, Slider } from "@theme";
|
| 2 |
import { useChatSettings } from "@utils/context/chatSettings/useChatSettings.ts";
|
| 3 |
import { formatBytes } from "@utils/format.ts";
|
| 4 |
import { MODELS } from "@utils/models.ts";
|
| 5 |
import { TOOLS } from "@utils/tools.ts";
|
| 6 |
import { Settings } from "lucide-react";
|
| 7 |
+
import {
|
| 8 |
+
useEffect,
|
| 9 |
+
useMemo,
|
| 10 |
+
useRef,
|
| 11 |
+
useState,
|
| 12 |
+
useSyncExternalStore,
|
| 13 |
+
} from "react";
|
| 14 |
|
| 15 |
import TextGeneration from "../textGeneration/TextGeneration.ts";
|
| 16 |
import cn from "../utils/classnames.ts";
|
| 17 |
import ChatForm from "./ChatForm.tsx";
|
| 18 |
+
import MessageContent from "./MessageContent.tsx";
|
| 19 |
+
import MessageToolCall from "./MessageToolCall.tsx";
|
| 20 |
|
| 21 |
enum State {
|
| 22 |
IDLE,
|
|
|
|
| 30 |
const initializedModelKey = useRef<string>(null);
|
| 31 |
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
| 32 |
const [state, setState] = useState<State>(State.IDLE);
|
|
|
|
| 33 |
const settingsKey = useRef<string>(null);
|
| 34 |
|
| 35 |
const generator = useMemo(() => new TextGeneration(), []);
|
| 36 |
+
const getSnapshot = () => generator.chatMessages;
|
| 37 |
+
const messages = useSyncExternalStore(
|
| 38 |
+
generator.onChatMessageUpdate,
|
| 39 |
+
getSnapshot
|
| 40 |
+
);
|
| 41 |
|
| 42 |
useEffect(() => {
|
| 43 |
const newSettingsKey = JSON.stringify(settings);
|
| 44 |
if (!settings || settingsKey.current === newSettingsKey) return;
|
| 45 |
settingsKey.current = newSettingsKey;
|
| 46 |
+
generator.initializeConversation(
|
| 47 |
+
TOOLS.filter((tool) => settings.tools.includes(tool.name)),
|
| 48 |
+
settings.temperature,
|
| 49 |
+
settings.enableThinking,
|
| 50 |
+
settings.systemPrompt
|
| 51 |
+
);
|
| 52 |
}, [settings]);
|
| 53 |
|
| 54 |
if (!settings) return;
|
|
|
|
| 70 |
await initializeModel();
|
| 71 |
}
|
| 72 |
setState(State.GENERATING);
|
| 73 |
+
await generator.runAgent(prompt);
|
| 74 |
|
| 75 |
+
/*let nextPrompt = prompt;
|
| 76 |
+
while (nextPrompt) {
|
| 77 |
+
const conversation = [...messages, { role: "user", content: prompt }];
|
| 78 |
+
setMessages(conversation);
|
| 79 |
+
|
| 80 |
+
const answer: Message = {
|
| 81 |
+
role: "assistant",
|
| 82 |
+
content: "",
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const { response, modelUsage } = await generator.generateText(
|
| 86 |
+
settings.modelKey,
|
| 87 |
+
conversation,
|
| 88 |
+
[],
|
| 89 |
+
settings.temperature,
|
| 90 |
+
settings.enableThinking,
|
| 91 |
+
(chunk) => {
|
| 92 |
+
answer.content += chunk;
|
| 93 |
+
setMessages([...conversation, answer]);
|
| 94 |
+
}
|
| 95 |
+
);
|
| 96 |
+
console.log(response, modelUsage);
|
| 97 |
+
setMessages([...conversation, { role: "assistant", content: response }]);
|
| 98 |
+
}*/
|
| 99 |
+
|
| 100 |
+
setState(State.READY);
|
| 101 |
};
|
| 102 |
|
| 103 |
const model = MODELS[settings.modelKey];
|
|
|
|
| 139 |
</div>
|
| 140 |
</div>
|
| 141 |
) : (
|
| 142 |
+
<div className="flex flex-col gap-4">
|
| 143 |
+
{messages
|
| 144 |
+
.filter(({ role }) => role !== "system")
|
| 145 |
+
.map((message, index) => (
|
| 146 |
+
<div
|
| 147 |
+
className={cn("w-3/4", {
|
| 148 |
+
"self-end": message.role === "user",
|
| 149 |
+
})}
|
| 150 |
+
key={index}
|
| 151 |
+
>
|
| 152 |
+
{message.role === "user" ? (
|
| 153 |
+
<Card>
|
| 154 |
+
<p>{message.content}</p>
|
| 155 |
+
</Card>
|
| 156 |
+
) : message.role === "assistant" ? (
|
| 157 |
+
<div className="space-y-1">
|
| 158 |
+
{message.content.map((part) =>
|
| 159 |
+
part.type === "tool" ? (
|
| 160 |
+
<MessageToolCall tool={part} />
|
| 161 |
+
) : (
|
| 162 |
+
<MessageContent content={part.content} />
|
| 163 |
+
)
|
| 164 |
+
)}
|
| 165 |
+
</div>
|
| 166 |
+
) : null}
|
| 167 |
+
</div>
|
| 168 |
+
))}
|
| 169 |
</div>
|
| 170 |
)}
|
| 171 |
<Card className="mt-auto">
|
| 172 |
+
<ChatForm
|
| 173 |
+
disabled={!ready}
|
| 174 |
+
onSubmit={(prompt) => generate(prompt)}
|
| 175 |
+
isGenerating={state === State.GENERATING}
|
| 176 |
+
onAbort={() => generator.abort()}
|
| 177 |
+
/>
|
| 178 |
</Card>
|
| 179 |
<div className="flex justify-between dark:border-gray-700">
|
| 180 |
<Button
|
src/chat/ChatForm.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
-
import {
|
|
|
|
|
|
|
| 2 |
import { Controller, useForm } from "react-hook-form";
|
| 3 |
|
| 4 |
-
import cn from "../utils/classnames.ts";
|
| 5 |
-
|
| 6 |
interface FormParams {
|
| 7 |
input: string;
|
| 8 |
}
|
|
@@ -11,31 +11,29 @@ export default function ChatForm({
|
|
| 11 |
className = "",
|
| 12 |
onSubmit,
|
| 13 |
disabled,
|
|
|
|
|
|
|
| 14 |
}: {
|
| 15 |
className?: string;
|
| 16 |
onSubmit: (prompt: string) => void;
|
| 17 |
disabled: boolean;
|
|
|
|
|
|
|
| 18 |
}) {
|
| 19 |
-
const {
|
| 20 |
-
control,
|
| 21 |
-
formState: { errors },
|
| 22 |
-
handleSubmit,
|
| 23 |
-
reset,
|
| 24 |
-
watch,
|
| 25 |
-
setValue,
|
| 26 |
-
} = useForm<FormParams>({
|
| 27 |
defaultValues: {
|
| 28 |
input: "",
|
| 29 |
},
|
| 30 |
});
|
| 31 |
|
| 32 |
-
const userPrompt = watch("input");
|
| 33 |
-
|
| 34 |
return (
|
| 35 |
<div className={cn(className)}>
|
| 36 |
<form
|
| 37 |
-
className="-
|
| 38 |
-
onSubmit={handleSubmit((data) =>
|
|
|
|
|
|
|
|
|
|
| 39 |
>
|
| 40 |
<Controller
|
| 41 |
name="input"
|
|
@@ -45,7 +43,6 @@ export default function ChatForm({
|
|
| 45 |
<input
|
| 46 |
id="text-input"
|
| 47 |
placeholder="Type your message..."
|
| 48 |
-
//error={errors.input?.message as string}
|
| 49 |
disabled={disabled}
|
| 50 |
value={field.value}
|
| 51 |
onChange={field.onChange}
|
|
@@ -56,6 +53,13 @@ export default function ChatForm({
|
|
| 56 |
/>
|
| 57 |
)}
|
| 58 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
</form>
|
| 60 |
</div>
|
| 61 |
);
|
|
|
|
| 1 |
+
import { Button, Tooltip } from "@theme";
|
| 2 |
+
import cn from "@utils/classnames.ts";
|
| 3 |
+
import { ArrowUp, Square } from "lucide-react";
|
| 4 |
import { Controller, useForm } from "react-hook-form";
|
| 5 |
|
|
|
|
|
|
|
| 6 |
interface FormParams {
|
| 7 |
input: string;
|
| 8 |
}
|
|
|
|
| 11 |
className = "",
|
| 12 |
onSubmit,
|
| 13 |
disabled,
|
| 14 |
+
isGenerating,
|
| 15 |
+
onAbort,
|
| 16 |
}: {
|
| 17 |
className?: string;
|
| 18 |
onSubmit: (prompt: string) => void;
|
| 19 |
disabled: boolean;
|
| 20 |
+
isGenerating: boolean;
|
| 21 |
+
onAbort: () => Promise<void>;
|
| 22 |
}) {
|
| 23 |
+
const { control, handleSubmit, reset } = useForm<FormParams>({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
defaultValues: {
|
| 25 |
input: "",
|
| 26 |
},
|
| 27 |
});
|
| 28 |
|
|
|
|
|
|
|
| 29 |
return (
|
| 30 |
<div className={cn(className)}>
|
| 31 |
<form
|
| 32 |
+
className="flex items-center"
|
| 33 |
+
onSubmit={handleSubmit((data) => {
|
| 34 |
+
onSubmit(data.input);
|
| 35 |
+
reset();
|
| 36 |
+
})}
|
| 37 |
>
|
| 38 |
<Controller
|
| 39 |
name="input"
|
|
|
|
| 43 |
<input
|
| 44 |
id="text-input"
|
| 45 |
placeholder="Type your message..."
|
|
|
|
| 46 |
disabled={disabled}
|
| 47 |
value={field.value}
|
| 48 |
onChange={field.onChange}
|
|
|
|
| 53 |
/>
|
| 54 |
)}
|
| 55 |
/>
|
| 56 |
+
{isGenerating ? (
|
| 57 |
+
<Tooltip text="Cancel">
|
| 58 |
+
<Button type="button" onClick={onAbort} iconLeft={<Square />} />
|
| 59 |
+
</Tooltip>
|
| 60 |
+
) : (
|
| 61 |
+
<Button type="submit" iconLeft={<ArrowUp />} disabled={disabled} />
|
| 62 |
+
)}
|
| 63 |
</form>
|
| 64 |
</div>
|
| 65 |
);
|
src/chat/MessageContent.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Button } from "@theme";
|
| 2 |
+
import cn from "@utils/classnames.ts";
|
| 3 |
+
import { Lightbulb } from "lucide-react";
|
| 4 |
+
import { useEffect, useState } from "react";
|
| 5 |
+
import showdown from "showdown";
|
| 6 |
+
|
| 7 |
+
const converter = new showdown.Converter();
|
| 8 |
+
interface ParsedContent {
|
| 9 |
+
thinkContent: string | null;
|
| 10 |
+
afterContent: string;
|
| 11 |
+
isThinking: boolean;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
function parseThinkTags(content: string): ParsedContent {
|
| 15 |
+
const openTagIndex = content.indexOf("<think>");
|
| 16 |
+
|
| 17 |
+
if (openTagIndex === -1) {
|
| 18 |
+
return {
|
| 19 |
+
thinkContent: null,
|
| 20 |
+
afterContent: content,
|
| 21 |
+
isThinking: false,
|
| 22 |
+
};
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const closeTagIndex = content.indexOf("</think>");
|
| 26 |
+
|
| 27 |
+
if (closeTagIndex === -1) {
|
| 28 |
+
return {
|
| 29 |
+
thinkContent: content.slice(openTagIndex + 7),
|
| 30 |
+
afterContent: "",
|
| 31 |
+
isThinking: true,
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
return {
|
| 36 |
+
thinkContent: content.slice(openTagIndex + 7, closeTagIndex),
|
| 37 |
+
afterContent: content.slice(closeTagIndex + 8),
|
| 38 |
+
isThinking: false,
|
| 39 |
+
};
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
const PROSE_CLASS_NAME =
|
| 43 |
+
"prose dark:prose-invert prose-headings:font-semibold prose-headings:mt-4 prose-headings:mb-2 prose-h3:text-base prose-p:my-2 prose-ul:my-2 prose-li:my-0";
|
| 44 |
+
|
| 45 |
+
export default function MessageContent({ content }: { content: string }) {
|
| 46 |
+
const [showThinking, setShowThinking] = useState(false);
|
| 47 |
+
const [thinkingTime, setThinkingTime] = useState(0);
|
| 48 |
+
const [forceThinkingComplete, setForceThinkingComplete] = useState(false);
|
| 49 |
+
const parsed = parseThinkTags(content);
|
| 50 |
+
|
| 51 |
+
// Detect when content stops changing and force thinking to complete
|
| 52 |
+
useEffect(() => {
|
| 53 |
+
if (parsed.isThinking) {
|
| 54 |
+
const timeout = setTimeout(() => {
|
| 55 |
+
setForceThinkingComplete(true);
|
| 56 |
+
}, 1000);
|
| 57 |
+
|
| 58 |
+
return () => clearTimeout(timeout);
|
| 59 |
+
} else {
|
| 60 |
+
setForceThinkingComplete(false);
|
| 61 |
+
}
|
| 62 |
+
}, [content, parsed.isThinking]);
|
| 63 |
+
|
| 64 |
+
useEffect(() => {
|
| 65 |
+
if (parsed.isThinking && !forceThinkingComplete) {
|
| 66 |
+
const startTime = Date.now();
|
| 67 |
+
const interval = setInterval(() => {
|
| 68 |
+
setThinkingTime((Date.now() - startTime) / 1000);
|
| 69 |
+
}, 100);
|
| 70 |
+
return () => clearInterval(interval);
|
| 71 |
+
}
|
| 72 |
+
}, [parsed.isThinking, forceThinkingComplete]);
|
| 73 |
+
|
| 74 |
+
const isThinking = parsed.isThinking && !forceThinkingComplete;
|
| 75 |
+
|
| 76 |
+
if (!parsed.thinkContent) {
|
| 77 |
+
return (
|
| 78 |
+
<div
|
| 79 |
+
className={cn(PROSE_CLASS_NAME, "max-w-none")}
|
| 80 |
+
dangerouslySetInnerHTML={{
|
| 81 |
+
__html: converter.makeHtml(content),
|
| 82 |
+
}}
|
| 83 |
+
/>
|
| 84 |
+
);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
return (
|
| 88 |
+
<div className="space-y-2">
|
| 89 |
+
<div>
|
| 90 |
+
<Button
|
| 91 |
+
variant="ghost"
|
| 92 |
+
color="mono"
|
| 93 |
+
size="xs"
|
| 94 |
+
onClick={() => setShowThinking(!showThinking)}
|
| 95 |
+
className="-ml-2 opacity-50"
|
| 96 |
+
loading={isThinking}
|
| 97 |
+
iconLeft={<Lightbulb />}
|
| 98 |
+
>
|
| 99 |
+
{isThinking ? "Thinking for" : "Thought for"}{" "}
|
| 100 |
+
{thinkingTime.toFixed(1)}s ({showThinking ? "Hide" : "Show"})
|
| 101 |
+
</Button>
|
| 102 |
+
{showThinking && (
|
| 103 |
+
<p className="max-w-none text-sm">{parsed.thinkContent}</p>
|
| 104 |
+
)}
|
| 105 |
+
</div>
|
| 106 |
+
{parsed.afterContent && (
|
| 107 |
+
<div
|
| 108 |
+
className={cn(PROSE_CLASS_NAME, "max-w-none")}
|
| 109 |
+
dangerouslySetInnerHTML={{
|
| 110 |
+
__html: converter.makeHtml(parsed.afterContent),
|
| 111 |
+
}}
|
| 112 |
+
/>
|
| 113 |
+
)}
|
| 114 |
+
</div>
|
| 115 |
+
);
|
| 116 |
+
}
|
src/chat/MessageToolCall.tsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Wrench } from "lucide-react";
|
| 2 |
+
import { useState } from "react";
|
| 3 |
+
|
| 4 |
+
import type { ChatMessageAssistantTool } from "../textGeneration/types.ts";
|
| 5 |
+
import { Loader } from "../theme";
|
| 6 |
+
import cn from "../utils/classnames.ts";
|
| 7 |
+
|
| 8 |
+
export default function MessageToolCall({
|
| 9 |
+
tool,
|
| 10 |
+
className = "",
|
| 11 |
+
}: {
|
| 12 |
+
tool: ChatMessageAssistantTool;
|
| 13 |
+
className?: string;
|
| 14 |
+
}) {
|
| 15 |
+
const [expanded, setExpanded] = useState<boolean>(false);
|
| 16 |
+
|
| 17 |
+
const isLoading = tool.result === "";
|
| 18 |
+
|
| 19 |
+
return (
|
| 20 |
+
<div
|
| 21 |
+
className={cn(
|
| 22 |
+
className,
|
| 23 |
+
"rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
|
| 24 |
+
)}
|
| 25 |
+
>
|
| 26 |
+
<div className="flex items-center justify-between gap-3">
|
| 27 |
+
<button
|
| 28 |
+
className="flex cursor-pointer items-center gap-2 text-xs text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100"
|
| 29 |
+
onClick={() => setExpanded(!expanded)}
|
| 30 |
+
>
|
| 31 |
+
{isLoading ? <Loader size="xs" /> : <Wrench className="h-3 w-3" />}
|
| 32 |
+
{isLoading ? "calling tool" : "called tool"} <b>{tool.name}</b>
|
| 33 |
+
</button>
|
| 34 |
+
</div>
|
| 35 |
+
{expanded && (
|
| 36 |
+
<div className="mt-2 space-y-2">
|
| 37 |
+
<div>
|
| 38 |
+
<div className="mb-1 text-xs text-gray-600 dark:text-gray-400">
|
| 39 |
+
Function:
|
| 40 |
+
</div>
|
| 41 |
+
<code className="block overflow-hidden rounded bg-white p-2 font-mono text-xs text-blue-600 dark:bg-gray-900 dark:text-blue-400">
|
| 42 |
+
{tool.functionSignature}
|
| 43 |
+
</code>
|
| 44 |
+
</div>
|
| 45 |
+
<div>
|
| 46 |
+
<div className="mb-1 text-xs text-gray-600 dark:text-gray-400">
|
| 47 |
+
Result:
|
| 48 |
+
</div>
|
| 49 |
+
<div className="overflow-hidden rounded bg-white p-2 text-sm whitespace-pre-wrap text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
| 50 |
+
{tool.result || "loading.."}
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
)}
|
| 55 |
+
</div>
|
| 56 |
+
);
|
| 57 |
+
}
|
src/layout/Header.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Button } from "@theme";
|
| 2 |
+
import cn from "@utils/classnames.ts";
|
| 3 |
+
import { useTheme } from "@utils/context/theme/useTheme.ts";
|
| 4 |
+
import { MonitorCog, Moon, Sun } from "lucide-react";
|
| 5 |
+
|
| 6 |
+
function Header({ className = "" }: { className?: string }) {
|
| 7 |
+
const { storedTheme, toggleTheme } = useTheme();
|
| 8 |
+
|
| 9 |
+
return (
|
| 10 |
+
<div className={cn(className)}>
|
| 11 |
+
<div className="flex items-center justify-between gap-2 border-b-1 border-gray-200 px-4 py-2 dark:border-gray-800">
|
| 12 |
+
<h1 className="font-bold">Transformers.js TextGeneration</h1>
|
| 13 |
+
<Button
|
| 14 |
+
color="mono"
|
| 15 |
+
variant="ghost"
|
| 16 |
+
onClick={toggleTheme}
|
| 17 |
+
size="sm"
|
| 18 |
+
aria-label="Toggle theme"
|
| 19 |
+
iconRight={
|
| 20 |
+
storedTheme === "system" ? (
|
| 21 |
+
<MonitorCog className="h-3 w-3" />
|
| 22 |
+
) : storedTheme === "light" ? (
|
| 23 |
+
<Sun className="h-3 w-3" />
|
| 24 |
+
) : (
|
| 25 |
+
<Moon className="h-3 w-3" />
|
| 26 |
+
)
|
| 27 |
+
}
|
| 28 |
+
>
|
| 29 |
+
Theme:{" "}
|
| 30 |
+
</Button>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export default Header;
|
src/textGeneration/TextGeneration.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
| 1 |
import { type Message } from "@huggingface/transformers";
|
| 2 |
|
| 3 |
-
import type { ChatTemplateTool } from "../utils/webMcp.ts";
|
| 4 |
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
type ModelUsage,
|
| 6 |
type Request,
|
| 7 |
RequestType,
|
|
@@ -12,6 +21,15 @@ import {
|
|
| 12 |
export default class TextGeneration {
|
| 13 |
private worker: Worker;
|
| 14 |
private requestId: number = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
constructor() {
|
| 17 |
this.worker = new Worker(
|
|
@@ -22,6 +40,26 @@ export default class TextGeneration {
|
|
| 22 |
);
|
| 23 |
}
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
private postWorkerMessage = (request: Request) =>
|
| 26 |
this.worker.postMessage(request);
|
| 27 |
private addWorkerEventListener = (
|
|
@@ -47,6 +85,7 @@ export default class TextGeneration {
|
|
| 47 |
if (data.type !== ResponseType.INITIALIZE_MODEL) return;
|
| 48 |
if (data.done) {
|
| 49 |
this.removeWorkerEventListener(listener);
|
|
|
|
| 50 |
resolve(data.progress);
|
| 51 |
}
|
| 52 |
onDownload(data.progress);
|
|
@@ -61,10 +100,22 @@ export default class TextGeneration {
|
|
| 61 |
});
|
| 62 |
}
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
public async abort() {
|
| 65 |
return new Promise<void>((resolve, reject) => {
|
| 66 |
const requestId = this.requestId++;
|
| 67 |
-
|
| 68 |
const listener = ({ data }: MessageEvent<Response>) => {
|
| 69 |
if (data.requestId !== requestId) return;
|
| 70 |
if (data.type === ResponseType.ERROR) {
|
|
@@ -85,47 +136,156 @@ export default class TextGeneration {
|
|
| 85 |
});
|
| 86 |
}
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
if (data.requestId !== requestId) return;
|
| 102 |
-
if (data.type === ResponseType.ERROR) {
|
| 103 |
-
this.removeWorkerEventListener(listener);
|
| 104 |
-
reject(data.message);
|
| 105 |
-
}
|
| 106 |
-
if (data.type === ResponseType.GENERATE_TEXT_DONE) {
|
| 107 |
-
this.removeWorkerEventListener(listener);
|
| 108 |
-
resolve({
|
| 109 |
-
response: data.response,
|
| 110 |
-
modelUsage: data.modelUsage,
|
| 111 |
-
});
|
| 112 |
-
}
|
| 113 |
-
if (data.type === ResponseType.GENERATE_TEXT_CHUNK) {
|
| 114 |
-
onChunkUpdate(data.chunk);
|
| 115 |
-
}
|
| 116 |
-
};
|
| 117 |
-
|
| 118 |
-
this.addWorkerEventListener(listener);
|
| 119 |
-
this.postWorkerMessage({
|
| 120 |
-
type: RequestType.GENERATE_MESSAGE,
|
| 121 |
-
modelKey,
|
| 122 |
-
messages,
|
| 123 |
-
tools,
|
| 124 |
-
requestId,
|
| 125 |
-
temperature,
|
| 126 |
-
enableThinking,
|
| 127 |
-
});
|
| 128 |
}
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
}
|
| 131 |
}
|
|
|
|
| 1 |
import { type Message } from "@huggingface/transformers";
|
| 2 |
|
|
|
|
| 3 |
import {
|
| 4 |
+
type WebMCPTool,
|
| 5 |
+
executeToolCall,
|
| 6 |
+
splitResponse,
|
| 7 |
+
webMCPToolToChatTemplateTool,
|
| 8 |
+
} from "../utils/webMcp.ts";
|
| 9 |
+
import {
|
| 10 |
+
type ChatMessage,
|
| 11 |
+
type ChatMessageAssistant,
|
| 12 |
+
type ChatMessageAssistantResponse,
|
| 13 |
+
type ChatMessageAssistantTool,
|
| 14 |
type ModelUsage,
|
| 15 |
type Request,
|
| 16 |
RequestType,
|
|
|
|
| 21 |
export default class TextGeneration {
|
| 22 |
private worker: Worker;
|
| 23 |
private requestId: number = 0;
|
| 24 |
+
private modelKey: string | null = null;
|
| 25 |
+
private tools: Array<WebMCPTool> | null = null;
|
| 26 |
+
private temperature: number | null = null;
|
| 27 |
+
private enableThinking: boolean | null = null;
|
| 28 |
+
private messages: Array<Message> = [];
|
| 29 |
+
private _chatMessages: Array<ChatMessage> = [];
|
| 30 |
+
private chatMessagesListener: Array<
|
| 31 |
+
(chatMessages: Array<ChatMessage>) => void
|
| 32 |
+
> = [];
|
| 33 |
|
| 34 |
constructor() {
|
| 35 |
this.worker = new Worker(
|
|
|
|
| 40 |
);
|
| 41 |
}
|
| 42 |
|
| 43 |
+
get chatMessages() {
|
| 44 |
+
return this._chatMessages;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
set chatMessages(chatMessages: Array<ChatMessage>) {
|
| 48 |
+
this._chatMessages = chatMessages;
|
| 49 |
+
this.chatMessagesListener.forEach((listener) => listener(chatMessages));
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
public onChatMessageUpdate = (
|
| 53 |
+
callback: (messages: Array<ChatMessage>) => void
|
| 54 |
+
) => {
|
| 55 |
+
this.chatMessagesListener.push(callback);
|
| 56 |
+
return () => {
|
| 57 |
+
this.chatMessagesListener = this.chatMessagesListener.filter(
|
| 58 |
+
(listener) => listener !== callback
|
| 59 |
+
);
|
| 60 |
+
};
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
private postWorkerMessage = (request: Request) =>
|
| 64 |
this.worker.postMessage(request);
|
| 65 |
private addWorkerEventListener = (
|
|
|
|
| 85 |
if (data.type !== ResponseType.INITIALIZE_MODEL) return;
|
| 86 |
if (data.done) {
|
| 87 |
this.removeWorkerEventListener(listener);
|
| 88 |
+
this.modelKey = modelKey;
|
| 89 |
resolve(data.progress);
|
| 90 |
}
|
| 91 |
onDownload(data.progress);
|
|
|
|
| 100 |
});
|
| 101 |
}
|
| 102 |
|
| 103 |
+
public initializeConversation(
|
| 104 |
+
tools: Array<WebMCPTool> = [],
|
| 105 |
+
temperature: number,
|
| 106 |
+
enableThinking: boolean,
|
| 107 |
+
systemPrompt: string
|
| 108 |
+
) {
|
| 109 |
+
this.tools = tools;
|
| 110 |
+
this.temperature = temperature;
|
| 111 |
+
this.enableThinking = enableThinking;
|
| 112 |
+
this.messages = [{ role: "system", content: systemPrompt }];
|
| 113 |
+
this.chatMessages = [{ role: "system", content: systemPrompt }];
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
public async abort() {
|
| 117 |
return new Promise<void>((resolve, reject) => {
|
| 118 |
const requestId = this.requestId++;
|
|
|
|
| 119 |
const listener = ({ data }: MessageEvent<Response>) => {
|
| 120 |
if (data.requestId !== requestId) return;
|
| 121 |
if (data.type === ResponseType.ERROR) {
|
|
|
|
| 136 |
});
|
| 137 |
}
|
| 138 |
|
| 139 |
+
private generateText = (
|
| 140 |
+
prompt: string,
|
| 141 |
+
role: "user" | "tool",
|
| 142 |
+
onResponseUpdate: (response: string) => void = () => {}
|
| 143 |
+
) => {
|
| 144 |
+
return new Promise<{
|
| 145 |
+
response: string;
|
| 146 |
+
modelUsage: ModelUsage;
|
| 147 |
+
interrupted: boolean;
|
| 148 |
+
}>((resolve, reject) => {
|
| 149 |
+
if (this.modelKey === null) {
|
| 150 |
+
reject("Model not initialized");
|
| 151 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
+
|
| 154 |
+
if (
|
| 155 |
+
this.tools === null ||
|
| 156 |
+
this.temperature === null ||
|
| 157 |
+
this.enableThinking === null
|
| 158 |
+
) {
|
| 159 |
+
reject("Conversation not initialized");
|
| 160 |
+
return;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
const requestId = this.requestId++;
|
| 164 |
+
this.messages = [...this.messages, { role, content: prompt }];
|
| 165 |
+
this.messages.push({ role: "assistant", content: "" });
|
| 166 |
+
|
| 167 |
+
let response = "";
|
| 168 |
+
|
| 169 |
+
const listener = ({ data }: MessageEvent<Response>) => {
|
| 170 |
+
if (data.requestId !== requestId) return;
|
| 171 |
+
if (data.type === ResponseType.ERROR) {
|
| 172 |
+
this.removeWorkerEventListener(listener);
|
| 173 |
+
reject(data.message);
|
| 174 |
+
}
|
| 175 |
+
if (data.type === ResponseType.GENERATE_TEXT_DONE) {
|
| 176 |
+
this.removeWorkerEventListener(listener);
|
| 177 |
+
resolve({
|
| 178 |
+
response: data.response,
|
| 179 |
+
modelUsage: data.modelUsage,
|
| 180 |
+
interrupted: data.interrupted,
|
| 181 |
+
});
|
| 182 |
+
}
|
| 183 |
+
if (data.type === ResponseType.GENERATE_TEXT_CHUNK) {
|
| 184 |
+
response = response + data.chunk;
|
| 185 |
+
onResponseUpdate(response);
|
| 186 |
+
}
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
this.addWorkerEventListener(listener);
|
| 190 |
+
this.postWorkerMessage({
|
| 191 |
+
type: RequestType.GENERATE_MESSAGE,
|
| 192 |
+
modelKey: this.modelKey,
|
| 193 |
+
messages: this.messages,
|
| 194 |
+
tools: this.tools.map(webMCPToolToChatTemplateTool),
|
| 195 |
+
requestId,
|
| 196 |
+
temperature: this.temperature,
|
| 197 |
+
enableThinking: this.enableThinking,
|
| 198 |
+
});
|
| 199 |
+
});
|
| 200 |
+
};
|
| 201 |
+
|
| 202 |
+
public async runAgent(prompt: string): Promise<void> {
|
| 203 |
+
let isUser = true;
|
| 204 |
+
|
| 205 |
+
this.chatMessages = [
|
| 206 |
+
...this.chatMessages,
|
| 207 |
+
{ role: "user", content: prompt },
|
| 208 |
+
];
|
| 209 |
+
|
| 210 |
+
while (prompt) {
|
| 211 |
+
const prevChatMessages = this.chatMessages;
|
| 212 |
+
const assistantMessage: ChatMessageAssistant = {
|
| 213 |
+
role: "assistant",
|
| 214 |
+
content: [],
|
| 215 |
+
interrupted: false,
|
| 216 |
+
};
|
| 217 |
+
|
| 218 |
+
this.chatMessages = [...prevChatMessages, assistantMessage];
|
| 219 |
+
|
| 220 |
+
const { modelUsage, interrupted } = await this.generateText(
|
| 221 |
+
prompt,
|
| 222 |
+
isUser ? "user" : "tool",
|
| 223 |
+
(partialResponse) => {
|
| 224 |
+
const parts = splitResponse(partialResponse);
|
| 225 |
+
assistantMessage.content = parts.map((part) =>
|
| 226 |
+
typeof part === "string"
|
| 227 |
+
? ({
|
| 228 |
+
type: "response",
|
| 229 |
+
content: part,
|
| 230 |
+
} as ChatMessageAssistantResponse)
|
| 231 |
+
: ({
|
| 232 |
+
type: "tool",
|
| 233 |
+
result: null,
|
| 234 |
+
functionSignature: `${part.name}(${JSON.stringify(
|
| 235 |
+
part.arguments
|
| 236 |
+
)})`,
|
| 237 |
+
...part,
|
| 238 |
+
} as ChatMessageAssistantTool)
|
| 239 |
+
);
|
| 240 |
+
this.chatMessages = [...prevChatMessages, assistantMessage];
|
| 241 |
+
}
|
| 242 |
+
);
|
| 243 |
+
isUser = false;
|
| 244 |
+
|
| 245 |
+
assistantMessage.modelUsage = modelUsage;
|
| 246 |
+
assistantMessage.interrupted = interrupted;
|
| 247 |
+
this.chatMessages = [...prevChatMessages, assistantMessage];
|
| 248 |
+
|
| 249 |
+
const toolCalls = assistantMessage.content.filter(
|
| 250 |
+
(c) => c.type === "tool"
|
| 251 |
+
);
|
| 252 |
+
if (toolCalls.length === 0) {
|
| 253 |
+
prompt = "";
|
| 254 |
+
continue;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
const toolResponses = await Promise.all(
|
| 258 |
+
toolCalls.map((tool) =>
|
| 259 |
+
executeToolCall(
|
| 260 |
+
{
|
| 261 |
+
name: tool.name,
|
| 262 |
+
arguments: tool.arguments,
|
| 263 |
+
id: tool.id,
|
| 264 |
+
},
|
| 265 |
+
this.tools || []
|
| 266 |
+
)
|
| 267 |
+
)
|
| 268 |
+
);
|
| 269 |
+
|
| 270 |
+
assistantMessage.modelUsage = modelUsage;
|
| 271 |
+
assistantMessage.content = assistantMessage.content.map((message) => {
|
| 272 |
+
if (message.type === "tool") {
|
| 273 |
+
const toolResponse = toolResponses.find(
|
| 274 |
+
(response) => response.id === message.id
|
| 275 |
+
);
|
| 276 |
+
if (toolResponse) {
|
| 277 |
+
return {
|
| 278 |
+
...message,
|
| 279 |
+
result: toolResponse.result,
|
| 280 |
+
};
|
| 281 |
+
}
|
| 282 |
+
return message;
|
| 283 |
+
} else {
|
| 284 |
+
return message;
|
| 285 |
+
}
|
| 286 |
+
});
|
| 287 |
+
this.chatMessages = [...prevChatMessages, assistantMessage];
|
| 288 |
+
prompt = toolResponses.map(({ result }) => result).join("\n");
|
| 289 |
+
}
|
| 290 |
}
|
| 291 |
}
|
src/textGeneration/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { type Message } from "@huggingface/transformers";
|
| 2 |
|
| 3 |
import type { MODELS } from "../utils/models.ts";
|
| 4 |
-
import type { ChatTemplateTool } from "../utils/webMcp.ts";
|
| 5 |
|
| 6 |
export enum RequestType {
|
| 7 |
INITIALIZE_MODEL,
|
|
@@ -59,6 +59,7 @@ interface ResponseGenerateTextDone {
|
|
| 59 |
type: ResponseType.GENERATE_TEXT_DONE;
|
| 60 |
response: string;
|
| 61 |
modelUsage: ModelUsage;
|
|
|
|
| 62 |
requestId: number;
|
| 63 |
}
|
| 64 |
|
|
@@ -96,6 +97,39 @@ export type Response =
|
|
| 96 |
| ResponseInitializeModel
|
| 97 |
| ResponseError;
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
/*
|
| 100 |
interface ResponseGenerateTextDone {}
|
| 101 |
|
|
|
|
| 1 |
import { type Message } from "@huggingface/transformers";
|
| 2 |
|
| 3 |
import type { MODELS } from "../utils/models.ts";
|
| 4 |
+
import type { ChatTemplateTool, ToolCallPayload } from "../utils/webMcp.ts";
|
| 5 |
|
| 6 |
export enum RequestType {
|
| 7 |
INITIALIZE_MODEL,
|
|
|
|
| 59 |
type: ResponseType.GENERATE_TEXT_DONE;
|
| 60 |
response: string;
|
| 61 |
modelUsage: ModelUsage;
|
| 62 |
+
interrupted: boolean;
|
| 63 |
requestId: number;
|
| 64 |
}
|
| 65 |
|
|
|
|
| 97 |
| ResponseInitializeModel
|
| 98 |
| ResponseError;
|
| 99 |
|
| 100 |
+
export interface ChatMessageUser {
|
| 101 |
+
role: "user";
|
| 102 |
+
content: string;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
export interface ChatMessageAssistant {
|
| 106 |
+
role: "assistant";
|
| 107 |
+
content: Array<ChatMessageAssistantResponse | ChatMessageAssistantTool>;
|
| 108 |
+
interrupted: boolean;
|
| 109 |
+
modelUsage?: ModelUsage;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
export interface ChatMessageSystem {
|
| 113 |
+
role: "system";
|
| 114 |
+
content: string;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
export interface ChatMessageAssistantResponse {
|
| 118 |
+
type: "response";
|
| 119 |
+
content: string;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
export interface ChatMessageAssistantTool extends ToolCallPayload {
|
| 123 |
+
type: "tool";
|
| 124 |
+
functionSignature: string;
|
| 125 |
+
result: string;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
export type ChatMessage =
|
| 129 |
+
| ChatMessageUser
|
| 130 |
+
| ChatMessageAssistant
|
| 131 |
+
| ChatMessageSystem;
|
| 132 |
+
|
| 133 |
/*
|
| 134 |
interface ResponseGenerateTextDone {}
|
| 135 |
|
src/textGeneration/worker/textGenerationWorker.ts
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
| 7 |
Tensor,
|
| 8 |
TextStreamer,
|
| 9 |
} from "@huggingface/transformers";
|
| 10 |
-
import { calculateDownloadProgress } from "
|
| 11 |
-
import { MODELS } from "
|
| 12 |
|
| 13 |
import {
|
| 14 |
type Request,
|
|
@@ -29,7 +29,6 @@ let cache: { pastKeyValues: any | null; key: string } = {
|
|
| 29 |
key: "",
|
| 30 |
};
|
| 31 |
let stoppingCriteria: any | null = null;
|
| 32 |
-
let abortController: AbortController = new AbortController();
|
| 33 |
|
| 34 |
const getTextGenerationPipeline = async (
|
| 35 |
modelKey: keyof typeof MODELS,
|
|
@@ -89,7 +88,6 @@ self.onmessage = async ({ data }: MessageEvent<Request>) => {
|
|
| 89 |
}
|
| 90 |
|
| 91 |
if (data.type === RequestType.GENERATE_MESSAGE_ABORT) {
|
| 92 |
-
abortController.abort();
|
| 93 |
stoppingCriteria.interrupt();
|
| 94 |
postMessage({
|
| 95 |
type: ResponseType.GENERATE_TEXT_ABORTED,
|
|
@@ -99,6 +97,7 @@ self.onmessage = async ({ data }: MessageEvent<Request>) => {
|
|
| 99 |
|
| 100 |
if (data.type === RequestType.GENERATE_MESSAGE) {
|
| 101 |
const MODEL = MODELS[data.modelKey];
|
|
|
|
| 102 |
|
| 103 |
const { messages, tools, requestId } = data;
|
| 104 |
console.log(messages, tools);
|
|
@@ -111,6 +110,8 @@ self.onmessage = async ({ data }: MessageEvent<Request>) => {
|
|
| 111 |
tools,
|
| 112 |
add_generation_prompt: true,
|
| 113 |
return_dict: true,
|
|
|
|
|
|
|
| 114 |
}) as {
|
| 115 |
input_ids: Tensor;
|
| 116 |
attention_mask: number[] | number[][] | Tensor;
|
|
@@ -128,16 +129,12 @@ self.onmessage = async ({ data }: MessageEvent<Request>) => {
|
|
| 128 |
}
|
| 129 |
};
|
| 130 |
|
| 131 |
-
const callbackFunction = (chunk: string) =>
|
| 132 |
-
if (abortController.signal?.aborted) {
|
| 133 |
-
throw new DOMException("Request cancelled", "AbortError");
|
| 134 |
-
}
|
| 135 |
postMessage({
|
| 136 |
type: ResponseType.GENERATE_TEXT_CHUNK,
|
| 137 |
chunk,
|
| 138 |
requestId,
|
| 139 |
});
|
| 140 |
-
};
|
| 141 |
|
| 142 |
const streamer = new TextStreamer(tokenizer, {
|
| 143 |
skip_prompt: true,
|
|
@@ -153,8 +150,10 @@ self.onmessage = async ({ data }: MessageEvent<Request>) => {
|
|
| 153 |
const { sequences, past_key_values } = (await model.generate({
|
| 154 |
...input,
|
| 155 |
max_new_tokens: 1024,
|
| 156 |
-
past_key_values: useCache ? cache.pastKeyValues : null,
|
| 157 |
return_dict_in_generate: true,
|
|
|
|
|
|
|
| 158 |
streamer,
|
| 159 |
})) as { sequences: Tensor; past_key_values: any };
|
| 160 |
const ended = performance.now();
|
|
@@ -184,6 +183,8 @@ self.onmessage = async ({ data }: MessageEvent<Request>) => {
|
|
| 184 |
]),
|
| 185 |
};
|
| 186 |
|
|
|
|
|
|
|
| 187 |
postMessage({
|
| 188 |
type: ResponseType.GENERATE_TEXT_DONE,
|
| 189 |
response,
|
|
@@ -196,6 +197,7 @@ self.onmessage = async ({ data }: MessageEvent<Request>) => {
|
|
| 196 |
modelKey: MODEL.modelId,
|
| 197 |
model: MODEL.title,
|
| 198 |
},
|
|
|
|
| 199 |
requestId,
|
| 200 |
});
|
| 201 |
}
|
|
|
|
| 7 |
Tensor,
|
| 8 |
TextStreamer,
|
| 9 |
} from "@huggingface/transformers";
|
| 10 |
+
import { calculateDownloadProgress } from "../../utils/calculateDownloadProgress.ts";
|
| 11 |
+
import { MODELS } from "../../utils/models.ts";
|
| 12 |
|
| 13 |
import {
|
| 14 |
type Request,
|
|
|
|
| 29 |
key: "",
|
| 30 |
};
|
| 31 |
let stoppingCriteria: any | null = null;
|
|
|
|
| 32 |
|
| 33 |
const getTextGenerationPipeline = async (
|
| 34 |
modelKey: keyof typeof MODELS,
|
|
|
|
| 88 |
}
|
| 89 |
|
| 90 |
if (data.type === RequestType.GENERATE_MESSAGE_ABORT) {
|
|
|
|
| 91 |
stoppingCriteria.interrupt();
|
| 92 |
postMessage({
|
| 93 |
type: ResponseType.GENERATE_TEXT_ABORTED,
|
|
|
|
| 97 |
|
| 98 |
if (data.type === RequestType.GENERATE_MESSAGE) {
|
| 99 |
const MODEL = MODELS[data.modelKey];
|
| 100 |
+
stoppingCriteria = new InterruptableStoppingCriteria();
|
| 101 |
|
| 102 |
const { messages, tools, requestId } = data;
|
| 103 |
console.log(messages, tools);
|
|
|
|
| 110 |
tools,
|
| 111 |
add_generation_prompt: true,
|
| 112 |
return_dict: true,
|
| 113 |
+
// @ts-expect-error
|
| 114 |
+
enable_thinking: data.enableThinking,
|
| 115 |
}) as {
|
| 116 |
input_ids: Tensor;
|
| 117 |
attention_mask: number[] | number[][] | Tensor;
|
|
|
|
| 129 |
}
|
| 130 |
};
|
| 131 |
|
| 132 |
+
const callbackFunction = (chunk: string) =>
|
|
|
|
|
|
|
|
|
|
| 133 |
postMessage({
|
| 134 |
type: ResponseType.GENERATE_TEXT_CHUNK,
|
| 135 |
chunk,
|
| 136 |
requestId,
|
| 137 |
});
|
|
|
|
| 138 |
|
| 139 |
const streamer = new TextStreamer(tokenizer, {
|
| 140 |
skip_prompt: true,
|
|
|
|
| 150 |
const { sequences, past_key_values } = (await model.generate({
|
| 151 |
...input,
|
| 152 |
max_new_tokens: 1024,
|
| 153 |
+
//past_key_values: useCache ? cache.pastKeyValues : null,
|
| 154 |
return_dict_in_generate: true,
|
| 155 |
+
temperature: data.temperature,
|
| 156 |
+
stopping_criteria: stoppingCriteria,
|
| 157 |
streamer,
|
| 158 |
})) as { sequences: Tensor; past_key_values: any };
|
| 159 |
const ended = performance.now();
|
|
|
|
| 183 |
]),
|
| 184 |
};
|
| 185 |
|
| 186 |
+
console.log("response", response);
|
| 187 |
+
|
| 188 |
postMessage({
|
| 189 |
type: ResponseType.GENERATE_TEXT_DONE,
|
| 190 |
response,
|
|
|
|
| 197 |
modelKey: MODEL.modelId,
|
| 198 |
model: MODEL.title,
|
| 199 |
},
|
| 200 |
+
interrupted: stoppingCriteria.interrupted,
|
| 201 |
requestId,
|
| 202 |
});
|
| 203 |
}
|
src/theme/button/Button.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { type ReactNode, cloneElement, isValidElement } from "react";
|
| 2 |
|
| 3 |
import cn from "../../utils/classnames.ts";
|
|
|
|
| 4 |
|
| 5 |
type ButtonColor = "primary" | "secondary" | "mono" | "danger";
|
| 6 |
type ButtonVariant = "solid" | "outline" | "ghost";
|
|
@@ -129,28 +130,7 @@ export default function Button({
|
|
| 129 |
|
| 130 |
const content = (
|
| 131 |
<>
|
| 132 |
-
{loading &&
|
| 133 |
-
<svg
|
| 134 |
-
className="h-4 w-4 animate-spin"
|
| 135 |
-
xmlns="http://www.w3.org/2000/svg"
|
| 136 |
-
fill="none"
|
| 137 |
-
viewBox="0 0 24 24"
|
| 138 |
-
>
|
| 139 |
-
<circle
|
| 140 |
-
className="opacity-25"
|
| 141 |
-
cx="12"
|
| 142 |
-
cy="12"
|
| 143 |
-
r="10"
|
| 144 |
-
stroke="currentColor"
|
| 145 |
-
strokeWidth="4"
|
| 146 |
-
/>
|
| 147 |
-
<path
|
| 148 |
-
className="opacity-75"
|
| 149 |
-
fill="currentColor"
|
| 150 |
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
| 151 |
-
/>
|
| 152 |
-
</svg>
|
| 153 |
-
)}
|
| 154 |
{!loading && iconLeft && renderIcon(iconLeft)}
|
| 155 |
{children && <span>{children}</span>}
|
| 156 |
{!loading && iconRight && renderIcon(iconRight)}
|
|
|
|
| 1 |
import { type ReactNode, cloneElement, isValidElement } from "react";
|
| 2 |
|
| 3 |
import cn from "../../utils/classnames.ts";
|
| 4 |
+
import { Loader } from "../index.ts";
|
| 5 |
|
| 6 |
type ButtonColor = "primary" | "secondary" | "mono" | "danger";
|
| 7 |
type ButtonVariant = "solid" | "outline" | "ghost";
|
|
|
|
| 130 |
|
| 131 |
const content = (
|
| 132 |
<>
|
| 133 |
+
{loading && <Loader size={size} />}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
{!loading && iconLeft && renderIcon(iconLeft)}
|
| 135 |
{children && <span>{children}</span>}
|
| 136 |
{!loading && iconRight && renderIcon(iconRight)}
|
src/theme/misc/Loader.tsx
CHANGED
|
@@ -1,9 +1,28 @@
|
|
| 1 |
import cn from "@utils/classnames.ts";
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
return (
|
| 5 |
-
<div className={cn(className, "flex items-center justify-center
|
| 6 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
</div>
|
| 8 |
);
|
| 9 |
}
|
|
|
|
| 1 |
import cn from "@utils/classnames.ts";
|
| 2 |
|
| 3 |
+
type LoaderSize = "xs" | "sm" | "md" | "lg";
|
| 4 |
+
|
| 5 |
+
interface LoaderProps {
|
| 6 |
+
className?: string;
|
| 7 |
+
size?: LoaderSize;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const sizeClasses: Record<LoaderSize, string> = {
|
| 11 |
+
xs: "h-3 w-3 border-2",
|
| 12 |
+
sm: "h-4 w-4 border-2",
|
| 13 |
+
md: "h-8 w-8 border-4",
|
| 14 |
+
lg: "h-12 w-12 border-[5px]",
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
export default function Loader({ className = "", size = "md" }: LoaderProps) {
|
| 18 |
return (
|
| 19 |
+
<div className={cn(className, "flex items-center justify-center")}>
|
| 20 |
+
<div
|
| 21 |
+
className={cn(
|
| 22 |
+
"animate-spin rounded-full border-gray-300 border-t-yellow-500 dark:border-gray-600 dark:border-t-yellow-400",
|
| 23 |
+
sizeClasses[size]
|
| 24 |
+
)}
|
| 25 |
+
/>
|
| 26 |
</div>
|
| 27 |
);
|
| 28 |
}
|
src/utils/context/chatSettings/ChatSettingsContext.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { DEFAULT_SYSTEM_PROMPT, MODELS } from "@utils/models.ts";
|
| 2 |
import { createContext } from "react";
|
| 3 |
|
| 4 |
-
import type { ChatSettingsContextType } from "./types.ts";
|
| 5 |
|
| 6 |
const ChatSettingsContext = createContext<ChatSettingsContextType>({
|
| 7 |
settings: null,
|
|
@@ -12,7 +12,7 @@ const ChatSettingsContext = createContext<ChatSettingsContextType>({
|
|
| 12 |
|
| 13 |
export default ChatSettingsContext;
|
| 14 |
|
| 15 |
-
export const DEFAULT_CHAT_SETTINGS = {
|
| 16 |
tools: [],
|
| 17 |
modelKey: Object.keys(MODELS)[0],
|
| 18 |
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
|
|
| 1 |
import { DEFAULT_SYSTEM_PROMPT, MODELS } from "@utils/models.ts";
|
| 2 |
import { createContext } from "react";
|
| 3 |
|
| 4 |
+
import type { ChatSettings, ChatSettingsContextType } from "./types.ts";
|
| 5 |
|
| 6 |
const ChatSettingsContext = createContext<ChatSettingsContextType>({
|
| 7 |
settings: null,
|
|
|
|
| 12 |
|
| 13 |
export default ChatSettingsContext;
|
| 14 |
|
| 15 |
+
export const DEFAULT_CHAT_SETTINGS: ChatSettings = {
|
| 16 |
tools: [],
|
| 17 |
modelKey: Object.keys(MODELS)[0],
|
| 18 |
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
src/utils/context/theme/ThemeContext.ts
CHANGED
|
@@ -2,6 +2,6 @@ import { createContext } from "react";
|
|
| 2 |
|
| 3 |
import type { ThemeContextType } from "./types.ts";
|
| 4 |
|
| 5 |
-
const ThemeContext = createContext<ThemeContextType
|
| 6 |
|
| 7 |
export default ThemeContext;
|
|
|
|
| 2 |
|
| 3 |
import type { ThemeContextType } from "./types.ts";
|
| 4 |
|
| 5 |
+
const ThemeContext = createContext<ThemeContextType>(null);
|
| 6 |
|
| 7 |
export default ThemeContext;
|
src/utils/models.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
export interface Model {
|
| 2 |
modelId: string;
|
| 3 |
title: string;
|
| 4 |
-
dtype: "fp16" | "q4" | "q4f16";
|
| 5 |
device: "webgpu";
|
| 6 |
size: number;
|
| 7 |
files: Record<string, number>;
|
|
@@ -38,6 +38,21 @@ export const MODELS: Record<string, Model> = {
|
|
| 38 |
"onnx/model_q4.onnx_data_1": 2_095_841_280,
|
| 39 |
},
|
| 40 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
granite350m: {
|
| 42 |
modelId: "onnx-community/granite-4.0-350m-ONNX-web",
|
| 43 |
title: "Granite-4.0 350M (fp16)",
|
|
@@ -84,6 +99,37 @@ export const MODELS: Record<string, Model> = {
|
|
| 84 |
"generation_config.json": 152,
|
| 85 |
},
|
| 86 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
} as const;
|
| 88 |
|
| 89 |
export type ModelKey = keyof typeof MODELS;
|
|
|
|
| 1 |
export interface Model {
|
| 2 |
modelId: string;
|
| 3 |
title: string;
|
| 4 |
+
dtype: "fp16" | "q4" | "q4f16" | "fp32";
|
| 5 |
device: "webgpu";
|
| 6 |
size: number;
|
| 7 |
files: Record<string, number>;
|
|
|
|
| 38 |
"onnx/model_q4.onnx_data_1": 2_095_841_280,
|
| 39 |
},
|
| 40 |
},
|
| 41 |
+
gemma3270m: {
|
| 42 |
+
modelId: "onnx-community/gemma-3-270m-it-ONNX",
|
| 43 |
+
title: "Gemma-3-270M (fp32)",
|
| 44 |
+
dtype: "fp32",
|
| 45 |
+
device: "webgpu",
|
| 46 |
+
size: 1_161_186_669,
|
| 47 |
+
files: {
|
| 48 |
+
"config.json": 1_612,
|
| 49 |
+
"tokenizer_config.json": 1_158_469,
|
| 50 |
+
"tokenizer.json": 20_323_106,
|
| 51 |
+
"generation_config.json": 172,
|
| 52 |
+
"onnx/model.onnx": 201_742,
|
| 53 |
+
"onnx/model.onnx_data": 1_139_501_568,
|
| 54 |
+
},
|
| 55 |
+
},
|
| 56 |
granite350m: {
|
| 57 |
modelId: "onnx-community/granite-4.0-350m-ONNX-web",
|
| 58 |
title: "Granite-4.0 350M (fp16)",
|
|
|
|
| 99 |
"generation_config.json": 152,
|
| 100 |
},
|
| 101 |
},
|
| 102 |
+
qwen34B: {
|
| 103 |
+
modelId: "onnx-community/Qwen3-4B-ONNX",
|
| 104 |
+
title: "Qwen3-4B (q4f16)",
|
| 105 |
+
dtype: "q4f16",
|
| 106 |
+
device: "webgpu",
|
| 107 |
+
size: 2_842_047_473,
|
| 108 |
+
files: {
|
| 109 |
+
"tokenizer.json": 9_117_040,
|
| 110 |
+
"tokenizer_config.json": 9_761,
|
| 111 |
+
"config.json": 1_780,
|
| 112 |
+
"onnx/model_q4f16.onnx": 59_762_833,
|
| 113 |
+
"generation_config.json": 219,
|
| 114 |
+
"onnx/model_q4f16.onnx_data_1": 677_150_720,
|
| 115 |
+
"onnx/model_q4f16.onnx_data": 2_096_005_120,
|
| 116 |
+
},
|
| 117 |
+
},
|
| 118 |
+
smolLM33B: {
|
| 119 |
+
modelId: "HuggingFaceTB/SmolLM3-3B-ONNX",
|
| 120 |
+
title: "SmolLM3-3B (q4f16)",
|
| 121 |
+
dtype: "q4f16",
|
| 122 |
+
device: "webgpu",
|
| 123 |
+
size: 2_136_257_855,
|
| 124 |
+
files: {
|
| 125 |
+
"tokenizer.json": 11_574_059,
|
| 126 |
+
"tokenizer_config.json": 56_256,
|
| 127 |
+
"config.json": 2_056,
|
| 128 |
+
"onnx/model_q4f16.onnx": 301_534,
|
| 129 |
+
"generation_config.json": 182,
|
| 130 |
+
"onnx/model_q4f16.onnx_data": 2_124_320_768,
|
| 131 |
+
},
|
| 132 |
+
},
|
| 133 |
} as const;
|
| 134 |
|
| 135 |
export type ModelKey = keyof typeof MODELS;
|
src/utils/tools.ts
CHANGED
|
@@ -2,15 +2,85 @@ import type { WebMCPTool } from "@utils/webMcp.ts";
|
|
| 2 |
|
| 3 |
const getCurrentTimeTool: WebMCPTool = {
|
| 4 |
name: "get_current_time",
|
| 5 |
-
description: "Returns the current date and time
|
| 6 |
inputSchema: {
|
| 7 |
type: "object",
|
| 8 |
properties: {},
|
| 9 |
required: [],
|
| 10 |
},
|
| 11 |
execute: async () => {
|
| 12 |
-
|
|
|
|
| 13 |
},
|
| 14 |
};
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
const getCurrentTimeTool: WebMCPTool = {
|
| 4 |
name: "get_current_time",
|
| 5 |
+
description: "Returns the current date and time.",
|
| 6 |
inputSchema: {
|
| 7 |
type: "object",
|
| 8 |
properties: {},
|
| 9 |
required: [],
|
| 10 |
},
|
| 11 |
execute: async () => {
|
| 12 |
+
const date = new Date();
|
| 13 |
+
return `ISO 8601: ${date.toISOString()}\ndd.mm.yyyy: ${date.toLocaleDateString()}\nhh:mm:ss: ${date.toLocaleTimeString()}`;
|
| 14 |
},
|
| 15 |
};
|
| 16 |
|
| 17 |
+
const getJokeTool: WebMCPTool = {
|
| 18 |
+
name: "get_joke",
|
| 19 |
+
description:
|
| 20 |
+
"Fetches a joke from the jokes API. Can filter by category, type, or search text.",
|
| 21 |
+
inputSchema: {
|
| 22 |
+
type: "object",
|
| 23 |
+
properties: {
|
| 24 |
+
category: {
|
| 25 |
+
type: "string",
|
| 26 |
+
description:
|
| 27 |
+
"Joke category: Any, Programming, Misc, Pun, Spooky, Christmas",
|
| 28 |
+
default: "Any",
|
| 29 |
+
},
|
| 30 |
+
type: {
|
| 31 |
+
type: "string",
|
| 32 |
+
description:
|
| 33 |
+
"Joke format: single (one-liner) or twopart (setup/delivery)",
|
| 34 |
+
default: "Any",
|
| 35 |
+
},
|
| 36 |
+
contains: {
|
| 37 |
+
type: "string",
|
| 38 |
+
description: "Search for jokes containing this text",
|
| 39 |
+
},
|
| 40 |
+
},
|
| 41 |
+
required: [],
|
| 42 |
+
},
|
| 43 |
+
execute: async (args) => {
|
| 44 |
+
const params = new URLSearchParams({ amount: "1" });
|
| 45 |
+
|
| 46 |
+
if (args.category && args.category !== "Any") {
|
| 47 |
+
params.append("category", args.category);
|
| 48 |
+
}
|
| 49 |
+
if (args.type && args.type !== "Any") {
|
| 50 |
+
params.append("type", args.type);
|
| 51 |
+
}
|
| 52 |
+
if (args.contains) {
|
| 53 |
+
params.append("contains", args.contains);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
try {
|
| 57 |
+
const response = await fetch(
|
| 58 |
+
`https://jokes.nico.dev/joke?${params.toString()}`
|
| 59 |
+
);
|
| 60 |
+
|
| 61 |
+
if (!response.ok) {
|
| 62 |
+
return `Error fetching joke: ${response.statusText}`;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
const data = await response.json();
|
| 66 |
+
|
| 67 |
+
if (data.error) {
|
| 68 |
+
return `Error: ${data.error}`;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
if (data.joke) {
|
| 72 |
+
return data.joke;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if (data.jokes && data.jokes.length > 0) {
|
| 76 |
+
return data.jokes[0].text;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
return "No joke found";
|
| 80 |
+
} catch (error) {
|
| 81 |
+
return `Error fetching joke: ${error instanceof Error ? error.message : "Unknown error"}`;
|
| 82 |
+
}
|
| 83 |
+
},
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
export const TOOLS = [getCurrentTimeTool, getJokeTool];
|
src/utils/webMcp.ts
CHANGED
|
@@ -139,6 +139,63 @@ export const extractToolCalls = (
|
|
| 139 |
return { toolCalls, message };
|
| 140 |
};
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
export const executeToolCall = async (
|
| 143 |
toolCall: ToolCallPayload,
|
| 144 |
tools: Array<WebMCPTool>
|
|
|
|
| 139 |
return { toolCalls, message };
|
| 140 |
};
|
| 141 |
|
| 142 |
+
export const splitResponse = (
|
| 143 |
+
text: string
|
| 144 |
+
): Array<string | ToolCallPayload> => {
|
| 145 |
+
const result: Array<string | ToolCallPayload> = [];
|
| 146 |
+
let lastIndex = 0;
|
| 147 |
+
|
| 148 |
+
// Match only complete tool calls (with closing tag)
|
| 149 |
+
const regex = /<tool_call>([\s\S]*?)<\/tool_call>/g;
|
| 150 |
+
let match: RegExpExecArray | null;
|
| 151 |
+
|
| 152 |
+
while ((match = regex.exec(text)) !== null) {
|
| 153 |
+
// Add text before the tool call
|
| 154 |
+
const textBefore = text.slice(lastIndex, match.index);
|
| 155 |
+
if (textBefore) {
|
| 156 |
+
result.push(textBefore);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// Parse and add the tool call
|
| 160 |
+
try {
|
| 161 |
+
const parsed = JSON.parse(match[1].trim());
|
| 162 |
+
if (parsed && typeof parsed.name === "string") {
|
| 163 |
+
result.push({
|
| 164 |
+
name: parsed.name,
|
| 165 |
+
arguments: parsed.arguments ?? {},
|
| 166 |
+
id: JSON.stringify({
|
| 167 |
+
name: parsed.name,
|
| 168 |
+
arguments: parsed.arguments ?? {},
|
| 169 |
+
}),
|
| 170 |
+
});
|
| 171 |
+
}
|
| 172 |
+
} catch {
|
| 173 |
+
// ignore malformed tool call payloads
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
lastIndex = regex.lastIndex;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Check if there's an incomplete tool call
|
| 180 |
+
const incompleteToolCallIndex = text.indexOf("<tool_call>", lastIndex);
|
| 181 |
+
|
| 182 |
+
if (incompleteToolCallIndex !== -1) {
|
| 183 |
+
// There's an incomplete tool call, only add text up to it
|
| 184 |
+
const textBefore = text.slice(lastIndex, incompleteToolCallIndex);
|
| 185 |
+
if (textBefore) {
|
| 186 |
+
result.push(textBefore);
|
| 187 |
+
}
|
| 188 |
+
} else {
|
| 189 |
+
// No incomplete tool call, add remaining text
|
| 190 |
+
const remainingText = text.slice(lastIndex);
|
| 191 |
+
if (remainingText) {
|
| 192 |
+
result.push(remainingText);
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
return result;
|
| 197 |
+
};
|
| 198 |
+
|
| 199 |
export const executeToolCall = async (
|
| 200 |
toolCall: ToolCallPayload,
|
| 201 |
tools: Array<WebMCPTool>
|
tsconfig.app.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
| 18 |
|
| 19 |
/* Linting */
|
| 20 |
"strict": true,
|
|
|
|
| 21 |
"noUnusedLocals": true,
|
| 22 |
"noUnusedParameters": true,
|
| 23 |
"erasableSyntaxOnly": false,
|
|
|
|
| 18 |
|
| 19 |
/* Linting */
|
| 20 |
"strict": true,
|
| 21 |
+
"strictNullChecks": false,
|
| 22 |
"noUnusedLocals": true,
|
| 23 |
"noUnusedParameters": true,
|
| 24 |
"erasableSyntaxOnly": false,
|
vite.config.ts
CHANGED
|
@@ -8,10 +8,18 @@ import tsconfigPaths from "vite-tsconfig-paths";
|
|
| 8 |
|
| 9 |
dotenv.config();
|
| 10 |
|
| 11 |
-
const TITLE = "Transformers.js
|
| 12 |
|
| 13 |
// https://vite.dev/config/
|
| 14 |
export default defineConfig({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
server: {
|
| 16 |
...(fs.existsSync(process.env.SSL_KEY || "") &&
|
| 17 |
fs.existsSync(process.env.SSL_CRT || "")
|
|
@@ -24,12 +32,4 @@ export default defineConfig({
|
|
| 24 |
: {}),
|
| 25 |
port: process.env.PORT ? parseInt(process.env.PORT) : 8080,
|
| 26 |
},
|
| 27 |
-
plugins: [
|
| 28 |
-
react(),
|
| 29 |
-
tailwindcss(),
|
| 30 |
-
tsconfigPaths(),
|
| 31 |
-
htmlPlugin({
|
| 32 |
-
title: TITLE,
|
| 33 |
-
}),
|
| 34 |
-
],
|
| 35 |
});
|
|
|
|
| 8 |
|
| 9 |
dotenv.config();
|
| 10 |
|
| 11 |
+
const TITLE = "Transformers.js TextGeneration";
|
| 12 |
|
| 13 |
// https://vite.dev/config/
|
| 14 |
export default defineConfig({
|
| 15 |
+
plugins: [
|
| 16 |
+
react(),
|
| 17 |
+
tsconfigPaths(),
|
| 18 |
+
tailwindcss(),
|
| 19 |
+
htmlPlugin({
|
| 20 |
+
title: TITLE,
|
| 21 |
+
}),
|
| 22 |
+
],
|
| 23 |
server: {
|
| 24 |
...(fs.existsSync(process.env.SSL_KEY || "") &&
|
| 25 |
fs.existsSync(process.env.SSL_CRT || "")
|
|
|
|
| 32 |
: {}),
|
| 33 |
port: process.env.PORT ? parseInt(process.env.PORT) : 8080,
|
| 34 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
});
|