feat: 部署初版测试
This commit is contained in:
162
extension/src/options/Strategies.vue
Normal file
162
extension/src/options/Strategies.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps<{ token: string }>()
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1'
|
||||
|
||||
const strategies = ref<Array<any>>([])
|
||||
const isLoading = ref(true)
|
||||
const isSubmitting = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
const showForm = ref(false)
|
||||
const form = ref({
|
||||
strategy_key: '',
|
||||
label: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const fetchStrategies = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users/me/strategies`, {
|
||||
headers: { 'Authorization': `Bearer ${props.token}` }
|
||||
})
|
||||
const data = await res.json()
|
||||
if (res.ok && data.code === 200) {
|
||||
strategies.value = data.data || []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createStrategy = async () => {
|
||||
isSubmitting.value = true
|
||||
errorMsg.value = ''
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users/me/strategies`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${props.token}`
|
||||
},
|
||||
body: JSON.stringify(form.value)
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok || data.code !== 200) {
|
||||
throw new Error(data.message || 'Failed to create strategy')
|
||||
}
|
||||
|
||||
// Reset form & reload
|
||||
form.value = { strategy_key: '', label: '', description: '' }
|
||||
showForm.value = false
|
||||
await fetchStrategies()
|
||||
|
||||
} catch (err: any) {
|
||||
errorMsg.value = err.message
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteStrategy = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this custom strategy?')) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users/me/strategies/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${props.token}` }
|
||||
})
|
||||
if (res.ok) {
|
||||
strategies.value = strategies.value.filter(s => s.id !== id)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.token) fetchStrategies()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
|
||||
<div v-if="isLoading" class="p-10 flex justify-center">
|
||||
<div class="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<p class="text-sm text-zinc-400">These custom prompt angles will appear dynamically in your Twitter sidebar.</p>
|
||||
<button
|
||||
v-if="!showForm"
|
||||
@click="showForm = true"
|
||||
class="px-4 py-2 bg-brand-primary/20 text-brand-primary border border-brand-primary/30 rounded-lg text-xs font-semibold hover:bg-brand-primary hover:text-white transition-colors flex items-center gap-1"
|
||||
>
|
||||
+ Create Custom Strategy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Create Form -->
|
||||
<form v-if="showForm" @submit.prevent="createStrategy" class="bg-white/5 border border-white/10 rounded-xl p-6 space-y-4 mb-8">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="text-sm font-semibold text-white">New Strategy</h3>
|
||||
<button type="button" @click="showForm = false" class="text-zinc-500 hover:text-white">✕</button>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMsg" class="p-3 bg-red-500/10 border border-red-500/20 text-red-400 text-xs rounded-lg">
|
||||
{{ errorMsg }}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[11px] font-medium text-zinc-400 uppercase tracking-widest">Internal ID</label>
|
||||
<input v-model="form.strategy_key" required class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50" placeholder="e.g. startup_pitch" />
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[11px] font-medium text-zinc-400 uppercase tracking-widest">Display Label (Sidebar)</label>
|
||||
<input v-model="form.label" required class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50" placeholder="e.g. Startup Pitch" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[11px] font-medium text-zinc-400 uppercase tracking-widest">Prompt Instructions</label>
|
||||
<textarea v-model="form.description" required rows="3" class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50 resize-none" placeholder="Instruct the AI exactly how to respond using this strategy. e.g. Be concise, act like a VC, ask challenging questions."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit" :disabled="isSubmitting" class="px-5 py-2 bg-brand-primary hover:bg-brand-primary/90 text-white rounded-lg text-sm font-medium transition-all shadow-lg flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed">
|
||||
<span v-if="isSubmitting" class="w-3 h-3 border-2 border-white/20 border-t-white rounded-full animate-spin"></span>
|
||||
{{ isSubmitting ? 'Saving...' : 'Add Strategy' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- List -->
|
||||
<div v-if="strategies.length === 0 && !showForm" class="text-center p-12 border border-dashed border-white/10 rounded-xl text-zinc-500">
|
||||
You haven't created any custom strategies yet.
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-for="s in strategies" :key="s.id" class="bg-white/5 border border-white/10 rounded-xl p-5 flex justify-between items-start group hover:border-white/20 transition-colors">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-white">{{ s.label }}</span>
|
||||
<span class="text-[10px] font-mono text-brand-primary bg-brand-primary/10 px-2 py-0.5 rounded-full">{{ s.strategy_key }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-400 leading-relaxed max-w-2xl">{{ s.description }}</p>
|
||||
</div>
|
||||
<button @click="deleteStrategy(s.id)" class="opacity-0 group-hover:opacity-100 transition-opacity text-red-500 hover:text-red-400 p-2 rounded-lg hover:bg-red-500/10">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user