-
-
- {role !== "system" &&
- (role == "user" ? (
-
- ) : (
- role[0].toUpperCase()
- ))}
-
-
-
- {isEmpty ? (
-
+ {
+ navigator.clipboard.writeText(content)
+ }}
>
- ...
-
- ) : (
-
- {cloneElement(children, {
- className: cn(
- "rounded-md p-4 !bg-gray-900",
- children.props.className,
- ),
- })}
-
- )
- },
+
+ Copy to clipboard
+
+ {
+ state.setEditing(true)
}}
>
- {content}
-
- )}
+
+
Edit
+
+
{
+ message.chat.remove(message)
+ }}
+ >
+
+ Remove
+
+
+ }
+ >
+
+
+
+ {state.isEditing ? (
+
message.update({ content: e.target.value })}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ state.setEditing(false)
+ message.update({ content: e.target.value })
+ }
+ }}
+ />
+ ) : isEmpty ? (
+
+ ) : (
+
+ )}
+
-
+
)
})
diff --git a/src/app/ChatSettings.tsx b/src/app/ChatSettings.tsx
deleted file mode 100644
index 0c498c0..0000000
--- a/src/app/ChatSettings.tsx
+++ /dev/null
@@ -1,325 +0,0 @@
-import { Textarea } from "../components/ui/textarea"
-import { ChatSettingsSlider } from "./ChatSettingsSlider"
-import { observer } from "mobx-react"
-import { PresetSelector } from "./PresetSelector"
-import { ChatSettingsModelSelector } from "./ChatSettingsModelSelector"
-import { useStore } from "@/store"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { TbPrompt } from "react-icons/tb"
-import { MdOutlineHistory, MdTune } from "react-icons/md"
-import { ImAttachment } from "react-icons/im"
-import { Input } from "@/components/ui/input"
-import { processFiles } from "@/lib/extraction"
-import { IoMdAdd, IoMdPerson, IoMdSettings, IoMdTrash } from "react-icons/io"
-import { PiTextbox } from "react-icons/pi"
-import { Button } from "@/components/ui/button"
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from "@radix-ui/react-accordion"
-import AutoTextarea from "./AutoTextarea"
-
-const ChatSettingsAccordionItem = observer(
- ({ id, icon: Icon, title, children }) => (
-
-
-
- {title}
-
-
- {children}
-
-
- ),
-)
-
-const SystemMessageAccordionItem = observer(() => {
- const store = useStore()
- const { activeChat: chat } = store
-
- return (
-
-
- {/*
*/}
-
chat.setSystemMessage(e.target.value)}
- />
-
-
- )
-})
-
-const UserMessageAccordionItem = observer(() => {
- const store = useStore()
- const { activeChat: chat } = store
- return (
-
-
- {/*
*/}
-
-
chat.setUserMessage(e.target.value)}
- />
-
-
- )
-})
-
-// const AssistantMessageAccordion = observer(() => {
-// const store = useStore()
-// const { activeChat: chat } = store
-
-// return (
-//
-// {/*
*/}
-//
-//
-// )
-// })
-
-const PromptTemplateAccordion = observer(() => {
- const store = useStore()
- const { activeChat: chat } = store
-
- return (
-
-
-
-
chat.setChatTemplate(e.target.value)}
- />
-
-
-
- {chat.template}
-
-
- )
-})
-
-const HistoryMessage = observer(({ message, ...rest }) => (
-
- {
- message.update({ role: e.target.value })
- }}
- {...rest}
- />
- {
- message.update({ content: e.target.value })
- }}
- {...rest}
- />
-
-
-))
-
-const ChatHistoryAccordionItem = observer(() => {
- const store = useStore()
- const { activeChat: chat } = store
-
- return (
-
-
-
-
-
-
-
-
- {chat.history.map((message, i) => (
-
- ))}
-
-
- )
-})
-
-const AttachmentsAccordionItem = observer(() => {
- const store = useStore()
- const { activeChat: chat } = store
-
- const handleAttachment = async (e) => {
- const docs = await processFiles(e.target.files)
- chat.setAttachments(docs)
- }
-
- return (
-
-
-
- )
-})
-
-const InferenceParamsAccordionItem = observer(() => {
- const store = useStore()
- const { activeChat: chat } = store
-
- return (
-
-
-
-
chat.updateOptions({ temperature: value })}
- />
- chat.updateOptions({ top_k: value })}
- />
- chat.updateOptions({ top_p: value })}
- />
- chat.updateOptions({ repeat_last_n: value })}
- />
- chat.updateOptions({ repeat_penalty: value })}
- />
- chat.updateOptions({ num_predict: value })}
- />
- chat.updateOptions({ num_ctx: value })}
- />
-
-
- )
-})
-
-export const ChatSettings = observer(() => (
-
-
-
-
-
-
-
-
- {/*
- */}
-
-
-
-
-
-))
diff --git a/src/app/ChatSettingsModelSelector.tsx b/src/app/ChatSettingsModelSelector.tsx
deleted file mode 100644
index 5770197..0000000
--- a/src/app/ChatSettingsModelSelector.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useStore } from "@/store"
-import { observer } from "mobx-react"
-import { IoMdRefresh } from "react-icons/io"
-import { Button } from "@/components/ui/button"
-
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "../components/ui/select"
-import { humanFileSize } from "@/lib/utils"
-
-export const ChatSettingsModelSelect = observer(() => {
- const { models } = useStore()
- const { activeChat: chat } = useStore()
-
- return (
-
- )
-})
-
-export const ChatSettingsModelSelector = observer(() => {
- const store = useStore()
- return (
-
-
-
-
- )
-})
diff --git a/src/app/ChatSettingsSlider.tsx b/src/app/ChatSettingsSlider.tsx
deleted file mode 100644
index 65aee30..0000000
--- a/src/app/ChatSettingsSlider.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Slider } from "../components/ui/slider"
-import { Label } from "../components/ui/label"
-import { observer } from "mobx-react"
-
-export const ChatSettingsSlider = observer(
- ({ label, id, value, onChange, ...props }) => (
-
-
-
-
- {value}
-
-
-
onChange(v[0])}
- className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
- aria-label={label}
- {...props}
- />
-
- ),
-)
diff --git a/src/app/ChatView.tsx b/src/app/ChatView.tsx
new file mode 100644
index 0000000..96320d6
--- /dev/null
+++ b/src/app/ChatView.tsx
@@ -0,0 +1,17 @@
+import { observer } from "mobx-react"
+import { ChatSettings } from "../components/ChatSettings"
+import { ChatConversation } from "./ChatConversation"
+import { useStore } from "@/store"
+
+export const ChatView = observer(() => {
+ const {
+ state: { resource: chat },
+ } = useStore()
+
+ return (
+
+
+
+
+ )
+})
diff --git a/src/app/ChatWithSettings.tsx b/src/app/ChatWithSettings.tsx
deleted file mode 100644
index c2f128c..0000000
--- a/src/app/ChatWithSettings.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { observer } from "mobx-react"
-import { ChatSettings } from "./ChatSettings"
-import { ChatConversation } from "./ChatConversation"
-
-export const ChatWithSettings = observer(() => {
- return (
-
-
-
-
- )
-})
diff --git a/src/app/CopyOverlay.tsx b/src/app/CopyOverlay.tsx
deleted file mode 100644
index 70f30fe..0000000
--- a/src/app/CopyOverlay.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-showCopy?: boolean
-
diff --git a/src/app/PlaygroundView.tsx b/src/app/PlaygroundView.tsx
new file mode 100644
index 0000000..cb7725e
--- /dev/null
+++ b/src/app/PlaygroundView.tsx
@@ -0,0 +1,63 @@
+import { AgentGenerationParams } from "@/components/AgentGenerationParams"
+import { useStore } from "@/store"
+import { observer } from "mobx-react"
+import { AgentAdapterPicker } from "./AgentView"
+import { useDebouncedCallback } from "use-debounce"
+export const PlaygroundView = observer(() => {
+ const {
+ playground: { agent, generate, abortController, output, update },
+ } = useStore()
+
+ const debounced = useDebouncedCallback(() => {
+ generate()
+ }, 200)
+
+ return (
+
+ )
+})
diff --git a/src/app/PresetSelector.tsx b/src/app/PresetSelector.tsx
deleted file mode 100644
index c5f9358..0000000
--- a/src/app/PresetSelector.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { useEffect, useState } from "react"
-// import Papa from "papaparse"
-// useEffect(() => {
-// const fetchPersonas = async () => {
-// const response = await fetch(
-// "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv",
-// )
-// const text = await response.text()
-// const results = Papa.parse(text, { header: true }).data
-// return results.sort((a, b) => a.act.localeCompare(b.act))
-// }
-
-// fetchPersonas().then(setPersonas)
-// }, [])
-
-export const PresetSelector = ({ presets, onSelect }) => {
- const sorted = presets.slice().sort((a, b) => a.key.localeCompare(b.name))
-
- return (
-
- )
-}
diff --git a/src/app/Router.tsx b/src/app/Router.tsx
new file mode 100644
index 0000000..762ba02
--- /dev/null
+++ b/src/app/Router.tsx
@@ -0,0 +1,30 @@
+import { useStore } from "@/store"
+import { AgentView } from "./AgentView"
+import { ChatView } from "./ChatView"
+import { observer } from "mobx-react"
+import { PlaygroundView } from "./PlaygroundView"
+import { ScrollArea } from "@radix-ui/react-scroll-area"
+
+export const Router = observer(() => {
+ const {
+ state: { route, resource },
+ } = useStore()
+
+ if (resource) {
+ if (route === "chat") {
+ return
+ } else if (route === "agent") {
+ return (
+
+ )
+ }
+ }
+
+ if (route === "playground") {
+ return
+ }
+
+ return null
+})
diff --git a/src/app/Sidebar.tsx b/src/app/Sidebar.tsx
index 0ccd4bb..d719894 100644
--- a/src/app/Sidebar.tsx
+++ b/src/app/Sidebar.tsx
@@ -1,77 +1,24 @@
-import { cn } from "@/lib/utils"
import { useStore } from "@/store"
import { observer } from "mobx-react"
import { ChatList } from "./SidebarChatList"
-import { Input } from "@/components/ui/input"
-import { IoIosFlash } from "react-icons/io"
-import { Button } from "@/components/ui/button"
-import { Label } from "@/components/ui/label"
-import { MdEdit } from "react-icons/md"
-import { FaNetworkWired } from "react-icons/fa"
-
-const ConnectButton = observer(() => {
- const store = useStore()
-
- return (
-
- )
-})
-
-const DisconnectButton = observer(() => {
- const store = useStore()
+import { AgentList } from "./SidebarAgentList"
+import { SidebarItem } from "../components/SidebarItem"
+import { SidebarSection } from "../components/SidebarSection"
+import { ToggleDarkButton } from "@/components/ToggleDarkButton"
+import { VscTools } from "react-icons/vsc"
+export const Sidebar = observer(() => {
+ const { agents, chats } = useStore()
return (
-
+
+
+
+
}>
+
+ Playground
+
+
+
+
)
})
-
-export const Sidebar = observer(
- ({ className, chats }: { className?: string; chats: any[] }) => {
- const store = useStore()
-
- const handleSubmit = (e: React.FormEvent
) => {
- e.preventDefault()
- store?.refreshModels()
- }
-
- return (
-
- )
- },
-)
diff --git a/src/app/SidebarAgentList.tsx b/src/app/SidebarAgentList.tsx
new file mode 100644
index 0000000..151fb44
--- /dev/null
+++ b/src/app/SidebarAgentList.tsx
@@ -0,0 +1,52 @@
+import { Button } from "@/components/ui/button"
+import { useStore } from "@/store"
+import { IoMdAdd } from "react-icons/io"
+import { observer } from "mobx-react"
+import { Instance } from "mobx-state-tree"
+import { Agent } from "@/store/chat"
+import { IoTrashOutline } from "react-icons/io5"
+import { PiRobot } from "react-icons/pi"
+import { SidebarItem } from "@/components/SidebarItem"
+
+export const AgentList = observer(
+ ({ agents }: { agents: Instance[] }) => {
+ const store = useStore()
+
+ const sorted = agents.slice().sort((a, b) => a.name.localeCompare(b.name))
+
+ return (
+
+
+
+ Agents
+
+
+
+ {sorted.map((agent) => (
+ {
+ e.stopPropagation()
+ store.removeAgent(agent)
+ }}
+ >
+
+
+ }
+ >
+ {agent.name ? agent.name : (No Name)}
+
+ ))}
+
+
+ )
+ },
+)
diff --git a/src/app/SidebarChatList.tsx b/src/app/SidebarChatList.tsx
index 533aeec..3aeab73 100644
--- a/src/app/SidebarChatList.tsx
+++ b/src/app/SidebarChatList.tsx
@@ -1,58 +1,59 @@
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { useStore } from "@/store"
-import { BsChatRightTextFill } from "react-icons/bs"
+import { BsChatRightText } from "react-icons/bs"
import { IoMdAdd } from "react-icons/io"
import { observer } from "mobx-react"
-import { Instance } from "mobx-state-tree"
-import { Chat } from "@/store/chat"
import { IoTrashOutline } from "react-icons/io5"
+import { SidebarItem } from "@/components/SidebarItem"
-export const ChatList = observer(
- ({ chats }: { chats: Instance[] }) => {
- const store = useStore()
- return (
-
-
-
- Chats
-
-
+export const ChatList = observer(() => {
+ const { newChat, removeChat, chats } = useStore()
- {chats.map((chat) => (
-
+ )
+})
diff --git a/src/components/ActionOverlay.tsx b/src/components/ActionOverlay.tsx
new file mode 100644
index 0000000..2ab16e9
--- /dev/null
+++ b/src/components/ActionOverlay.tsx
@@ -0,0 +1,26 @@
+import { cn } from "@/lib/utils"
+import { observer } from "mobx-react"
+
+export const ActionOverlay = observer(
+ ({
+ children,
+ actions,
+ className,
+ }: {
+ children: React.ReactNode
+ actions: React.ReactNode
+ className?: string
+ }) => (
+
+ {children}
+
+ {actions}
+
+
+ ),
+)
diff --git a/src/components/AgentConversation.tsx b/src/components/AgentConversation.tsx
new file mode 100644
index 0000000..ef23d9e
--- /dev/null
+++ b/src/components/AgentConversation.tsx
@@ -0,0 +1,17 @@
+import { observer } from "mobx-react"
+import { Agent } from "@/store/Agent"
+import { Instance } from "mobx-state-tree"
+import { AgentMessage } from "./AgentMessage"
+import { Message } from "@/store/Message"
+
+export const AgentHistory: React.FC<{
+ agent: Instance
+}> = observer(({ agent }) => (
+
+
+ {agent.messages.map((message: Instance
, i: number) => (
+
+ ))}
+
+
+))
diff --git a/src/components/AgentGenerationParams.tsx b/src/components/AgentGenerationParams.tsx
new file mode 100644
index 0000000..667a1c2
--- /dev/null
+++ b/src/components/AgentGenerationParams.tsx
@@ -0,0 +1,169 @@
+import { SliderCheckbox } from "./SliderCheckbox"
+import { Instance } from "mobx-state-tree"
+import { Agent } from "@/store/Agent"
+import { observer, useLocalStore } from "mobx-react"
+import { cn } from "@/lib/utils"
+import { Input } from "./ui/input"
+import { Label } from "./ui/label"
+import { Checkbox } from "./ui/checkbox"
+import { IoMdTrash } from "react-icons/io"
+import { Button } from "./ui/button"
+
+const GENERATION_PARAMS = [
+ {
+ id: "num_predict",
+ label: "Max. Tokens",
+ max: 1024 * 8,
+ step: 64,
+ },
+ {
+ id: "temperature",
+ label: "Temperature",
+ max: 1,
+ step: 0.1,
+ },
+ {
+ id: "top_k",
+ label: "Top K",
+ max: 1,
+ step: 0.1,
+ },
+ {
+ id: "top_p",
+ label: "Top P",
+ max: 1,
+ step: 0.1,
+ },
+ {
+ id: "num_ctx",
+ label: "Context Window",
+ max: 1024 * 8,
+ step: 64,
+ },
+ {
+ id: "repeat_last_n",
+ label: "Repeat Window",
+ max: 1024 * 8,
+ step: 64,
+ },
+ {
+ id: "repeat_penalty",
+ label: "Reptition Penalty",
+ max: 2,
+ step: 0.1,
+ },
+]
+
+const AgentStopPatternEditor: React.FC<{
+ agent: Instance
+}> = observer(({ agent }) => {
+ const state = useLocalStore(() => ({
+ draft: "",
+ update(value: string) {
+ this.draft = value
+ },
+ }))
+
+ return (
+
+
+
+
+ )
+})
+
+export const AgentGenerationParams: React.FC<{
+ agent: Instance
+}> = observer(({ className, agent }) => {
+ const changeProps = (key: string) => ({
+ value: agent.parameters[key],
+ onChange: (value: string) => {
+ agent.update({ parameters: { ...agent.parameters, [key]: value } })
+ },
+ checked: agent.checkedOptions.includes(key),
+ onCheckedChange: (value: boolean) => {
+ if (value) {
+ agent.update({
+ checkedOptions: Array.from(new Set([...agent.checkedOptions, key])),
+ })
+ } else {
+ agent.update({
+ checkedOptions: agent.checkedOptions.filter((x) => x !== key),
+ })
+ }
+ },
+ })
+
+ const parameters = GENERATION_PARAMS.filter((param) =>
+ agent.adapter.parameters.includes(param.id),
+ )
+
+ return (
+
+ {parameters.map((param) => (
+
+ ))}
+
+
+
+ )
+})
diff --git a/src/components/AgentGenerationParamsAccordionItem.tsx b/src/components/AgentGenerationParamsAccordionItem.tsx
new file mode 100644
index 0000000..57787fc
--- /dev/null
+++ b/src/components/AgentGenerationParamsAccordionItem.tsx
@@ -0,0 +1,16 @@
+import { observer } from "mobx-react"
+import { MdTune } from "react-icons/md"
+import { Instance } from "mobx-state-tree"
+import { AgentGenerationParams } from "./AgentGenerationParams"
+import { AppAccordionItem } from "../app/AppAccordionItem"
+import { Agent } from "@/store/Agent"
+
+export const AgentInferenceParamsAccordionItem = observer(
+ ({ agent }: { agent: Instance }) => {
+ return (
+
+
+
+ )
+ },
+)
diff --git a/src/components/AgentMessage.tsx b/src/components/AgentMessage.tsx
new file mode 100644
index 0000000..28b4106
--- /dev/null
+++ b/src/components/AgentMessage.tsx
@@ -0,0 +1,41 @@
+import { observer } from "mobx-react"
+import { Button } from "@/components/ui/button"
+import { IoMdTrash } from "react-icons/io"
+import { Select } from "./Select"
+import { AutoTextarea } from "./AutoTextarea"
+
+export const AgentMessage = observer(({ message, ...rest }) => (
+
+
+
+
{
+ message.agent.removeMessage(message)
+ }}
+ >
+
+
+
+))
diff --git a/src/components/AgentPicker.tsx b/src/components/AgentPicker.tsx
new file mode 100644
index 0000000..2fad7e2
--- /dev/null
+++ b/src/components/AgentPicker.tsx
@@ -0,0 +1,28 @@
+import { observer } from "mobx-react"
+import { Instance } from "mobx-state-tree"
+import { useStore } from "@/store"
+import { Chat } from "@/store/Chat"
+
+import { Picker } from "./Picker"
+
+export const AgentPicker = observer(
+ ({ chat, ...props }: { chat: Instance }) => {
+ const { agents } = useStore()
+
+ const options = agents.map((agent) => ({
+ key: agent.name,
+ value: agent.name,
+ }))
+
+ return (
+ {
+ chat.update({ agent: agents.find((a) => a.name === agent) })
+ }}
+ />
+ )
+ },
+)
diff --git a/src/components/AutoTextarea.tsx b/src/components/AutoTextarea.tsx
new file mode 100644
index 0000000..3fb54f6
--- /dev/null
+++ b/src/components/AutoTextarea.tsx
@@ -0,0 +1,41 @@
+import { cn } from "@/lib/utils"
+import { Textarea } from "@/components/ui/textarea"
+import { useRef, useEffect, PropsWithoutRef } from "react"
+import { observer } from "mobx-react"
+
+export const AutoTextarea = observer(
+ ({
+ className,
+ value,
+ onChange,
+ maxRows = 8,
+ ...rest
+ }: PropsWithoutRef & {
+ maxRows?: number
+ onChange: (e: React.ChangeEvent) => void
+ }) => {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ if (!ref.current) return
+ ref.current.style.height = "0px"
+ const { scrollHeight } = ref.current
+ ref.current.style.height = `min(${
+ getComputedStyle(ref.current).lineHeight
+ } * ${maxRows + 1}, ${scrollHeight}px)`
+ }, [value])
+
+ return (
+
+ )
+ },
+)
diff --git a/src/components/ChatSettings.tsx b/src/components/ChatSettings.tsx
new file mode 100644
index 0000000..870cf96
--- /dev/null
+++ b/src/components/ChatSettings.tsx
@@ -0,0 +1,198 @@
+import { SliderCheckbox } from "./SliderCheckbox"
+import { observer } from "mobx-react"
+import { Select } from "./Select"
+import { ModelPicker } from "./ModelPicker"
+import { useStore } from "@/store"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { TbPrompt } from "react-icons/tb"
+import { MdOutlineHistory, MdTune } from "react-icons/md"
+import { ImAttachment } from "react-icons/im"
+import { Input } from "@/components/ui/input"
+import { IoMdAdd, IoMdPerson, IoMdSettings, IoMdTrash } from "react-icons/io"
+import { PiTextbox } from "react-icons/pi"
+import { Button } from "@/components/ui/button"
+import { AutoTextarea } from "./AutoTextarea"
+import { AgentPicker } from "./AgentPicker"
+import { AttachmentsAccordionItem } from "@/app/AttachmentsAccordionItem"
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@radix-ui/react-accordion"
+
+const ChatSettingsAccordionItem = observer(
+ ({ id, icon: Icon, title, children }) => (
+
+
+
+ {title}
+
+
+ {children}
+
+
+ ),
+)
+
+const SystemMessageAccordionItem = observer(() => {
+ const store = useStore()
+ const { activeChat: chat } = store
+
+ return (
+
+
+ {/*
*/}
+
chat.setSystemMessage(e.target.value)}
+ />
+
+
+ )
+})
+
+const UserMessageAccordionItem = observer(() => {
+ const store = useStore()
+ const { activeChat: chat } = store
+ return (
+
+
+ {/*
*/}
+
+
chat.setUserMessage(e.target.value)}
+ />
+
+
+ )
+})
+
+const PromptTemplateAccordion = observer(() => {
+ const store = useStore()
+ const { activeChat: chat } = store
+
+ return (
+
+
+
+
chat.setChatTemplate(e.target.value)}
+ />
+
+
+
+ {chat.agent.template}
+
+
+ )
+})
+
+const HistoryMessage = observer(({ message, ...rest }) => (
+
+ {
+ message.update({ role: e.target.value })
+ }}
+ {...rest}
+ />
+ {
+ message.update({ content: e.target.value })
+ }}
+ {...rest}
+ />
+
+
+
+
+))
+
+const ChatHistoryAccordionItem = observer(() => {
+ const store = useStore()
+ const {
+ state: { resource: chat },
+ } = store
+
+ return (
+
+
+
+ chat.addMessage({ role: "system", content: "{{system}}" })
+ }
+ >
+
+
+
+
chat.addMessage({ role: "user", content: "{{user}}" })}
+ >
+
+
+
+
+ chat.addMessage({ role: "user", content: "{{attachments}}" })
+ }
+ >
+
+
+
+
chat.addMessage({ role: "", content: "" })}
+ >
+
+
+
+
+
+ {chat.history.map((message, i) => (
+
+ ))}
+
+
+ )
+})
+
+export const ChatSettings = observer(({ chat }) => (
+
+
+
+))
diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx
new file mode 100644
index 0000000..bf23edf
--- /dev/null
+++ b/src/components/Editor.tsx
@@ -0,0 +1,34 @@
+import MonacoEditor from "react-monaco-editor"
+import * as monacoEditor from "monaco-editor"
+
+export const Editor = ({ agent }) => {
+ return (
+ {
+ agent.update({ promptTemplate: value })
+ debounced()
+ }}
+ editorDidMount={(
+ editor: monacoEditor.editor.IStandaloneCodeEditor,
+ monaco: typeof monacoEditor,
+ ) => {
+ monaco.languages.registerCompletionItemProvider("plaintext", {
+ async provideInlineCompletionItems() {},
+ })
+ }}
+ />
+ )
+}
diff --git a/src/components/ModelPicker.tsx b/src/components/ModelPicker.tsx
new file mode 100644
index 0000000..2363c05
--- /dev/null
+++ b/src/components/ModelPicker.tsx
@@ -0,0 +1,29 @@
+import { observer } from "mobx-react"
+import { IoMdRefresh } from "react-icons/io"
+import { Button } from "@/components/ui/button"
+import { ModelSelect } from "./ModelSelect"
+import { Instance } from "mobx-state-tree"
+import { Agent } from "@/store/Agent"
+import { Input } from "./ui/input"
+
+export const ModelPicker = observer(
+ ({ agent }: { agent: Instance }) => {
+ return (
+
+ {agent.adapter.isMultiModal ? (
+ <>
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+ )
+ },
+)
diff --git a/src/components/ModelSelect.tsx b/src/components/ModelSelect.tsx
new file mode 100644
index 0000000..638ca0c
--- /dev/null
+++ b/src/components/ModelSelect.tsx
@@ -0,0 +1,38 @@
+import { observer } from "mobx-react"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "./ui/select"
+import { humanFileSize } from "@/lib/utils"
+
+export const ModelSelect = observer(({ agent, ...props }) => {
+ const { models } = agent.adapter
+
+ return (
+
+ )
+})
diff --git a/src/components/Picker.tsx b/src/components/Picker.tsx
new file mode 100644
index 0000000..239f3cb
--- /dev/null
+++ b/src/components/Picker.tsx
@@ -0,0 +1,44 @@
+import { observer } from "mobx-react"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "./ui/select"
+
+export const Picker = observer(
+ ({
+ options,
+ value,
+ onChange,
+ ...props
+ }: {
+ onChange: (option: string) => void
+ options: { key: string; value: string }[]
+ }) => {
+ options = options.sort((a, b) => a.value.localeCompare(b.value))
+
+ return (
+
+ )
+ },
+)
diff --git a/src/components/PresetPicker.tsx b/src/components/PresetPicker.tsx
new file mode 100644
index 0000000..61ed1fd
--- /dev/null
+++ b/src/components/PresetPicker.tsx
@@ -0,0 +1,55 @@
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { observer } from "mobx-react"
+import { TbSelector } from "react-icons/tb"
+import { HiOutlineDocumentDownload } from "react-icons/hi"
+
+export const NewComponent: React.FC<{
+ onSelect: (preset: any) => void
+ presets: any[]
+}> = ({ onSelect, presets }) => {
+ if (presets.length === 0) {
+ return (
+
+ No presets
+
+ )
+ }
+ return (
+ <>
+ {presets.map((preset) => (
+ onSelect(preset.value)}>
+ {preset.key}
+
+ ))}
+ >
+ )
+}
+
+export const PresetPicker = observer(
+ ({
+ presets = [],
+ onSelect,
+ }: {
+ presets: any[]
+ onSelect: (preset: any) => void
+ }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+ },
+)
diff --git a/src/components/Select.tsx b/src/components/Select.tsx
new file mode 100644
index 0000000..54d7978
--- /dev/null
+++ b/src/components/Select.tsx
@@ -0,0 +1,47 @@
+import {
+ Select as Base,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { observer } from "mobx-react"
+
+export type Option = {
+ key: string
+ value: string
+}
+
+export const Select = observer(
+ ({
+ className,
+ options = [],
+ onChange,
+ placeholder,
+ ...props
+ }: {
+ className?: string
+ value: string
+ options: Option[]
+ onChange: (value: string) => void
+ placeholder?: string
+ }) => {
+ const sorted = options?.slice().sort((a, b) => a.key.localeCompare(b.key))
+
+ return (
+
+
+
+
+
+
+ {sorted.map(({ key, value }) => (
+
+ {key}
+
+ ))}
+
+
+ )
+ },
+)
diff --git a/src/components/Sheet.tsx b/src/components/Sheet.tsx
new file mode 100644
index 0000000..50ab4d8
--- /dev/null
+++ b/src/components/Sheet.tsx
@@ -0,0 +1,5 @@
+import { observer } from "mobx-react"
+
+export const Sheet = observer(() => {
+ return Sheet
+})
diff --git a/src/components/SidebarItem.tsx b/src/components/SidebarItem.tsx
new file mode 100644
index 0000000..e18c14a
--- /dev/null
+++ b/src/components/SidebarItem.tsx
@@ -0,0 +1,38 @@
+import { cn } from "@/lib/utils"
+import { useStore } from "@/store"
+import { observer } from "mobx-react"
+
+export const SidebarItem = observer(
+ ({
+ children,
+ icon: Icon,
+ route,
+ resource,
+ actions,
+ ...props
+ }: {
+ children: React.ReactNode
+ }) => {
+ const { state } = useStore()
+
+ return (
+ state.navigate(resource ?? route)}
+ className={cn("flex items-center w-full text-sm px-4 py-2 rounded", {
+ "bg-secondary":
+ state.route === route &&
+ (resource ? state.resource === resource : true),
+ })}
+ >
+
+ {Icon &&
}
+
+ {children}
+
+ {actions &&
{actions}
}
+
+
+ )
+ },
+)
diff --git a/src/components/SidebarSection.tsx b/src/components/SidebarSection.tsx
new file mode 100644
index 0000000..8e97612
--- /dev/null
+++ b/src/components/SidebarSection.tsx
@@ -0,0 +1,16 @@
+import { observer } from "mobx-react"
+
+export const SidebarSection = observer(
+ ({ title, actions, children }: { children: React.ReactNode }) => (
+
+
+
+ {title}
+ {actions}
+
+
+ {children}
+
+
+ ),
+)
diff --git a/src/components/SliderCheckbox.tsx b/src/components/SliderCheckbox.tsx
new file mode 100644
index 0000000..d986cdc
--- /dev/null
+++ b/src/components/SliderCheckbox.tsx
@@ -0,0 +1,42 @@
+import { Slider as Base } from "./ui/slider"
+import { Label } from "./ui/label"
+import { observer } from "mobx-react"
+import { Checkbox } from "./ui/checkbox"
+import { cn } from "@/lib/utils"
+import { Input } from "./ui/input"
+
+export const SliderCheckbox = observer(
+ ({ label, id, value, onChange, checked, onCheckedChange, ...props }) => (
+
+
+
+ {
+ onCheckedChange(true)
+ onChange(Number(e.target.value))
+ }}
+ />
+
+
{
+ onCheckedChange(true)
+ onChange(v[0])
+ }}
+ className="ml-6 [&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
+ aria-label={label}
+ {...props}
+ />
+
+ ),
+)
diff --git a/src/app/ToggleDarkButton.tsx b/src/components/ToggleDarkButton.tsx
similarity index 76%
rename from src/app/ToggleDarkButton.tsx
rename to src/components/ToggleDarkButton.tsx
index 851fe7d..a934258 100644
--- a/src/app/ToggleDarkButton.tsx
+++ b/src/components/ToggleDarkButton.tsx
@@ -2,7 +2,7 @@ import { Button } from "@/components/ui/button"
import { BsFillLightbulbFill, BsLightbulb } from "react-icons/bs"
import { useTheme } from "@/components/ThemeProvider"
-export const ToggleDarkButton = () => {
+export const ToggleDarkButton = ({ size = "1em" }) => {
const theme = useTheme()
const toggleTheme = () => {
@@ -12,9 +12,9 @@ export const ToggleDarkButton = () => {
return (
{theme.theme == "dark" ? (
-
+
) : (
-
+
)}
)
diff --git a/src/components/VariablesAccordionItem.tsx b/src/components/VariablesAccordionItem.tsx
new file mode 100644
index 0000000..b4bf5c3
--- /dev/null
+++ b/src/components/VariablesAccordionItem.tsx
@@ -0,0 +1,42 @@
+import { observer } from "mobx-react"
+import { useStore } from "@/store"
+import { Input } from "@/components/ui/input"
+import { IoMdAdd, IoMdTrash } from "react-icons/io"
+import { Button } from "@/components/ui/button"
+import { HiMiniVariable } from "react-icons/hi2"
+import { AppAccordionItem } from "../app/AppAccordionItem"
+
+export const VariablesAccordionItem = observer(() => {
+ const store = useStore()
+ const {
+ state: { resource: agent },
+ } = store
+
+ return (
+
+ agent.addVariable({ key: "", value: "" })}>
+
+
+
+
+
+ )
+})
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 0ba4277..204e7f8 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -21,6 +21,7 @@ const buttonVariants = cva(
},
size: {
default: "h-10 px-4 py-2",
+ xs: "h-8 rounded-md px-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
@@ -30,7 +31,7 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
- }
+ },
)
export interface ButtonProps
@@ -49,7 +50,7 @@ const Button = React.forwardRef(
{...props}
/>
)
- }
+ },
)
Button.displayName = "Button"
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index 677d05f..9cc0e54 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.tsx
@@ -5,21 +5,20 @@ import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes {}
-const Input = React.forwardRef(
+export const Input = React.forwardRef(
({ className, type, ...props }, ref) => {
return (
)
- }
+ },
)
-Input.displayName = "Input"
-export { Input }
+Input.displayName = "Input"
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
index 9161381..c2468e8 100644
--- a/src/components/ui/select.tsx
+++ b/src/components/ui/select.tsx
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
- className
+ className,
)}
{...props}
>
@@ -41,7 +41,7 @@ const SelectContent = React.forwardRef<
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
- className
+ className,
)}
position={position}
{...props}
@@ -50,7 +50,7 @@ const SelectContent = React.forwardRef<
className={cn(
"p-1",
position === "popper" &&
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
@@ -80,7 +80,7 @@ const SelectItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
+ className,
)}
{...props}
>
diff --git a/src/index.tsx b/src/index.tsx
index 6553549..db0d718 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,23 +1,40 @@
import React from "react"
import ReactDOM from "react-dom/client"
-import App from "./app/App"
+import AppView from "./app/AppView"
import { Store, StoreContext } from "./store"
import { ThemeProvider } from "./components/ThemeProvider"
import { onSnapshot } from "mobx-state-tree"
import "./index.css"
-let data = {}
-const raw = window.localStorage.getItem("store")
-if (raw) {
- data = JSON.parse(window.localStorage.getItem("store")!)
+function loadStore() {
+ let data = {}
+ const raw = window.localStorage.getItem("store")
+ if (raw) {
+ data = JSON.parse(window.localStorage.getItem("store")!)
+ }
+ try {
+ return Store.create(data)
+ } catch (e) {
+ console.error(e)
+ if (
+ window.confirm(
+ "Could not to load application data from local storage, clear it?",
+ )
+ ) {
+ window.localStorage.removeItem("store")
+ return Store.create({})
+ }
+
+ throw e
+ }
}
-const store = Store.create(data)
+const store = loadStore()
ReactDOM.createRoot(document.getElementById("root")!).render(
-
-
+
+
,
diff --git a/src/lib/OllamaClient.ts b/src/lib/OllamaClient.ts
deleted file mode 100644
index c1f4c9f..0000000
--- a/src/lib/OllamaClient.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-type GenerateOptions = {
- stop?: string
- seed?: number
- temperature?: number
- top_k?: number
- top_p?: number
- repeat_penalty?: number
- num_predict?: number
- num_ctx?: number
-}
-
-export class OllamaClient {
- endpoint: string
-
- constructor(endpoint: string) {
- this.endpoint = endpoint
- }
-
- async models() {
- const models = await fetch(`${this.endpoint}/api/tags`)
- const result = await models.json()
- return result.models
- }
-
- async info(modelName: string) {
- const options = await fetch(`${this.endpoint}/api/show`, {
- method: "POST",
- body: JSON.stringify({
- name: modelName,
- }),
- })
- return await options.json()
- }
- _completion(prompt: string, model: string, options?: GenerateOptions) {
- return fetch(`${this.endpoint}/api/generate`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- prompt,
- model,
- stream: true,
- template: "{{ .Prompt }}",
- options,
- }),
- })
- }
-
- async *completion(prompt: string, model: string, options?: GenerateOptions) {
- const response = await this._completion(prompt, model, options)
-
- if (!response.ok || !response.body) {
- throw new Error(
- `Failed to generate: ${response.status} ${await response.text()}`,
- )
- }
-
- const reader = response.body.getReader()
- const decoder = new TextDecoder()
-
- while (true) {
- const { done, value } = await reader.read()
- if (done) {
- break
- }
- const chunks = decoder.decode(value).split("\n")
- for (const chunk of chunks) {
- if (!chunk) {
- continue
- }
- try {
- const json = JSON.parse(chunk)
- if (json.done) {
- break
- }
- yield json.response
- } catch (e) {
- console.error(chunk)
- throw e
- }
- }
- }
- }
-}
diff --git a/src/lib/adapters/HuggingFace.ts b/src/lib/adapters/HuggingFace.ts
new file mode 100644
index 0000000..839de87
--- /dev/null
+++ b/src/lib/adapters/HuggingFace.ts
@@ -0,0 +1,56 @@
+// https://github.com/huggingface/text-generation-inference/blob/main/docs/openapi.json
+
+import { HfInferenceEndpoint, TextGenerationArgs } from "@huggingface/inference"
+import { listModels } from "@huggingface/hub"
+import { GenerationParams } from "@/store/Agent"
+import { Instance } from "mobx-state-tree"
+
+function transformParams(
+ params: Instance,
+): TextGenerationArgs["parameters"] {
+ return {
+ max_new_tokens: params.num_predict,
+ repetition_penalty: params.repeat_penalty,
+ return_full_text: true,
+ temperature: params.temperature,
+ top_k: params.top_k,
+ top_p: params.top_p,
+ stop_sequences: params.stop,
+ do_sample: true,
+ // TODO: add support for these params
+ // max_time
+ // num_return_sequences
+ // truncate
+ }
+}
+export class HuggingFaceAdapter {
+ endpoint: HfInferenceEndpoint
+ parameters = [
+ "num_predict",
+ "repeat_last_n",
+ "repeat_penalty",
+ "stop",
+ "temperature",
+ "top_p",
+ "top_k",
+ ] as (keyof Instance)[]
+
+ constructor(public baseUrl: string) {
+ this.endpoint = new HfInferenceEndpoint(baseUrl)
+ }
+
+ async *completion(
+ prompt: string,
+ model: string, // hf endpoints are not multi-model
+ options?: Instance,
+ signal?: AbortSignal,
+ ) {
+ for await (const output of this.endpoint.textGenerationStream({
+ inputs: prompt,
+ parameters: transformParams(options ?? {}),
+ })) {
+ if (signal?.aborted) break
+ yield output.token.text
+ }
+ }
+}
diff --git a/src/lib/adapters/Ollama.ts b/src/lib/adapters/Ollama.ts
new file mode 100644
index 0000000..f86c794
--- /dev/null
+++ b/src/lib/adapters/Ollama.ts
@@ -0,0 +1,134 @@
+import { GenerationParams } from "@/store/Agent"
+import { Instance } from "mobx-state-tree"
+
+type OllamaCompletionRequestParams = {
+ num_ctx?: number
+ num_predict?: number
+ repeat_last_n?: number
+ repeat_penalty?: number
+ seed?: number
+ stop?: string[]
+ temperature?: number
+ top_k?: number
+ top_p?: number
+}
+
+export class OllamaAdapter {
+ baseUrl: string
+ parameters = [
+ "num_ctx",
+ "num_predict",
+ "repeat_last_n",
+ "repeat_penalty",
+ "seed",
+ "stop",
+ "temperature",
+ "top_p",
+ "top_k",
+ ] as (keyof Instance)[]
+
+ constructor(baseUrl: string) {
+ this.baseUrl = baseUrl
+ }
+
+ async models() {
+ const models = await fetch(`${this.baseUrl}/api/tags`)
+ const result = await models.json()
+ return result.models
+ }
+
+ async meta(modelName: string) {
+ const options = await fetch(`${this.baseUrl}/api/show`, {
+ method: "POST",
+ body: JSON.stringify({
+ name: modelName,
+ }),
+ })
+ return await options.json()
+ }
+
+ async embed(prompt: string, model: string) {
+ const embeddings = await fetch(`${this.baseUrl}/api/embeddings`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ model, prompt }),
+ })
+ const res = await embeddings.json()
+ return res.embedding
+ }
+
+ completionRequest(
+ prompt: string,
+ model: string,
+ options?: OllamaCompletionRequestParams,
+ signal?: AbortSignal,
+ ) {
+ return fetch(`${this.baseUrl}/api/generate`, {
+ signal,
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ prompt,
+ model,
+ stream: true,
+ template: "{{ .Prompt }}",
+ options,
+ }),
+ })
+ }
+
+ async *completion(
+ prompt: string,
+ model: string,
+ options?: Instance,
+ signal?: AbortSignal,
+ ) {
+ const response = await this.completionRequest(
+ prompt,
+ model,
+ options,
+ signal,
+ )
+
+ if (!response.ok || !response.body) {
+ throw new Error(
+ `Failed to generate: ${response.status} ${await response.text()}`,
+ )
+ }
+
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder()
+
+ let remainder = ""
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) {
+ break
+ }
+ const chunks = (remainder + decoder.decode(value)).split("\n")
+ for (const chunk of chunks) {
+ if (!chunk) {
+ continue
+ }
+ try {
+ const json = JSON.parse(chunk)
+ if (json.done) {
+ break
+ }
+ yield json.response
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ remainder += chunk
+ continue
+ }
+ console.error(chunk)
+ throw e
+ }
+ }
+ }
+ }
+}
diff --git a/src/lib/adapters/index.ts b/src/lib/adapters/index.ts
new file mode 100644
index 0000000..5301b1a
--- /dev/null
+++ b/src/lib/adapters/index.ts
@@ -0,0 +1,12 @@
+import { OllamaAdapter } from "."
+import { HuggingFaceAdapter } from "."
+
+export { HuggingFaceAdapter } from "./HuggingFace"
+export { OllamaAdapter } from "./Ollama"
+
+export const AdapterMap = {
+ HuggingFace: HuggingFaceAdapter,
+ Ollama: OllamaAdapter,
+}
+
+export const AdapterList = Object.keys(AdapterMap)
diff --git a/src/lib/extraction.ts b/src/lib/extraction.ts
index 265095d..58f185f 100644
--- a/src/lib/extraction.ts
+++ b/src/lib/extraction.ts
@@ -52,7 +52,7 @@ async function extractWithPDFParse(file: File) {
return text
}
-async function _processFile(file: File) {
+export async function processFile(file: File) {
if (file.name.endsWith(".docx")) {
return extractWithMammoth(file)
} else if (file.name.endsWith(".pdf")) {
@@ -62,36 +62,31 @@ async function _processFile(file: File) {
}
}
-function stripNonPrintableAndNormalize(
- text: string,
- stripSurrogatesAndFormats = true,
-) {
- // strip control chars. optionally, keep surrogates and formats
- if (stripSurrogatesAndFormats) {
- // text = text.replace(/\p{C}/gu, "")
- } else {
- // text = text.replace(/\p{Cc}/gu, "")
- text = text.replace(/\p{Co}/gu, "")
- text = text.replace(/\p{Cn}/gu, "")
- }
-
- // other common tasks are to normalize newlines and other whitespace
-
- // normalize newline
- text = text.replace(/\n\r/g, "\n")
- text = text.replace(/\p{Zl}/gu, "\n")
- text = text.replace(/\p{Zp}/gu, "\n")
-
- // normalize space
- return text.replace(/\p{Zs}/gu, " ")
-}
-
-async function processFile(file: File) {
- const text = await _processFile(file)
- return stripNonPrintableAndNormalize(text)
-}
-
-export async function processFiles(files: FileList) {
- const results = await Promise.all(Array.from(files).map(processFile))
- return results.filter(Boolean)
-}
+// function stripNonPrintableAndNormalize(
+// text: string,
+// stripSurrogatesAndFormats = true,
+// ) {
+// // strip control chars. optionally, keep surrogates and formats
+// if (stripSurrogatesAndFormats) {
+// // text = text.replace(/\p{C}/gu, "")
+// } else {
+// // text = text.replace(/\p{Cc}/gu, "")
+// text = text.replace(/\p{Co}/gu, "")
+// text = text.replace(/\p{Cn}/gu, "")
+// }
+
+// // other common tasks are to normalize newlines and other whitespace
+
+// // normalize newline
+// text = text.replace(/\n\r/g, "\n")
+// text = text.replace(/\p{Zl}/gu, "\n")
+// text = text.replace(/\p{Zp}/gu, "\n")
+
+// // normalize space
+// return text.replace(/\p{Zs}/gu, " ")
+// }
+
+// export async function processFile(file: File) {
+// const text = await _processFile(file)
+// return stripNonPrintableAndNormalize(text)
+// }
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
deleted file mode 100644
index 4aee78d..0000000
--- a/src/lib/utils.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { type ClassValue, clsx } from "clsx"
-import { twMerge } from "tailwind-merge"
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
-}
-
-export const randomId = () => Math.random().toString(36).substr(2, 9)
-
-export function humanFileSize(size: number) {
- if (size > 0) {
- const i = Math.floor(Math.log(size) / Math.log(1024))
- return (
- (size / Math.pow(1024, i)).toFixed(2) * 1 +
- ["B", "kB", "MB", "GB", "TB"][i]
- )
- }
- return "0.00MB"
-}
diff --git a/src/lib/utils.tsx b/src/lib/utils.tsx
new file mode 100644
index 0000000..45f3dc4
--- /dev/null
+++ b/src/lib/utils.tsx
@@ -0,0 +1,121 @@
+import rehypeParse from "rehype-parse"
+import rehypeRemark from "rehype-remark"
+import remarkStringify from "remark-stringify"
+import remarkMath from "remark-math"
+import remarkGfm from "remark-gfm"
+import remarkEmoji from "remark-emoji"
+import { twMerge } from "tailwind-merge"
+import { unified } from "unified"
+import { type ClassValue, clsx } from "clsx"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
+
+export const randomId = () => Math.random().toString(36).substr(2, 9)
+
+export function humanFileSize(size: number) {
+ if (size > 0) {
+ const i = Math.floor(Math.log(size) / Math.log(1024))
+ return (
+ (size / Math.pow(1024, i)).toFixed(2) * 1 +
+ ["B", "kB", "MB", "GB", "TB"][i]
+ )
+ }
+ return "0.00MB"
+}
+
+export function cosineSimilarity(v1: number[], v2: number[]) {
+ if (v1.length !== v2.length) {
+ return -1
+ }
+ let dotProduct = 0
+ let v1_mag = 0
+ let v2_mag = 0
+ for (let i = 0; i < v1.length; i++) {
+ dotProduct += v1[i] * v2[i]
+ v1_mag += v1[i] ** 2
+ v2_mag += v2[i] ** 2
+ }
+ return dotProduct / (Math.sqrt(v1_mag) * Math.sqrt(v2_mag))
+}
+
+export function zip(...arrays: any[]) {
+ return arrays[0].map((_: any, i: string | number) =>
+ arrays.map((array: { [x: string]: any }) => array[i]),
+ )
+}
+export function wrapArray(value: any) {
+ return Array.isArray(value) ? value : [value]
+}
+
+export function registerTag(
+ engine,
+ key: string,
+ fn: (value: any, emitter: Emitter) => void,
+) {
+ engine.registerTag(
+ key,
+ class extends Tag {
+ private value: Value
+ constructor(
+ token: TagToken,
+ remainTokens: TopLevelToken[],
+ liquid: Liquid,
+ ) {
+ super(token, remainTokens, liquid)
+ this.value = new Value(token.args, liquid)
+ }
+ async render(ctx: Context, emitter: Emitter) {
+ const value = await toPromise(this.value.value(ctx))
+ fn(value, emitter)
+ }
+ },
+ )
+}
+export function html2md(data: any) {
+ return unified()
+ .use(rehypeParse)
+ .use(rehypeRemark)
+ .use(remarkGfm)
+ .use(remarkEmoji)
+ .use(remarkMath)
+ .use(remarkStringify)
+ .processSync(data).value
+}
+
+export async function extractHTML(input: string) {
+ const url = new URL(input)
+ const response = await fetch(input)
+ const html = await response.text()
+ const doc = new DOMParser().parseFromString(html, "text/html")
+ const body = doc.querySelector("body")
+
+ function removeComments(node: Node) {
+ if (node.nodeType === Node.COMMENT_NODE) {
+ node.parentNode?.removeChild(node)
+ } else {
+ for (let i = 0; i < node.childNodes.length; i++) {
+ removeComments(node.childNodes[i])
+ }
+ }
+ }
+
+ removeComments(body)
+
+ body?.querySelectorAll("img").forEach((img) => {
+ const src = img.getAttribute("src")
+ if (!src?.startsWith("http")) {
+ img.setAttribute("src", url.origin + src)
+ }
+ })
+
+ body?.querySelectorAll("a").forEach((a) => {
+ const href = a.getAttribute("href")
+ if (!href?.startsWith("http")) {
+ a.setAttribute("href", url.origin + href)
+ }
+ })
+
+ return body
+}
diff --git a/src/store/AdapterFactory.ts b/src/store/AdapterFactory.ts
new file mode 100644
index 0000000..40fbbf1
--- /dev/null
+++ b/src/store/AdapterFactory.ts
@@ -0,0 +1,43 @@
+import { types as t } from "mobx-state-tree"
+import { Instance } from "mobx-state-tree"
+import { AdapterList, AdapterMap } from "@/lib/adapters"
+import { Model } from "./Model"
+import { autorun } from "mobx"
+
+export const AdapterFactory = t
+ .model("AdapterFactory", {
+ baseUrl: t.optional(t.string, ""),
+ models: t.optional(t.array(Model), []),
+ type: t.optional(t.enumeration(AdapterList), AdapterList[0]),
+ })
+ .views((self) => ({
+ get isMultiModal() {
+ // todo: make an abstract adapter class for multi-model endpoints
+ return self.type === "Ollama"
+ },
+ get instance() {
+ return new AdapterMap[self.type as keyof typeof AdapterMap](self.baseUrl)
+ },
+
+ get parameters() {
+ return this.instance.parameters
+ },
+ }))
+ .actions((self) => ({
+ afterCreate() {
+ this.refreshModels()
+ },
+ update(props: Partial>) {
+ Object.assign(self, props)
+ },
+ async refreshModels() {
+ if (self.isMultiModal) {
+ try {
+ const models = await self.instance.models()
+ this.update({ models })
+ } catch (e) {
+ // console.error(e)
+ }
+ }
+ },
+ }))
diff --git a/src/store/Agent.ts b/src/store/Agent.ts
new file mode 100644
index 0000000..8d54ea0
--- /dev/null
+++ b/src/store/Agent.ts
@@ -0,0 +1,202 @@
+import { Liquid } from "liquidjs"
+import { destroy, types as t } from "mobx-state-tree"
+import { randomId } from "@/lib/utils"
+import { Message } from "./Message"
+import { Instance } from "mobx-state-tree"
+import { reaction, toJS } from "mobx"
+import { AdapterFactory } from "./AdapterFactory"
+import { Template } from "./Template"
+
+export const GenerationParams = t.model("Options", {
+ temperature: t.optional(t.number, 0.7),
+ top_k: t.optional(t.number, 40),
+ top_p: t.optional(t.number, 0.9),
+ repeat_last_n: t.optional(t.number, -1),
+ repeat_penalty: t.optional(t.number, 1.18),
+ num_predict: t.optional(t.number, -2),
+ num_ctx: t.optional(t.number, 2048),
+ stop: t.optional(t.array(t.string), []),
+})
+
+const Variable = t.model("Variable", {
+ key: t.string,
+ value: t.string,
+})
+
+type Turn = {
+ user: string
+ assistant: string
+}
+
+export const Agent = t
+ .model("Agent", {
+ id: t.optional(t.identifier, randomId),
+ name: t.optional(t.string, ""),
+ model: t.optional(t.string, ""),
+ adapter: t.optional(AdapterFactory, {}),
+ parameters: t.optional(GenerationParams, {}),
+ checkedOptions: t.optional(t.array(t.string), []),
+ promptTemplate: t.optional(t.string, ""),
+ promptPreview: t.optional(t.string, ""),
+ messages: t.array(Message),
+ variables: t.array(Variable),
+ templates: t.array(Template),
+ })
+
+ .views((self) => ({
+ get overridedOptions() {
+ return Object.fromEntries(
+ self.checkedOptions.map((key) => [
+ key,
+ self.parameters[key as keyof typeof self.parameters],
+ ]),
+ )
+ },
+ get templateMap() {
+ return new Map(self.templates.map((t) => [t.id, t.content]))
+ },
+ get engine() {
+ const self = this
+ return new Liquid({
+ relativeReference: false,
+ fs: {
+ readFileSync(file: string) {
+ return self.templateMap.get(file) ?? ""
+ },
+ async readFile(file: string) {
+ return self.templateMap.get(file) ?? ""
+ },
+ existsSync(file) {
+ return self.templateMap.has(file)
+ },
+ async exists(file) {
+ return self.templateMap.has(file)
+ },
+ contains() {
+ return true
+ },
+ resolve(_root, file, _ext) {
+ return file
+ },
+ },
+ })
+ },
+ async renderTemplate(template: string, extra = {}) {
+ const locals = {
+ ...extra,
+ ...Object.fromEntries(self.variables.map((v) => [v.key, v.value])),
+ }
+ const templates = this.engine.parse(template)
+
+ return await this.engine.render(templates, locals)
+ },
+ turns(messages: Instance[] = []): Turn[] {
+ messages = messages.filter((message) => message.role !== "system")
+ let prev: any
+ return messages.reduce((acc, message) => {
+ if (prev && message.role === "assistant") {
+ acc.push({
+ user: prev.content,
+ assistant: message.content,
+ })
+ }
+ prev = message
+
+ return acc
+ }, [])
+ },
+ async buildTemplate(messages: Instance[] = [], extra = {}) {
+ messages = [...self.messages, ...messages]
+
+ const turns = this.turns(messages)
+
+ const locals = {
+ ...extra,
+ system: messages.filter((message) => message.role === "system"),
+ turns,
+ messages: messages
+ .filter((message) => message.open || !message.isEmpty)
+ .map((message) => ({
+ closed: !message.open,
+ ...message,
+ })),
+ }
+
+ let result = await this.renderTemplate(self.promptTemplate, locals)
+
+ let last = null
+ while (last !== result) {
+ last = result
+ result = await this.renderTemplate(result, extra)
+ }
+ return result
+ },
+
+ completion(prompt: string, signal?: AbortSignal) {
+ console.log(prompt)
+ return self.adapter.instance.completion(
+ prompt,
+ self.model,
+ this.overridedOptions as typeof self.parameters,
+ signal,
+ )
+ },
+ }))
+ .actions((self) => ({
+ afterCreate() {
+ reaction(
+ () =>
+ [self.promptTemplate, self.templates, self.turns, self.messages].map(
+ (x) => toJS(x),
+ ),
+ this.handlePromptTemplateChange,
+ )
+ this.handlePromptTemplateChange()
+ },
+ update(props: Partial>) {
+ Object.assign(self, props)
+ },
+ remove(thing: any) {
+ destroy(thing)
+ },
+ addVariable(variable: Partial>) {
+ self.variables.push(
+ Variable.create(variable as Instance),
+ )
+ },
+ addMessage(message: Partial>) {
+ self.messages.push(Message.create(message))
+ },
+ addTemplate(
+ template: Partial> &
+ Pick, "id">,
+ ) {
+ self.templates.push(Template.create(template))
+ },
+ removeMessage(message: Instance) {
+ self.messages.remove(message)
+ },
+ async handlePromptTemplateChange() {
+ try {
+ const promptPreview = await self.renderTemplate(self.promptTemplate, {
+ prompt: "What is your name?",
+ messages: [
+ ...self.messages,
+ Message.create({
+ role: "user",
+ content: "My name is Bob",
+ }),
+ Message.create({
+ role: "assistant",
+ content: "Hello Bob!",
+ }),
+ ],
+ })
+
+ this.update({ promptPreview })
+ } catch (e: any) {
+ console.log(e.message)
+ return e.message
+ }
+ },
+ }))
diff --git a/src/store/Attachment.ts b/src/store/Attachment.ts
new file mode 100644
index 0000000..36838b5
--- /dev/null
+++ b/src/store/Attachment.ts
@@ -0,0 +1,37 @@
+import { Instance, getParent, types as t } from "mobx-state-tree"
+import { Chat } from "./Chat"
+
+export const Attachment = t
+ .model("Attachment", {
+ name: t.string,
+ content: t.string,
+ path: t.string,
+ size: t.number,
+ type: t.string,
+ embedding: t.maybeNull(t.array(t.number)),
+ })
+ .views((self) => ({
+ get chat() {
+ return getParent>(self, 2)
+ },
+ get lines() {
+ // return self.content.split(/[\r\n\.\:]/g).filter(Boolean)
+ return self.content.split(/[\r\n]/g).filter(Boolean)
+ },
+ get chunks() {
+ const chunks = []
+ for (let i = 0; i < this.lines.length; i++) {
+ chunks.push(
+ [this.lines[i - 1], this.lines[i], this.lines[i + 1]]
+ .filter(Boolean)
+ .join("\n"),
+ )
+ }
+ return [...new Set(chunks)]
+ },
+ }))
+ .actions((self) => ({
+ update(props: Partial) {
+ Object.assign(self, props)
+ },
+ }))
diff --git a/src/store/Chat.ts b/src/store/Chat.ts
new file mode 100644
index 0000000..9ac927c
--- /dev/null
+++ b/src/store/Chat.ts
@@ -0,0 +1,232 @@
+import {
+ toPromise,
+ TagToken,
+ Context,
+ Emitter,
+ TopLevelToken,
+ Value,
+ Tag,
+ Liquid,
+} from "liquidjs"
+
+import MiniSearch from "minisearch"
+
+import { destroy, detach, isAlive, types as t } from "mobx-state-tree"
+import { randomId } from "@/lib/utils"
+import { Message } from "./Message"
+import { Instance } from "mobx-state-tree"
+
+import { Agent } from "./Agent"
+import { Attachment } from "./Attachment"
+import { autorun } from "mobx"
+
+export const Chat = t
+ .model("Chat", {
+ id: t.optional(t.identifier, randomId),
+ prompt: t.optional(t.string, ""),
+ agent: t.safeReference(Agent),
+ attachments: t.array(Attachment),
+ messages: t.array(Message),
+ embeddingModelName: t.optional(t.string, "Supabase/gte-small"),
+ createdAt: t.optional(t.Date, () => new Date()),
+ })
+ .volatile((self) => ({
+ abortController: null as AbortController | null,
+ }))
+ .views((self) => ({
+ get nonEmptyMessages() {
+ return self.messages.filter((m) => !m.isEmpty)
+ },
+ get userMessages() {
+ return this.nonEmptyMessages.filter((message) => message.role === "user")
+ },
+ get assistantMessages() {
+ return this.nonEmptyMessages.filter(
+ (message) => message.role === "assistant",
+ )
+ },
+ get lastMessage() {
+ return this.nonEmptyMessages[this.nonEmptyMessages.length - 1]
+ },
+ get lastUserMessage() {
+ return this.userMessages[this.userMessages.length - 1]
+ },
+ get lastAssistantMessage() {
+ return this.assistantMessages[this.assistantMessages.length - 1]
+ },
+ get title() {
+ if (this.userMessages.length === 0) {
+ if (self.agent) {
+ return `New Chat with ${self.agent.name}`
+ }
+ return `New Chat`
+ }
+ return this.lastMessage?.content.slice(0, 80)
+ },
+ get modifiedAt() {
+ return this.lastMessage?.date ?? self.createdAt
+ },
+ get documents() {
+ return Object.fromEntries(
+ self.attachments.flatMap((attachment) => {
+ return attachment.lines.map((line: string, i) => {
+ return [
+ `File "${attachment.path}", Line ${i + 1}`,
+ [attachment.lines[i - 1], line, attachment.lines[i + 1]]
+ .join(" ")
+ .trim(),
+ ].filter(Boolean)
+ })
+ }),
+ )
+ },
+ get index() {
+ let miniSearch = new MiniSearch({
+ fields: ["id", "text"], // fields to index for full-text search
+ storeFields: ["id", "text"], // fields to return with search results
+ })
+
+ Object.keys(this.documents).forEach((key) => {
+ miniSearch.add({ id: key, text: this.documents[key] })
+ })
+ return miniSearch
+ },
+ get isRunning() {
+ return !!self.abortController
+ },
+ get locals() {
+ return {
+ prompt: this.lastUserMessage?.content || self.prompt,
+ attachments: self.attachments.map((x) => [...new Set(x.lines)]),
+ }
+ },
+ }))
+
+ .actions((self) => ({
+ abort() {
+ self.abortController?.abort()
+ self.update({ abortController: null })
+ },
+ afterCreate() {
+ autorun(() => self.registerTags(self.agent?.engine))
+ },
+ registerTags(engine) {
+ if (!engine) return
+
+ // register("retrieve", async (value, emitter) => {
+ // if (!self.agent || !self.agent.model) return
+ // self.index.search(value).forEach((doc) => {
+ // emitter.write(`- "${doc.text}" (${doc.id})\n`)
+ // })
+ // })
+ },
+
+ addMessage(message: Instance) {
+ self.messages.push(message)
+ },
+ clear() {
+ self.messages.clear()
+ },
+ pop() {
+ while (self.lastMessage?.role === "assistant") {
+ self.messages.pop()
+ }
+
+ const content = self.lastMessage?.content
+ const lastUserMessage = self.messages.pop()
+ if (lastUserMessage) {
+ self.prompt = content
+ }
+ },
+ send(content: string, { reply = true } = {}) {
+ if (!content) return
+
+ const message = {
+ role: "user",
+ content,
+ date: new Date(),
+ }
+
+ self.messages.push(message)
+
+ if (reply) {
+ this.reply()
+ }
+ },
+ async reply({ newMessage = true } = {}) {
+ if (!self.agent) return
+
+ const messages = self.nonEmptyMessages
+
+ let message
+
+ try {
+ const template = await self.agent.buildTemplate(messages, self.locals)
+
+ if (newMessage) {
+ messages.push(
+ Message.create({
+ role: "assistant",
+ content: "",
+ open: true,
+ }),
+ )
+ } else {
+ self.lastMessage.update({ open: true })
+ }
+
+ this.abort()
+ this.update({ abortController: new AbortController() })
+
+ const stream = self.agent.completion(
+ template,
+ self.abortController!.signal,
+ )
+
+ message = self.lastMessage
+ if (newMessage) {
+ message = Message.create({
+ role: "assistant",
+ content: "",
+ open: true,
+ })
+ this.addMessage(message)
+ }
+
+ for await (const chunk of stream) {
+ if (isAlive(message)) {
+ message.update({ content: message.content + chunk })
+ } else {
+ this.abort()
+ }
+ }
+ } catch (error) {
+ if (error.name === "AbortError") {
+ return
+ }
+ const content = "An error occurred. Check the console for more details."
+ if (!message) {
+ message = Message.create({
+ role: "assistant",
+ content,
+ })
+ this.addMessage(message)
+ }
+ message.update({ content })
+ console.error(error)
+ } finally {
+ message?.update({ open: false })
+ this.update({ abortController: null })
+ }
+ },
+ regenerate() {
+ self.messages.pop()
+ this.reply()
+ },
+ update(props: Partial>) {
+ Object.assign(self, props)
+ },
+ remove(thing: any) {
+ destroy(thing)
+ },
+ }))
diff --git a/src/store/Chat.tsx b/src/store/Chat.tsx
deleted file mode 100644
index ed3dfae..0000000
--- a/src/store/Chat.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import Mustache from "mustache"
-import { destroy, getParent, types as t } from "mobx-state-tree"
-import { randomId } from "@/lib/utils"
-import { Message } from "./Message"
-import { CHAT_TEMPLATES } from "./Presets"
-import { Instance } from "mobx-state-tree"
-Mustache.escape = (text) => text
-
-const DEFAULT_SYSTEM_MESSAGE = `A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions. Current time is {{time}}.`
-const DEFAULT_USER_MESSAGE = ``
-const DEFAULT_ASSISTANT_MESSAGE = `In order to provide short and concise responses without any unnecessary apologies or social formalities, I will get straight to the point in my communication. This approach allows us to be efficient with our time and focus on conveying important information clearly and accurately. By cutting out superfluous language, we can avoid wasting words and ensure that our messages are direct and impactful. Additionally, this style of communication promotes a more casual and relaxed interaction between individuals, allowing for a more natural flow of conversation without any artificial barriers or obstacles. Overall, short and concise answers with no apologies or social formalities enable us to communicate`
-
-const InferenceOptions = t.model("Options", {
- temperature: t.optional(t.number, 0.7),
- top_k: t.optional(t.number, 40),
- top_p: t.optional(t.number, 0.9),
- repeat_last_n: t.optional(t.number, -1),
- repeat_penalty: t.optional(t.number, 1.18),
- num_predict: t.optional(t.number, -2),
- num_ctx: t.optional(t.number, 2048),
- stop: t.optional(t.array(t.string), []),
-})
-
-export const Chat = t
- .model("Chat", {
- id: t.optional(t.identifier, randomId),
- prompt: t.optional(t.string, ""),
- model: t.maybeNull(t.string),
- chat_template: t.optional(t.string, Object.values(CHAT_TEMPLATES)[0]),
- attachments: t.array(t.string),
- system_message: t.optional(t.string, DEFAULT_SYSTEM_MESSAGE),
- user_message: t.optional(t.string, DEFAULT_USER_MESSAGE),
- assistant_message: t.optional(t.string, DEFAULT_ASSISTANT_MESSAGE),
- options: t.optional(InferenceOptions, {}),
- messages: t.array(Message),
- history: t.array(Message),
- })
- .views((self) => ({
- get nonEmptyMessages() {
- return self.messages.filter((m) => !m.isEmpty)
- },
- get userMessages() {
- return this.nonEmptyMessages.filter((message) => message.role === "user")
- },
- get assistantMessages() {
- return this.nonEmptyMessages.filter(
- (message) => message.role === "assistant",
- )
- },
- get lastMessage() {
- return this.nonEmptyMessages[this.nonEmptyMessages.length - 1]
- },
- get lastUserMessage() {
- return this.userMessages[this.userMessages.length - 1]
- },
- get lastAssistantMessage() {
- return this.assistantMessages[this.assistantMessages.length - 1]
- },
- get title() {
- if (this.userMessages.length === 0) {
- if (self.model) {
- return `New Chat with ${self.model}`
- }
- return `New Chat`
- }
- return this.lastMessage?.content.slice(0, 20)
- },
- renderTemplate(template: string, extra = {}) {
- try {
- return Mustache.render(template, {
- upperCase: () => (text, render) => render(text).toUpperCase(),
- attachments: self.attachments.join("\n"),
- system: self.system_message,
- user: self.user_message,
- assistant: self.assistant_message,
- prompt: this.lastUserMessage?.content,
- time: new Date().toLocaleString(),
- ...extra,
- })
- } catch (e) {
- return e.message
- }
- },
- get template() {
- return this.buildTemplate()
- },
- buildTemplate(newMessages: Instance[] = []) {
- const messages = [
- ...self.history,
- ...self.messages,
- ...newMessages,
- ].filter(Boolean)
-
- let result = this.renderTemplate(self.chat_template, {
- messages: messages
- .filter((message) => message.open || !message.isEmpty)
- .map((message) => ({
- closed: !message.open,
- ...message,
- })),
- })
-
- let last = null
- while (last !== result) {
- last = result
- result = this.renderTemplate(result)
- }
- return result
- },
- }))
- .actions((self) => ({
- setPrompt(prompt: string) {
- self.prompt = prompt
- },
- addHistoryMessage(message: Instance) {
- self.history.push(message)
- },
- setAttachments(attachments: string[]) {
- self.attachments.replace(attachments)
- },
- setChatTemplate(template: string) {
- self.chat_template = template
- },
- setUserMessage(message: string) {
- self.user_message = message
- },
- setAssistantMessage(message: string) {
- self.assistant_message = message
- },
- addMessage(message: Instance) {
- self.messages.push(message)
- },
- onClearConversation() {
- self.messages.clear()
- },
- pop() {
- const message = self.lastMessage
- if (message.role === "user") {
- self.prompt = message.content
- }
- self.messages.pop()
- },
- userMessage(content: string) {
- if (!content) return
-
- const message = {
- role: "user",
- content,
- date: new Date(),
- }
-
- self.messages.push(message)
-
- this.respond()
- },
- async respond(newMessage = true) {
- const { client } = getParent(self, 2)
-
- const newMessages = []
- if (newMessage) {
- newMessages.push(
- Message.create({
- role: "assistant",
- content: "",
- open: true,
- }),
- )
- } else {
- self.lastMessage.update({ open: true })
- }
-
- const template = self.buildTemplate(newMessages)
- console.log(template)
-
- const stream = client.completion(template, self.model, self.options)
-
- let message = self.lastMessage
- if (newMessage) {
- message = Message.create({
- role: "assistant",
- content: "",
- open: true,
- })
- this.addMessage(message)
- }
-
- for await (const chunk of stream) {
- message.update({ content: message.content + chunk })
- }
-
- message.update({ open: false })
- },
- regenerate() {
- self.messages.pop()
- this.respond()
- },
- setModel(model: string) {
- self.model = model
- },
- setSystemMessage(message: string) {
- self.system_message = message
- },
- updateOptions(options: Partial) {
- Object.assign(self.options, options)
- },
- remove(thing: any) {
- destroy(thing)
- },
- }))
diff --git a/src/store/Message.tsx b/src/store/Message.ts
similarity index 57%
rename from src/store/Message.tsx
rename to src/store/Message.ts
index 48596ee..fcbcfb2 100644
--- a/src/store/Message.tsx
+++ b/src/store/Message.ts
@@ -1,9 +1,10 @@
-import { getParent, types as t } from "mobx-state-tree"
+import { getParent, getParentOfType, types as t } from "mobx-state-tree"
+import { Chat } from "./Chat"
export const Message = t
.model("Message", {
role: t.optional(t.string, "user"),
- content: t.string,
+ content: t.optional(t.string, ""),
date: t.optional(t.Date, () => new Date()),
open: t.optional(t.boolean, false),
})
@@ -11,12 +12,18 @@ export const Message = t
get isEmpty() {
return !self.content
},
+ get chat() {
+ return getParentOfType(self, Chat)
+ },
+ get agent() {
+ return getParent(self, 2)
+ },
}))
.actions((self) => ({
update(props: Partial) {
Object.assign(self, props)
},
remove() {
- getParent(self, 2).remove(self)
+ self.agent.remove(self)
},
}))
diff --git a/src/store/Model.tsx b/src/store/Model.ts
similarity index 100%
rename from src/store/Model.tsx
rename to src/store/Model.ts
diff --git a/src/store/Playground.ts b/src/store/Playground.ts
new file mode 100644
index 0000000..ebf4ca3
--- /dev/null
+++ b/src/store/Playground.ts
@@ -0,0 +1,43 @@
+import { types as t } from "mobx-state-tree"
+import { Agent } from "./Agent"
+
+export const Playground = t
+ .model("Playground", {
+ output: t.optional(t.string, ""),
+ agent: t.optional(Agent, {}),
+ })
+ .volatile((self) => ({
+ abortController: null as AbortController | null,
+ }))
+ .actions((self) => ({
+ update(props: Partial) {
+ Object.assign(self, props)
+ },
+ abort() {
+ self.abortController?.abort()
+ self.update({ abortController: null })
+ },
+ async generate() {
+ this.abort()
+
+ this.update({ output: "", abortController: new AbortController() })
+
+ console.log(`"${self.agent.promptTemplate}"`)
+ try {
+ if (!self.agent.promptTemplate) return
+ const output = self.agent.adapter.instance.completion(
+ self.agent.promptTemplate,
+ self.agent.model,
+ self.agent.overridedOptions,
+ self.abortController!.signal,
+ )
+ for await (const chunk of output) {
+ this.update({ output: self.output + chunk })
+ }
+ } catch (e) {
+ console.error(e)
+ } finally {
+ this.abort()
+ }
+ },
+ }))
diff --git a/src/store/Presets.tsx b/src/store/Presets.tsx
deleted file mode 100644
index fd1cea8..0000000
--- a/src/store/Presets.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { types as t } from "mobx-state-tree"
-
-export const Preset = t.model("Preset", {
- key: t.string,
- value: t.frozen(),
-})
-
-export const INFERENCE_PRESETS = {
- "LLaMA Precise": {
- temperature: 0.7,
- top_k: 40,
- top_p: 0.1,
- repeat_penalty: 1.18,
- },
- Shortwave: {
- temperature: 1.53,
- top_k: 33,
- top_p: 0.64,
- repeat_penalty: 1.07,
- },
- "Space Alien": {
- temperature: 1.31,
- top_k: 72,
- top_p: 0.29,
- repeat_penalty: 1.09,
- },
- "Star Chat": {
- temperature: 0.2,
- top_k: 50,
- top_p: 0.95,
- repeat_penalty: 1.0,
- },
- Yara: {
- temperature: 0.82,
- top_k: 72,
- top_p: 0.21,
- repeat_penalty: 1.19,
- },
- "Big-O": {
- temperature: 0.87,
- top_k: 85,
- top_p: 0.99,
- repeat_penalty: 1.01,
- },
-}
-
-export const SYSTEM_TEMPLATES = [
- "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.",
- "Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.",
-]
-
-export const CHAT_TEMPLATES = {
- ChatML: `{{#messages}}
-<|im_start|>{{role}}
-{{content}}{{#closed}}<|im_end|>{{/closed}}
-{{/messages}}`,
-
- "Mistral Instruct": `[INST] {{attachments}} {{prompt}} [/INST]`,
-
- Vicuna: `{{system}}
-{{#messages}}
-{{#upperCase}}{{role}}{{/upperCase}}: {{content}}
-{{/messages}}`,
-
- "Alpaca Instruct": `{{system}}
-
-### Instruction:
-{{prompt}}
-
-### Response:`,
-}
-
-export const Presets = t.model("Presets", {
- system: t.array(Preset),
- user: t.array(Preset),
- chat: t.optional(t.array(Preset), transform(CHAT_TEMPLATES)),
- inference: t.optional(t.array(Preset), transform(INFERENCE_PRESETS)),
-})
-
-function transform(what: any) {
- return Object.keys(what).map((key) => ({
- key,
- value: what[key],
- }))
-}
diff --git a/src/store/State.ts b/src/store/State.ts
new file mode 100644
index 0000000..01eddd7
--- /dev/null
+++ b/src/store/State.ts
@@ -0,0 +1,26 @@
+import { types as t, Instance, getType } from "mobx-state-tree"
+import { Chat } from "./Chat"
+import { Agent } from "./Agent"
+
+export const State = t
+ .model("State", {
+ route: t.maybeNull(t.string),
+ resource: t.maybeNull(t.reference(t.union(Chat, Agent))),
+ })
+ .actions((self) => ({
+ update(state: Partial>) {
+ Object.assign(self, state)
+ },
+ navigate(target: any) {
+ if (typeof target === "string") {
+ return this.update({ route: target, resource: null })
+ }
+
+ switch (getType(target)) {
+ case Chat:
+ return this.update({ route: "chat", resource: target })
+ case Agent:
+ return this.update({ route: "agent", resource: target })
+ }
+ },
+ }))
diff --git a/src/store/Store.ts b/src/store/Store.ts
new file mode 100644
index 0000000..d0903b0
--- /dev/null
+++ b/src/store/Store.ts
@@ -0,0 +1,45 @@
+import { types as t, Instance, destroy } from "mobx-state-tree"
+import { Chat } from "./Chat"
+import { Model } from "./Model"
+import { Agent } from "./Agent"
+import { State } from "./State"
+import { Playground } from "./Playground"
+import { DEFAULT_AGENTS } from "./defaults"
+
+export const Store = t
+ .model("Store", {
+ chats: t.array(Chat),
+ agents: t.optional(t.array(Agent), DEFAULT_AGENTS),
+ models: t.array(Model),
+ state: t.optional(State, {}),
+ playground: t.optional(Playground, {}),
+ })
+
+ .actions((self) => ({
+ addChat(chat: Instance = {} as Instance) {
+ self.chats.push(chat)
+ self.state.navigate(self.chats[self.chats.length - 1])
+ },
+ addAgent(agent: Instance = {} as Instance) {
+ self.agents.push(agent)
+ self.state.navigate(self.agents[self.agents.length - 1])
+ },
+ newChat() {
+ this.addChat({ agent: self?.agents[0] })
+ },
+ newAgent() {
+ this.addAgent()
+ },
+ removeChat(chat: Instance) {
+ if (self.state.resource == chat) {
+ self.state.resource = null
+ }
+ destroy(chat)
+ },
+ removeAgent(agent: Instance) {
+ if (self.state.resource == agent) {
+ self.state.resource = null
+ }
+ destroy(agent)
+ },
+ }))
diff --git a/src/store/Store.tsx b/src/store/Store.tsx
deleted file mode 100644
index 1c085bb..0000000
--- a/src/store/Store.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { types as t, Instance, destroy } from "mobx-state-tree"
-import { Chat } from "./Chat"
-import { OllamaClient } from "@/lib/OllamaClient"
-import { Model } from "./Model"
-import { Presets } from "./Presets"
-
-export const Store = t
- .model("Store", {
- endpoint: t.optional(t.string, "http://localhost:11434"),
- connected: t.optional(t.boolean, false),
- activeChat: t.maybeNull(t.reference(Chat)),
- chats: t.array(Chat),
- models: t.array(Model),
- presets: t.optional(Presets, {}),
- })
- .views((self) => ({
- get client() {
- return new OllamaClient(self.endpoint)
- },
- }))
-
- .actions((self) => ({
- setEndpoint(endpoint: string) {
- self.endpoint = endpoint
- },
- setConnected(connected: boolean) {
- self.connected = connected
- },
- addChat(chat: Instance = {} as Instance) {
- self.chats.push(chat)
- self.activeChat = self.chats[self.chats.length - 1]
- },
- newChat() {
- if (self.models.length === 0) {
- alert(
- "You currently have models available. Download one first using `ollama pull `.",
- )
- }
- this.addChat({ model: self?.models[0].name })
- },
- afterCreate() {
- this.refreshModels()
- },
- refreshModels() {
- self.client
- .models()
-
- .then((models) => {
- this.setModels(models)
- if (self.chats.length === 0) {
- this.newChat()
- }
- this.setConnected(true)
- })
- .catch((err) => {
- console.error(err)
- this.setConnected(false)
- })
- },
- setModels(values: typeof self.models) {
- self.models = values
- },
- removeChat(chat: Instance) {
- if (self.activeChat == chat) {
- self.activeChat = null
- }
- destroy(chat)
- },
- setActiveChat(chat: Instance) {
- self.activeChat = chat
- },
- }))
diff --git a/src/store/Template.ts b/src/store/Template.ts
new file mode 100644
index 0000000..a5850d7
--- /dev/null
+++ b/src/store/Template.ts
@@ -0,0 +1,13 @@
+import { types as t } from "mobx-state-tree";
+import { Instance } from "mobx-state-tree";
+
+export const Template = t
+ .model("Template", {
+ id: t.identifier,
+ content: t.optional(t.string, ""),
+ })
+ .actions((self) => ({
+ update(props: Partial>) {
+ Object.assign(self, props);
+ },
+ }));
diff --git a/src/store/defaults.ts b/src/store/defaults.ts
new file mode 100644
index 0000000..fe34b58
--- /dev/null
+++ b/src/store/defaults.ts
@@ -0,0 +1,37 @@
+export const DEFAULT_AGENTS = [
+ {
+ id: "HuggingFaceH4/zephyr-7b-beta",
+ name: "Zephyr 7B β",
+ adapter: {
+ baseUrl: "HuggingFaceH4/zephyr-7b-beta",
+ type: "HuggingFace",
+ },
+ parameters: {
+ temperature: 0.7,
+ top_k: 40,
+ top_p: 0.9,
+ repeat_last_n: -1,
+ repeat_penalty: 1.18,
+ num_predict: -2,
+ num_ctx: 2048,
+ stop: ["", "<|system|>", "<|user|>", "<|assistant|>"],
+ },
+ checkedOptions: [],
+ promptTemplate:
+ '{%- for message in messages -%}\n{% include "message" %}\n{% endfor -%}\n<|assistant|>\n',
+ messages: [
+ {
+ role: "system",
+ content:
+ "You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.\n\nIf a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information.",
+ },
+ ],
+ templates: [
+ {
+ id: "message",
+ content:
+ "<|{{message.role}}|>\n{{message.content}}{%if message.closed%}{%endif%}",
+ },
+ ],
+ },
+]
diff --git a/src/store/index.tsx b/src/store/index.ts
similarity index 100%
rename from src/store/index.tsx
rename to src/store/index.ts