local.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import { createStore } from "solid-js/store"
  2. import { batch, createEffect, createMemo, createSignal, onMount } from "solid-js"
  3. import { useSync } from "@tui/context/sync"
  4. import { useTheme } from "@tui/context/theme"
  5. import { uniqueBy } from "remeda"
  6. import path from "path"
  7. import { Global } from "@/global"
  8. import { iife } from "@/util/iife"
  9. import { createSimpleContext } from "./helper"
  10. import { useToast } from "../ui/toast"
  11. import { createEventBus } from "@solid-primitives/event-bus"
  12. export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
  13. name: "Local",
  14. init: (props: { initialModel?: string; initialAgent?: string; initialPrompt?: string }) => {
  15. const sync = useSync()
  16. const toast = useToast()
  17. function isModelValid(model: { providerID: string; modelID: string }) {
  18. const provider = sync.data.provider.find((x) => x.id === model.providerID)
  19. return !!provider?.models[model.modelID]
  20. }
  21. function getFirstValidModel(
  22. ...modelFns: (() => { providerID: string; modelID: string } | undefined)[]
  23. ) {
  24. for (const modelFn of modelFns) {
  25. const model = modelFn()
  26. if (!model) continue
  27. if (isModelValid(model)) return model
  28. }
  29. }
  30. // Set initial model if provided
  31. onMount(() => {
  32. batch(() => {
  33. if (props.initialAgent) {
  34. agent.set(props.initialAgent)
  35. }
  36. if (props.initialModel) {
  37. const [providerID, modelID] = props.initialModel.split("/")
  38. if (!providerID || !modelID)
  39. return toast.show({
  40. variant: "warning",
  41. message: `Invalid model format: ${props.initialModel}`,
  42. duration: 3000,
  43. })
  44. model.set({ providerID, modelID }, { recent: true })
  45. }
  46. })
  47. })
  48. // Automatically update model when agent changes
  49. createEffect(() => {
  50. const value = agent.current()
  51. if (value.model) {
  52. if (isModelValid(value.model))
  53. model.set({
  54. providerID: value.model.providerID,
  55. modelID: value.model.modelID,
  56. })
  57. else
  58. toast.show({
  59. variant: "warning",
  60. message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
  61. duration: 3000,
  62. })
  63. }
  64. })
  65. const agent = iife(() => {
  66. const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
  67. const [agentStore, setAgentStore] = createStore<{
  68. current: string
  69. }>({
  70. current: agents()[0].name,
  71. })
  72. const { theme } = useTheme()
  73. const colors = createMemo(() => [
  74. theme.secondary,
  75. theme.accent,
  76. theme.success,
  77. theme.warning,
  78. theme.primary,
  79. theme.error,
  80. ])
  81. return {
  82. list() {
  83. return agents()
  84. },
  85. current() {
  86. return agents().find((x) => x.name === agentStore.current)!
  87. },
  88. set(name: string) {
  89. if (!agents().some((x) => x.name === name))
  90. return toast.show({
  91. variant: "warning",
  92. message: `Agent not found: ${name}`,
  93. duration: 3000,
  94. })
  95. setAgentStore("current", name)
  96. },
  97. move(direction: 1 | -1) {
  98. batch(() => {
  99. let next = agents().findIndex((x) => x.name === agentStore.current) + direction
  100. if (next < 0) next = agents().length - 1
  101. if (next >= agents().length) next = 0
  102. const value = agents()[next]
  103. setAgentStore("current", value.name)
  104. })
  105. },
  106. color(name: string) {
  107. const index = agents().findIndex((x) => x.name === name)
  108. return colors()[index % colors().length]
  109. },
  110. }
  111. })
  112. const model = iife(() => {
  113. const [modelStore, setModelStore] = createStore<{
  114. ready: boolean
  115. model: Record<
  116. string,
  117. {
  118. providerID: string
  119. modelID: string
  120. }
  121. >
  122. recent: {
  123. providerID: string
  124. modelID: string
  125. }[]
  126. }>({
  127. ready: false,
  128. model: {},
  129. recent: [],
  130. })
  131. const file = Bun.file(path.join(Global.Path.state, "model.json"))
  132. file
  133. .json()
  134. .then((x) => {
  135. setModelStore("recent", x.recent)
  136. })
  137. .catch(() => {})
  138. .finally(() => {
  139. setModelStore("ready", true)
  140. })
  141. const fallbackModel = createMemo(() => {
  142. if (sync.data.config.model) {
  143. const [providerID, modelID] = sync.data.config.model.split("/")
  144. if (isModelValid({ providerID, modelID })) {
  145. return {
  146. providerID,
  147. modelID,
  148. }
  149. }
  150. }
  151. for (const item of modelStore.recent) {
  152. if (isModelValid(item)) {
  153. return item
  154. }
  155. }
  156. const provider = sync.data.provider[0]
  157. const model = Object.values(provider.models)[0]
  158. return {
  159. providerID: provider.id,
  160. modelID: model.id,
  161. }
  162. })
  163. const currentModel = createMemo(() => {
  164. const a = agent.current()
  165. return getFirstValidModel(
  166. () => modelStore.model[a.name],
  167. () => a.model,
  168. fallbackModel,
  169. )!
  170. })
  171. return {
  172. current: currentModel,
  173. get ready() {
  174. return modelStore.ready
  175. },
  176. recent() {
  177. return modelStore.recent
  178. },
  179. parsed: createMemo(() => {
  180. const value = currentModel()
  181. const provider = sync.data.provider.find((x) => x.id === value.providerID)!
  182. const model = provider.models[value.modelID]
  183. return {
  184. provider: provider.name ?? value.providerID,
  185. model: model.name ?? value.modelID,
  186. }
  187. }),
  188. cycle(direction: 1 | -1) {
  189. const current = currentModel()
  190. if (!current) return
  191. const recent = modelStore.recent
  192. const index = recent.findIndex(
  193. (x) => x.providerID === current.providerID && x.modelID === current.modelID,
  194. )
  195. if (index === -1) return
  196. let next = index + direction
  197. if (next < 0) next = recent.length - 1
  198. if (next >= recent.length) next = 0
  199. const val = recent[next]
  200. if (!val) return
  201. setModelStore("model", agent.current().name, { ...val })
  202. },
  203. set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
  204. batch(() => {
  205. if (!isModelValid(model)) {
  206. toast.show({
  207. message: `Model ${model.providerID}/${model.modelID} is not valid`,
  208. variant: "warning",
  209. duration: 3000,
  210. })
  211. return
  212. }
  213. setModelStore("model", agent.current().name, model)
  214. if (options?.recent) {
  215. const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
  216. if (uniq.length > 5) uniq.pop()
  217. setModelStore("recent", uniq)
  218. Bun.write(
  219. file,
  220. JSON.stringify({
  221. recent: modelStore.recent,
  222. }),
  223. )
  224. }
  225. })
  226. },
  227. }
  228. })
  229. const setInitialPrompt = createEventBus<string>()
  230. onMount(() => {
  231. if (props.initialPrompt)
  232. setInitialPrompt.emit(props.initialPrompt)
  233. })
  234. const result = {
  235. model,
  236. agent,
  237. get setInitialPrompt() {
  238. return setInitialPrompt
  239. },
  240. }
  241. return result
  242. },
  243. })