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:
Dorian
2026-03-02 14:30:19 +00:00
parent 88e211c87d
commit a5a32e3566
5 changed files with 305 additions and 71 deletions
@@ -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>