ai.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import { defineStore } from 'pinia'
  2. import { ref } from 'vue'
  3. import axios from 'axios'
  4. export interface AiConfig {
  5. provider: string
  6. endpoint: string
  7. model: string
  8. enabled: boolean
  9. }
  10. export const useAiStore = defineStore('ai', () => {
  11. const config = ref<AiConfig>({
  12. provider: 'ollama',
  13. endpoint: 'http://ollama:11434',
  14. model: 'llama3.2',
  15. enabled: true,
  16. })
  17. const models = ref<string[]>([])
  18. const loading = ref(false)
  19. const saving = ref(false)
  20. const modelsLoading = ref(false)
  21. const error = ref<string | null>(null)
  22. async function fetchConfig() {
  23. try {
  24. const res = await axios.get('/api/ai/config')
  25. config.value = res.data
  26. } catch (err) {
  27. console.error('AI config fetch error:', err)
  28. }
  29. }
  30. async function saveConfig(updates: Partial<AiConfig>): Promise<boolean> {
  31. saving.value = true
  32. error.value = null
  33. try {
  34. const merged = { ...config.value, ...updates }
  35. await axios.put('/api/ai/config', merged)
  36. config.value = merged
  37. return true
  38. } catch (err: any) {
  39. error.value = err.response?.data?.error || 'Failed to save config'
  40. return false
  41. } finally {
  42. saving.value = false
  43. }
  44. }
  45. async function fetchModels(overrideEndpoint?: string): Promise<boolean> {
  46. modelsLoading.value = true
  47. error.value = null
  48. try {
  49. const params = overrideEndpoint ? { endpoint: overrideEndpoint } : {}
  50. const res = await axios.get('/api/ai/models', { params })
  51. models.value = res.data.models || []
  52. return true
  53. } catch (err: any) {
  54. error.value = err.response?.data?.error || 'Could not connect to Ollama'
  55. models.value = []
  56. return false
  57. } finally {
  58. modelsLoading.value = false
  59. }
  60. }
  61. async function generate(prompt: string, system?: string, model?: string): Promise<string> {
  62. loading.value = true
  63. error.value = null
  64. try {
  65. const res = await axios.post('/api/ai/generate', { prompt, system, model })
  66. return res.data.text as string
  67. } catch (err: any) {
  68. error.value = err.response?.data?.error || 'Generation failed'
  69. throw err
  70. } finally {
  71. loading.value = false
  72. }
  73. }
  74. async function* streamGenerate(
  75. prompt: string,
  76. system?: string,
  77. model?: string,
  78. signal?: AbortSignal,
  79. ): AsyncGenerator<string> {
  80. const response = await fetch('/api/ai/stream', {
  81. method: 'POST',
  82. headers: { 'Content-Type': 'application/json' },
  83. body: JSON.stringify({ prompt, system, model }),
  84. signal,
  85. })
  86. if (!response.ok || !response.body) {
  87. throw new Error(`Stream request failed: ${response.status}`)
  88. }
  89. const reader = response.body.getReader()
  90. const decoder = new TextDecoder()
  91. let buffer = ''
  92. while (true) {
  93. const { done, value } = await reader.read()
  94. if (done) break
  95. buffer += decoder.decode(value, { stream: true })
  96. const lines = buffer.split('\n')
  97. buffer = lines.pop() ?? ''
  98. for (const line of lines) {
  99. if (!line.startsWith('data: ')) continue
  100. const payload = line.slice(6)
  101. try {
  102. const parsed = JSON.parse(payload) as { token?: string; done?: boolean; error?: string }
  103. if (parsed.error) throw new Error(parsed.error)
  104. if (parsed.token) yield parsed.token
  105. if (parsed.done) return
  106. } catch (e) {
  107. if (e instanceof SyntaxError) continue
  108. throw e
  109. }
  110. }
  111. }
  112. }
  113. return {
  114. config, models, loading, saving, modelsLoading, error,
  115. fetchConfig, saveConfig, fetchModels, generate, streamGenerate,
  116. }
  117. })