feat(ai): add Anthropic Claude adapter as primary AI provider
Wire up Claude Messages API with SSE streaming, supporting the different event format (content_block_delta) vs OpenRouter's OpenAI-compatible format. Claude is the default when its API key is present. - Dual provider system: Anthropic (direct) and OpenRouter - Claude streams via content_block_delta events, OpenRouter via choices[0].delta.content - Model picker dropdown in chat header (click model name to switch) - Available models: Claude Sonnet 4, Opus 4, Haiku 3.5, Llama 4 Maverick/Scout (free), Mistral Small 3.1 (free) - Sidebar status shows active provider name - anthropic-dangerous-direct-browser-access header for browser use Made-with: Cursor
This commit is contained in:
@@ -7,7 +7,16 @@
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-sm font-semibold text-white/96 truncate">{{ title }}</h2>
|
||||
<p class="text-[10px] text-white/40 truncate font-mono">{{ conversationId }}</p>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="text-[10px] text-white/40 truncate font-mono">{{ conversationId }}</p>
|
||||
<span class="text-[10px] text-white/20">·</span>
|
||||
<button
|
||||
class="text-[10px] text-accent/70 hover:text-accent transition-colors truncate"
|
||||
@click="showModelPicker = !showModelPicker"
|
||||
>
|
||||
{{ modelDisplayName }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,10 +54,39 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Transition name="picker">
|
||||
<div
|
||||
v-if="showModelPicker"
|
||||
class="absolute left-0 right-0 top-full z-20 mx-3 mt-1 glass-card p-3 space-y-3 animate-fade-up-fast"
|
||||
>
|
||||
<div v-for="provider in availableProviders" :key="provider.id">
|
||||
<p class="text-[10px] font-semibold text-white/40 uppercase tracking-wider mb-1.5 px-1">
|
||||
{{ provider.name }}
|
||||
</p>
|
||||
<div class="space-y-0.5">
|
||||
<button
|
||||
v-for="model in provider.models"
|
||||
:key="model.id"
|
||||
class="w-full text-left px-3 py-2 rounded-lg text-xs transition-all duration-200"
|
||||
:class="model.id === activeModel && provider.id === activeProvider
|
||||
? 'nav-tab-active'
|
||||
: 'text-white/60 hover:text-white hover:bg-white/10'"
|
||||
@click="selectModel(provider.id, model.id)"
|
||||
>
|
||||
{{ model.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAI } from '@/composables/useAI'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
conversationId: string
|
||||
@@ -61,4 +99,35 @@ defineEmits<{
|
||||
newChat: []
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const { activeProvider, activeModel, availableProviders, setProvider, setModel } = useAI()
|
||||
const showModelPicker = ref(false)
|
||||
|
||||
const modelDisplayName = computed(() => {
|
||||
for (const p of availableProviders.value) {
|
||||
const m = p.models.find((mm) => mm.id === activeModel.value)
|
||||
if (m) return m.name
|
||||
}
|
||||
return activeModel.value
|
||||
})
|
||||
|
||||
function selectModel(providerId: string, modelId: string) {
|
||||
setProvider(providerId as 'anthropic' | 'openrouter')
|
||||
setModel(modelId)
|
||||
showModelPicker.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.picker-enter-active {
|
||||
transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.picker-leave-active {
|
||||
transition: all 0.15s ease-in;
|
||||
}
|
||||
.picker-enter-from,
|
||||
.picker-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user