109 lines
3.1 KiB
TypeScript
109 lines
3.1 KiB
TypeScript
import { useChatStore } from '@/stores/chat'
|
|||
|
|
|
||
|
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions'
|
||
|
|
|
||
|
|
interface OpenRouterMessage {
|
||
|
|
role: 'system' | 'user' | 'assistant'
|
||
|
|
content: string
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useAI() {
|
||
|
|
const chatStore = useChatStore()
|
||
|
|
|
||
|
|
async function sendMessage(userText: string) {
|
||
|
|
const apiKey = import.meta.env.VITE_OPENROUTER_API_KEY
|
||
|
|
if (!apiKey) {
|
||
|
|
console.error('VITE_OPENROUTER_API_KEY not set in .env.local')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
let convId = chatStore.activeConversationId
|
||
|
|
if (!convId) {
|
||
|
|
convId = chatStore.createConversation()
|
||
|
|
}
|
||
|
|
|
||
|
|
chatStore.addMessage(convId, { role: 'user', content: userText })
|
||
|
|
|
||
|
|
const assistantMsg = chatStore.addMessage(convId, { role: 'assistant', content: '' })
|
||
|
|
if (!assistantMsg) return
|
||
|
|
|
||
|
|
chatStore.isStreaming = true
|
||
|
|
|
||
|
|
const history: OpenRouterMessage[] = chatStore.messages
|
||
|
|
.filter((m) => m.id !== assistantMsg.id)
|
||
|
|
.map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content }))
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(OPENROUTER_URL, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Bearer ${apiKey}`,
|
||
|
|
'HTTP-Referer': window.location.origin,
|
||
|
|
'X-Title': 'AIUI',
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
model: 'meta-llama/llama-4-maverick:free',
|
||
|
|
messages: [
|
||
|
|
{
|
||
|
|
role: 'system',
|
||
|
|
content: 'You are AIUI, a helpful AI assistant. Be concise and helpful. When discussing films, provide rich details including genre, year, director, and rating.',
|
||
|
|
},
|
||
|
|
...history,
|
||
|
|
],
|
||
|
|
stream: true,
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!res.ok) {
|
||
|
|
const err = await res.text()
|
||
|
|
chatStore.appendToLastMessage(convId, `Error: ${res.status} — ${err}`)
|
||
|
|
chatStore.isStreaming = false
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const reader = res.body?.getReader()
|
||
|
|
const decoder = new TextDecoder()
|
||
|
|
|
||
|
|
if (!reader) {
|
||
|
|
chatStore.appendToLastMessage(convId, 'Error: No response body')
|
||
|
|
chatStore.isStreaming = false
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
let buffer = ''
|
||
|
|
while (true) {
|
||
|
|
const { done, value } = await reader.read()
|
||
|
|
if (done) break
|
||
|
|
|
||
|
|
buffer += decoder.decode(value, { stream: true })
|
||
|
|
const lines = buffer.split('\n')
|
||
|
|
buffer = lines.pop() ?? ''
|
||
|
|
|
||
|
|
for (const line of lines) {
|
||
|
|
const trimmed = line.trim()
|
||
|
|
if (!trimmed || !trimmed.startsWith('data: ')) continue
|
||
|
|
const data = trimmed.slice(6)
|
||
|
|
if (data === '[DONE]') break
|
||
|
|
|
||
|
|
try {
|
||
|
|
const parsed = JSON.parse(data)
|
||
|
|
const delta = parsed.choices?.[0]?.delta?.content
|
||
|
|
if (delta) {
|
||
|
|
chatStore.appendToLastMessage(convId, delta)
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// skip malformed chunks
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
chatStore.appendToLastMessage(convId, `\n\nConnection error: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
||
|
|
} finally {
|
||
|
|
chatStore.isStreaming = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return { sendMessage }
|
||
|
|
}
|