From ecbff6093b2de91c50b56f065b5c4fad153c4d80 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sun, 18 Feb 2024 13:23:47 +0530 Subject: [PATCH] manage model --- package.json | 1 + src/components/Common/Playground/Message.tsx | 2 +- src/components/Option/Layout.tsx | 4 +- src/components/Option/Models/index.tsx | 223 ++++++++++++++++++ .../Option/Playground/PlaygroundForm.tsx | 56 +++-- .../Option/Playground/PlaygroundMessage.tsx | 3 +- src/hooks/useMessageOption.tsx | 65 +++-- src/libs/byte-formater.ts | 23 ++ src/routes/option-model.tsx | 3 +- src/services/ollama.ts | 52 ++++ 10 files changed, 392 insertions(+), 40 deletions(-) create mode 100644 src/components/Option/Models/index.tsx create mode 100644 src/libs/byte-formater.ts diff --git a/package.json b/package.json index c29ecab..40a03f8 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@tanstack/react-query": "^5.17.19", "antd": "^5.13.3", "axios": "^1.6.7", + "dayjs": "^1.11.10", "html-to-text": "^9.0.5", "langchain": "^0.1.9", "lucide-react": "^0.323.0", diff --git a/src/components/Common/Playground/Message.tsx b/src/components/Common/Playground/Message.tsx index 17fb6bd..d3da9b5 100644 --- a/src/components/Common/Playground/Message.tsx +++ b/src/components/Common/Playground/Message.tsx @@ -26,7 +26,7 @@ export const PlaygroundMessage = (props: Props) => { return (
+ className={`group w-full text-gray-800 dark:text-gray-100`}>
diff --git a/src/components/Option/Layout.tsx b/src/components/Option/Layout.tsx index 5b74a09..bd67272 100644 --- a/src/components/Option/Layout.tsx +++ b/src/components/Option/Layout.tsx @@ -93,14 +93,14 @@ export default function OptionLayout({ + className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> + className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> diff --git a/src/components/Option/Models/index.tsx b/src/components/Option/Models/index.tsx new file mode 100644 index 0000000..1e7fafd --- /dev/null +++ b/src/components/Option/Models/index.tsx @@ -0,0 +1,223 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { Skeleton, Table, Tag, Tooltip, notification, Modal, Input } from "antd" +import { bytePerSecondFormatter } from "~libs/byte-formater" +import { deleteModel, getAllModels } from "~services/ollama" +import { Trash, RotateCcw, Download } from "lucide-react" +import dayjs from "dayjs" +import relativeTime from "dayjs/plugin/relativeTime" +import { useState } from "react" +import { useForm } from "@mantine/form" + +dayjs.extend(relativeTime) + +export const ModelsBody = () => { + const queryClient = useQueryClient() + const [open, setOpen] = useState(false) + + const form = useForm({ + initialValues: { + model: "" + } + }) + + const { data, status } = useQuery({ + queryKey: ["fetchAllModels"], + queryFn: getAllModels + }) + + const { mutate: deleteOllamaModel } = useMutation({ + mutationFn: deleteModel, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["fetchAllModels"] + }) + notification.success({ + message: "Model Deleted", + description: "Model has been deleted successfully" + }) + }, + onError: (error) => { + notification.error({ + message: "Error", + description: error?.message || "Something went wrong" + }) + } + }) + + const pullModel = async (modelName: string) => { + notification.info({ + message: "Pulling Model", + description: `Pulling ${modelName} model. For more details, check the extension icon.` + }) + + setOpen(false) + + form.reset() + + chrome.runtime.sendMessage({ + type: "pull_model", + modelName + }) + + return true + } + + const { mutate: pullOllamaModel } = useMutation({ + mutationFn: pullModel + }) + + return ( +
+
+ {/* Add new model button */} +
+
+
+ +
+
+
+ + {status === "pending" && } + + {status === "success" && ( + ( + + {`${text?.slice(0, 5)}...${text?.slice(-4)}`} + + ) + }, + { + title: "Modified", + dataIndex: "modified_at", + key: "modified_at", + render: (text: string) => dayjs(text).fromNow(true) + }, + { + title: "Size", + dataIndex: "size", + key: "size", + render: (text: number) => bytePerSecondFormatter(text) + }, + { + title: "Action", + render: (_, record) => ( +
+ + + + + + +
+ ) + } + ]} + expandable={{ + expandedRowRender: (record) => ( +
+ ), + defaultExpandAllRows: false + }} + bordered + dataSource={data} + rowKey={(record) => `${record.model}-${record.digest}`} + /> + )} + + + setOpen(false)}> +
pullOllamaModel(values.model))}> + + + + +
+ + ) +} diff --git a/src/components/Option/Playground/PlaygroundForm.tsx b/src/components/Option/Playground/PlaygroundForm.tsx index 9366e34..291d90c 100644 --- a/src/components/Option/Playground/PlaygroundForm.tsx +++ b/src/components/Option/Playground/PlaygroundForm.tsx @@ -7,7 +7,7 @@ import XMarkIcon from "@heroicons/react/24/outline/XMarkIcon" import { toBase64 } from "~libs/to-base64" import { useMessageOption } from "~hooks/useMessageOption" import { Tooltip } from "antd" -import { MicIcon, MicOffIcon } from "lucide-react" +import { MicIcon, StopCircleIcon } from "lucide-react" import { Image } from "antd" import { useSpeechRecognition } from "~hooks/useSpeechRecognition" @@ -60,8 +60,13 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { useDynamicTextareaSize(textareaRef, form.values.message, 300) - const { onSubmit, selectedModel, chatMode, speechToTextLanguage } = - useMessageOption() + const { + onSubmit, + selectedModel, + chatMode, + speechToTextLanguage, + stopStreamingRequest + } = useMessageOption() const { isListening, start, stop, transcript } = useSpeechRecognition() @@ -208,23 +213,34 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { - + {!isSending ? ( + + ) : ( + + + + )} diff --git a/src/components/Option/Playground/PlaygroundMessage.tsx b/src/components/Option/Playground/PlaygroundMessage.tsx index 1882558..6d5ca19 100644 --- a/src/components/Option/Playground/PlaygroundMessage.tsx +++ b/src/components/Option/Playground/PlaygroundMessage.tsx @@ -25,8 +25,7 @@ export const PlaygroundMessage = (props: Props) => { }, [isBtnPressed]) return ( -
+
diff --git a/src/hooks/useMessageOption.tsx b/src/hooks/useMessageOption.tsx index f22d27b..745b0fe 100644 --- a/src/hooks/useMessageOption.tsx +++ b/src/hooks/useMessageOption.tsx @@ -12,6 +12,7 @@ import { import { useStoreMessageOption } from "~store/option" import { saveHistory, saveMessage } from "~libs/db" import { useNavigate } from "react-router-dom" +import { notification } from "antd" export type BotResponse = { bot: { @@ -253,22 +254,58 @@ export const useMessageOption = () => { setIsProcessing(false) } catch (e) { + console.log(e) + + if (e?.name === "AbortError") { + newMessage[appendingIndex].message = newMessage[ + appendingIndex + ].message.slice(0, -1) + + setHistory([ + ...history, + { + role: "user", + content: message, + image + }, + { + role: "assistant", + content: newMessage[appendingIndex].message + } + ]) + + if (historyId) { + await saveMessage(historyId, selectedModel, "user", message, [image]) + await saveMessage( + historyId, + selectedModel, + "assistant", + newMessage[appendingIndex].message, + [] + ) + } else { + const newHistoryId = await saveHistory(message) + await saveMessage(newHistoryId.id, selectedModel, "user", message, [ + image + ]) + await saveMessage( + newHistoryId.id, + selectedModel, + "assistant", + newMessage[appendingIndex].message, + [] + ) + setHistoryId(newHistoryId.id) + } + } else { + notification.error({ + message: "Error", + description: e?.message || "Something went wrong" + }) + } + setIsProcessing(false) setStreaming(false) - - setMessages([ - ...messages, - { - isBot: true, - name: selectedModel, - message: `Something went wrong. Check out the following logs: - \`\`\` - ${e?.message} - \`\`\` - `, - sources: [] - } - ]) } } diff --git a/src/libs/byte-formater.ts b/src/libs/byte-formater.ts new file mode 100644 index 0000000..3499487 --- /dev/null +++ b/src/libs/byte-formater.ts @@ -0,0 +1,23 @@ +const UNITS = [ + "byte", + "kilobyte", + "megabyte", + "gigabyte", + "terabyte", + "petabyte" +] + +const getValueAndUnit = (n: number) => { + const i = n == 0 ? 0 : Math.floor(Math.log(n) / Math.log(1024)) + const value = n / Math.pow(1024, i) + return { value, unit: UNITS[i] } +} + +export const bytePerSecondFormatter = (n: number) => { + const { unit, value } = getValueAndUnit(n) + return new Intl.NumberFormat("en", { + notation: "compact", + style: "unit", + unit + }).format(value) +} diff --git a/src/routes/option-model.tsx b/src/routes/option-model.tsx index 1cbbf87..3f5a020 100644 --- a/src/routes/option-model.tsx +++ b/src/routes/option-model.tsx @@ -1,9 +1,10 @@ import OptionLayout from "~components/Option/Layout" +import { ModelsBody } from "~components/Option/Models" export const OptionModal = () => { return ( - yo + ) } diff --git a/src/services/ollama.ts b/src/services/ollama.ts index 5ca0fcd..fb256ed 100644 --- a/src/services/ollama.ts +++ b/src/services/ollama.ts @@ -53,6 +53,47 @@ export const isOllamaRunning = async () => { } } +export const getAllModels = async () => { + const baseUrl = await getOllamaURL() + const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`) + if (!response.ok) { + throw new Error(response.statusText) + } + const json = await response.json() + + return json.models as { + name: string + model: string + modified_at: string + size: number + digest: string + details: { + parent_model: string + format: string + family: string + families: string[] + parameter_size: string + quantization_level: string + } + }[] +} + +export const deleteModel= async (model: string) => { + const baseUrl = await getOllamaURL() + const response = await fetch(`${cleanUrl(baseUrl)}/api/delete`, { + method: "DELETE", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ name: model }) + }) + + if (!response.ok) { + throw new Error(response.statusText) + } + return response.json() +} + export const fetchModels = async () => { try { const baseUrl = await getOllamaURL() @@ -65,6 +106,17 @@ export const fetchModels = async () => { return json.models as { name: string model: string + modified_at: string + size: number + digest: string + details: { + parent_model: string + format: string + family: string + families: string[] + parameter_size: string + quantization_level: string + } }[] } catch (e) { console.error(e)