feat: nuxt-claude 프로젝트 초기 커밋
Some checks failed
ci / ci (22, ubuntu-latest) (push) Failing after 25m52s
Some checks failed
ci / ci (22, ubuntu-latest) (push) Failing after 25m52s
Made-with: Cursor
This commit is contained in:
145
app/composables/useAiChat.ts
Normal file
145
app/composables/useAiChat.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { AiConversation, AiMessage } from '~/types/ai'
|
||||
|
||||
export function useAiChat() {
|
||||
const client = useSupabaseClient()
|
||||
const user = useSupabaseUser()
|
||||
|
||||
const conversations = ref<AiConversation[]>([])
|
||||
const currentConversation = ref<AiConversation | null>(null)
|
||||
const messages = ref<AiMessage[]>([])
|
||||
const streamingContent = ref('')
|
||||
const isStreaming = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchConversations() {
|
||||
if (!user.value) return
|
||||
const { data } = await client
|
||||
.from('ai_conversations')
|
||||
.select('*')
|
||||
.order('updated_at', { ascending: false })
|
||||
conversations.value = (data as AiConversation[]) ?? []
|
||||
}
|
||||
|
||||
async function createConversation(title: string = '새 대화') {
|
||||
if (!user.value) return null
|
||||
const { data, error } = await client
|
||||
.from('ai_conversations')
|
||||
.insert({ user_id: user.value.id, title })
|
||||
.select()
|
||||
.single()
|
||||
if (error) return null
|
||||
const conv = data as AiConversation
|
||||
conversations.value.unshift(conv)
|
||||
return conv
|
||||
}
|
||||
|
||||
async function selectConversation(conv: AiConversation) {
|
||||
currentConversation.value = conv
|
||||
await fetchMessages(conv.id)
|
||||
}
|
||||
|
||||
async function fetchMessages(conversationId: string) {
|
||||
const { data } = await client
|
||||
.from('ai_messages')
|
||||
.select('*')
|
||||
.eq('conversation_id', conversationId)
|
||||
.order('created_at', { ascending: true })
|
||||
messages.value = (data as AiMessage[]) ?? []
|
||||
}
|
||||
|
||||
async function deleteConversation(id: string) {
|
||||
await client.from('ai_conversations').delete().eq('id', id)
|
||||
conversations.value = conversations.value.filter(c => c.id !== id)
|
||||
if (currentConversation.value?.id === id) {
|
||||
currentConversation.value = null
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(content: string) {
|
||||
if (!currentConversation.value || isStreaming.value) return
|
||||
|
||||
// Save user message to DB
|
||||
const { data: userMsg } = await client
|
||||
.from('ai_messages')
|
||||
.insert({
|
||||
conversation_id: currentConversation.value.id,
|
||||
role: 'user',
|
||||
content
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (userMsg) messages.value.push(userMsg as AiMessage)
|
||||
|
||||
// Start streaming
|
||||
isStreaming.value = true
|
||||
streamingContent.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/ai/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
conversationId: currentConversation.value.id,
|
||||
messages: messages.value.map(m => ({ role: m.role, content: m.content }))
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('API 오류')
|
||||
|
||||
const reader = response.body!.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
streamingContent.value += decoder.decode(value, { stream: true })
|
||||
}
|
||||
|
||||
// Save assistant message to DB
|
||||
const finalContent = streamingContent.value
|
||||
const { data: assistantMsg } = await client
|
||||
.from('ai_messages')
|
||||
.insert({
|
||||
conversation_id: currentConversation.value.id,
|
||||
role: 'assistant',
|
||||
content: finalContent
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (assistantMsg) messages.value.push(assistantMsg as AiMessage)
|
||||
|
||||
// Update conversation title if it's the first message
|
||||
if (messages.value.length === 2) {
|
||||
const title = content.slice(0, 30) + (content.length > 30 ? '...' : '')
|
||||
await client
|
||||
.from('ai_conversations')
|
||||
.update({ title })
|
||||
.eq('id', currentConversation.value.id)
|
||||
const conv = conversations.value.find(c => c.id === currentConversation.value!.id)
|
||||
if (conv) conv.title = title
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Streaming error:', e)
|
||||
} finally {
|
||||
isStreaming.value = false
|
||||
streamingContent.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
conversations: readonly(conversations),
|
||||
currentConversation: readonly(currentConversation),
|
||||
messages: readonly(messages),
|
||||
streamingContent: readonly(streamingContent),
|
||||
isStreaming: readonly(isStreaming),
|
||||
loading: readonly(loading),
|
||||
fetchConversations,
|
||||
createConversation,
|
||||
selectConversation,
|
||||
deleteConversation,
|
||||
sendMessage
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user