Compare commits
5 Commits
extension-
...
extension-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
945157424d | ||
|
|
4b7f308528 | ||
|
|
95ee3fa61d | ||
|
|
d82d59cbe4 | ||
|
|
eb7efae32a |
@@ -42,5 +42,17 @@
|
|||||||
},
|
},
|
||||||
"description": "Toggle InsightReply Sidebar"
|
"description": "Toggle InsightReply Sidebar"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": [
|
||||||
|
"src/assets/*",
|
||||||
|
"assets/*"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"https://twitter.com/*",
|
||||||
|
"https://x.com/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,37 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import Auth from './options/Auth.vue'
|
||||||
|
import Profile from './options/Profile.vue'
|
||||||
|
import Strategies from './options/Strategies.vue'
|
||||||
|
import Competitors from './options/Competitors.vue'
|
||||||
|
import HotTweets from './options/HotTweets.vue'
|
||||||
|
|
||||||
const activeTab = ref<'settings' | 'history'>('settings')
|
const activeTab = ref<'profile' | 'strategies' | 'radar' | 'growth' | 'history'>('growth')
|
||||||
|
const token = ref('')
|
||||||
// Settings State
|
const isLoading = ref(true)
|
||||||
const identity = ref('Independent Developer / Founder')
|
|
||||||
const language = ref('auto')
|
|
||||||
const isSaving = ref(false)
|
|
||||||
|
|
||||||
// History State
|
|
||||||
const historyList = ref<Array<any>>([])
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Load settings
|
chrome.storage.local.get(['jwt_token'], (res) => {
|
||||||
chrome.storage.sync.get(['identity', 'language'], (res) => {
|
if (res.jwt_token) {
|
||||||
if (res.identity) identity.value = String(res.identity)
|
token.value = String(res.jwt_token)
|
||||||
if (res.language) language.value = String(res.language)
|
}
|
||||||
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Load history
|
const onAuthenticated = (newToken: string) => {
|
||||||
|
token.value = newToken
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
chrome.storage.local.remove(['jwt_token', 'user_id'], () => {
|
||||||
|
token.value = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// History State (Moved to separate view or handled here)
|
||||||
|
const historyList = ref<Array<any>>([])
|
||||||
|
onMounted(() => {
|
||||||
chrome.storage.local.get(['history'], (res) => {
|
chrome.storage.local.get(['history'], (res) => {
|
||||||
if (Array.isArray(res.history)) {
|
if (Array.isArray(res.history)) {
|
||||||
historyList.value = res.history
|
historyList.value = res.history
|
||||||
@@ -26,18 +39,6 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveSettings = () => {
|
|
||||||
isSaving.value = true
|
|
||||||
chrome.storage.sync.set({
|
|
||||||
identity: identity.value,
|
|
||||||
language: language.value
|
|
||||||
}, () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
isSaving.value = false
|
|
||||||
}, 600)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyToClipboard = (text: string) => {
|
const copyToClipboard = (text: string) => {
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text)
|
||||||
}
|
}
|
||||||
@@ -47,158 +48,163 @@ const clearHistory = () => {
|
|||||||
historyList.value = []
|
historyList.value = []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchDashboard = () => {
|
|
||||||
chrome.tabs.create({ url: chrome.runtime.getURL('options.html') })
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-[400px] h-[600px] glass-card flex flex-col overflow-hidden text-[#f8fafc] selection:bg-rose-500/30">
|
<div class="w-[400px] h-[600px] glass-card flex flex-col overflow-hidden text-[#f8fafc] selection:bg-rose-500/30 font-sans">
|
||||||
|
|
||||||
|
<!-- Background Decor -->
|
||||||
|
<div class="fixed top-0 left-0 w-full h-full pointer-events-none overflow-hidden -z-10">
|
||||||
|
<div class="absolute -top-[10%] -left-[10%] w-[50%] h-[50%] bg-rose-900/10 blur-[100px] rounded-full"></div>
|
||||||
|
<div class="absolute top-[40%] -right-[5%] w-[40%] h-[60%] bg-blue-900/10 blur-[100px] rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Premium Header -->
|
<!-- Premium Header -->
|
||||||
<div class="px-6 py-5 flex justify-between items-center border-b border-white/5 bg-white/[0.02]">
|
<div class="px-6 py-4 flex justify-between items-center border-b border-white/5 bg-white/[0.02] backdrop-blur-md sticky top-0 z-50">
|
||||||
<div>
|
<div class="flex items-center gap-3">
|
||||||
<h1 class="text-xl font-bold tracking-tight bg-gradient-to-br from-rose-400 via-violet-400 to-blue-400 bg-clip-text text-transparent opacity-90">
|
<div class="p-2 rounded-lg bg-gradient-to-br from-rose-500 to-rose-600 shadow-lg shadow-rose-900/20">
|
||||||
InsightReply
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" class="text-white"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>
|
||||||
</h1>
|
</div>
|
||||||
<div class="flex items-center gap-2 mt-0.5">
|
<div>
|
||||||
<span class="text-[10px] text-zinc-500 uppercase tracking-[0.2em] font-bold">Social Copilot</span>
|
<h1 class="text-sm font-black tracking-tight bg-gradient-to-br from-white to-zinc-400 bg-clip-text text-transparent">
|
||||||
<span class="h-1 w-1 rounded-full bg-zinc-700"></span>
|
InsightReply
|
||||||
<span class="text-[10px] text-zinc-500 font-medium">Pro Max</span>
|
</h1>
|
||||||
|
<div class="flex items-center gap-1.5 mt-[-2px]">
|
||||||
|
<div class="w-1 h-1 rounded-full bg-emerald-500 animate-pulse" v-if="token"></div>
|
||||||
|
<span class="text-[8px] text-zinc-500 uppercase tracking-[0.2em] font-black">{{ token ? 'Pro Max Online' : 'Authentication Required' }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Launch Dashboard Button -->
|
<button v-if="token" @click="logout" class="text-[10px] font-bold text-zinc-500 hover:text-rose-400 transition-apple uppercase tracking-widest">Logout</button>
|
||||||
<button
|
|
||||||
@click="launchDashboard"
|
|
||||||
class="p-2 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 transition-apple group"
|
|
||||||
title="Open Dashboard"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-zinc-400 group-hover:text-rose-400 transition-apple"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Segmented Control (Tabs) -->
|
<!-- Content Area -->
|
||||||
<div class="px-6 pt-5">
|
<div class="flex-1 overflow-y-auto custom-scrollbar relative">
|
||||||
<div class="flex p-1 bg-black/40 rounded-xl border border-white/5 backdrop-blur-md">
|
<div v-if="isLoading" class="flex items-center justify-center h-full">
|
||||||
<button
|
<div class="w-8 h-8 border-2 border-white/10 border-t-rose-500 rounded-full animate-spin"></div>
|
||||||
@click="activeTab = 'settings'"
|
|
||||||
:class="['flex-1 py-1.5 text-xs font-semibold rounded-lg transition-apple', activeTab === 'settings' ? 'bg-zinc-800 text-white shadow-[0_2px_8px_rgba(0,0,0,0.4)]' : 'text-zinc-500 hover:text-zinc-300']"
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="activeTab = 'history'"
|
|
||||||
:class="['flex-1 py-1.5 text-xs font-semibold rounded-lg transition-apple', activeTab === 'history' ? 'bg-zinc-800 text-white shadow-[0_2px_8px_rgba(0,0,0,0.4)]' : 'text-zinc-500 hover:text-zinc-300']"
|
|
||||||
>
|
|
||||||
History
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<div v-else-if="!token" class="p-8 h-full flex flex-col justify-center animate-in fade-in zoom-in-95 duration-500">
|
||||||
<div class="flex-1 overflow-y-auto px-6 py-5 custom-scrollbar">
|
<div class="text-center space-y-3 mb-8">
|
||||||
|
<h2 class="text-xl font-black tracking-tight">Welcome Back</h2>
|
||||||
<!-- Settings Tab -->
|
<p class="text-xs text-zinc-500 font-medium px-4">Synchronize your AI personality and custom strategies across all sessions.</p>
|
||||||
<div v-if="activeTab === 'settings'" class="animate-in fade-in slide-in-from-bottom-2 duration-500 space-y-6">
|
</div>
|
||||||
<div class="space-y-4">
|
<Auth @authenticated="onAuthenticated" />
|
||||||
<div class="bg-white/[0.03] border border-white/5 rounded-2xl p-4 space-y-4">
|
</div>
|
||||||
<h2 class="text-xs font-bold text-zinc-500 uppercase tracking-widest flex items-center gap-2">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="text-rose-500"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
<div v-else class="h-full">
|
||||||
Identity Profile
|
<div v-if="activeTab === 'growth'" class="p-6 pb-24 animate-in fade-in slide-in-from-right-4 duration-500">
|
||||||
</h2>
|
<HotTweets :token="token" />
|
||||||
|
</div>
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2">
|
<div v-if="activeTab === 'profile'" class="p-6 pb-24 animate-in fade-in slide-in-from-right-4 duration-500">
|
||||||
<label class="text-[11px] font-semibold text-zinc-400 ml-1">Identity Label</label>
|
<Profile :token="token" />
|
||||||
<input
|
</div>
|
||||||
v-model="identity"
|
|
||||||
type="text"
|
<div v-if="activeTab === 'strategies'" class="p-6 pb-24 animate-in fade-in slide-in-from-right-4 duration-500">
|
||||||
class="w-full bg-black/40 border border-white/5 rounded-xl px-4 py-3 text-sm text-white focus:outline-none focus:ring-2 focus:ring-rose-500/20 focus:border-rose-500/40 transition-apple placeholder:text-zinc-600"
|
<Strategies :token="token" />
|
||||||
placeholder="e.g. AI Founder, Indie Hacker"
|
</div>
|
||||||
/>
|
|
||||||
|
<div v-if="activeTab === 'radar'" class="p-6 pb-24 animate-in fade-in slide-in-from-right-4 duration-500">
|
||||||
|
<Competitors :token="token" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'history'" class="p-6 pb-24 animate-in fade-in slide-in-from-right-4 duration-500 space-y-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xs font-bold text-zinc-500 uppercase tracking-[0.2em]">Activity Log</h2>
|
||||||
|
<button @click="clearHistory" class="text-[10px] font-bold text-rose-500/80 hover:text-rose-400 transition-apple uppercase tracking-widest">Clear</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="historyList.length === 0" class="flex flex-col items-center justify-center py-20 text-zinc-600 space-y-4">
|
||||||
|
<div class="p-5 rounded-full bg-white/[0.02] border border-white/5">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="opacity-30"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-xs font-bold">No activity detected</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div v-else class="space-y-6">
|
||||||
<label class="text-[11px] font-semibold text-zinc-400 ml-1">Reply Language</label>
|
<div v-for="(item, idx) in historyList" :key="idx" class="bg-white/[0.02] border border-white/5 rounded-2xl p-5 space-y-4">
|
||||||
<div class="relative">
|
<div class="flex justify-between items-center">
|
||||||
<select
|
<div class="text-[9px] font-mono text-zinc-600 tracking-tighter">{{ new Date(item.timestamp).toLocaleString() }}</div>
|
||||||
v-model="language"
|
<div class="h-1 w-1 rounded-full bg-rose-500/40"></div>
|
||||||
class="w-full bg-black/40 border border-white/5 rounded-xl px-4 py-3 text-sm text-white focus:outline-none focus:ring-2 focus:ring-rose-500/20 focus:border-rose-500/40 transition-apple appearance-none cursor-pointer"
|
</div>
|
||||||
>
|
<div class="text-[11px] leading-relaxed text-zinc-400 italic bg-black/20 rounded-xl p-3 border border-white/[0.02]">
|
||||||
<option value="auto">Auto (Match Tweet)</option>
|
"{{ item.tweetContent?.substring(0, 80) }}..."
|
||||||
<option value="en">English (en)</option>
|
</div>
|
||||||
<option value="zh">Chinese (zh)</option>
|
<div class="space-y-3">
|
||||||
</select>
|
<div v-for="(reply, rIdx) in item.replies" :key="rIdx" class="bg-white/[0.03] rounded-xl p-4 group relative border border-white/5 hover:border-rose-500/20 transition-apple">
|
||||||
<div class="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-500">
|
<div class="flex justify-between items-center mb-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
<span class="text-[8px] font-bold text-rose-400 bg-rose-400/10 px-2 py-0.5 rounded-full uppercase tracking-widest">{{ reply.strategy }}</span>
|
||||||
|
<button @click="copyToClipboard(reply.content)" class="opacity-0 group-hover:opacity-100 transition-apple text-[9px] text-zinc-400 hover:text-white flex items-center gap-1.5">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-zinc-300 leading-relaxed font-medium">{{ reply.content }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="saveSettings"
|
|
||||||
:disabled="isSaving"
|
|
||||||
class="w-full py-3.5 bg-gradient-to-r from-rose-600 to-rose-500 hover:from-rose-500 hover:to-rose-400 text-white rounded-2xl text-sm font-bold transition-apple shadow-[0_8px_20px_rgba(136,19,55,0.3)] flex items-center justify-center gap-2 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<span v-if="isSaving" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></span>
|
|
||||||
{{ isSaving ? 'Saving...' : 'Save Preferences' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- History Tab -->
|
|
||||||
<div v-if="activeTab === 'history'" class="animate-in fade-in slide-in-from-bottom-2 duration-500 flex flex-col h-full">
|
|
||||||
<div class="flex justify-between items-center mb-6 px-1">
|
|
||||||
<h2 class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em]">Activity Log</h2>
|
|
||||||
<button @click="clearHistory" class="text-[10px] font-bold text-rose-500/80 hover:text-rose-400 transition-apple uppercase tracking-widest">Clear All</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="historyList.length === 0" class="flex-1 flex flex-col items-center justify-center text-zinc-500 space-y-4 pb-12">
|
|
||||||
<div class="p-5 rounded-full bg-white/[0.02] border border-white/5">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="opacity-20"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs font-medium tracking-wide">No generations found</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="space-y-6">
|
|
||||||
<div v-for="(item, idx) in historyList" :key="idx" class="bg-white/[0.02] border border-white/5 rounded-2xl p-5 space-y-4 hover:border-white/10 transition-apple">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<div class="text-[10px] font-mono text-zinc-600 tracking-tighter">{{ new Date(item.timestamp).toLocaleString() }}</div>
|
|
||||||
<div class="h-1.5 w-1.5 rounded-full bg-rose-500/40"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-[11px] leading-relaxed text-zinc-400 italic bg-black/20 rounded-xl p-3 border border-white/[0.02]">
|
|
||||||
"{{ item.tweetContent?.substring(0, 80) }}{{ item.tweetContent?.length > 80 ? '...' : '' }}"
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div v-for="(reply, rIdx) in item.replies" :key="rIdx" class="bg-white/[0.03] rounded-xl p-4 group relative border border-white/5 hover:border-rose-500/20 transition-apple">
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<span class="text-[9px] font-bold text-rose-400 bg-rose-400/10 px-2 py-0.5 rounded-full uppercase tracking-widest">{{ reply.strategy }}</span>
|
|
||||||
<button @click="copyToClipboard(reply.content)" class="opacity-0 group-hover:opacity-100 transition-apple text-[10px] text-zinc-400 hover:text-white flex items-center gap-1.5">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-zinc-300 leading-relaxed font-medium">{{ reply.content }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Apple Style Bottom Tab Bar -->
|
||||||
|
<nav v-if="token" class="h-20 bg-black/40 backdrop-blur-3xl border-t border-white/5 flex items-center justify-around px-4 pb-4 shrink-0 z-[100]">
|
||||||
|
<button @click="activeTab = 'growth'" :class="['tab-item', activeTab === 'growth' ? 'text-orange-400 drop-shadow-[0_0_10px_rgba(251,146,60,0.4)]' : 'text-zinc-600']">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5Z"/></svg>
|
||||||
|
<span>Growth</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="activeTab = 'radar'" :class="['tab-item', activeTab === 'radar' ? 'text-blue-400 drop-shadow-[0_0_10px_rgba(59,130,246,0.4)]' : 'text-zinc-600']">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M2 12h2"/><path d="M20 12h2"/></svg>
|
||||||
|
<span>Radar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="activeTab = 'strategies'" :class="['tab-item', activeTab === 'strategies' ? 'text-rose-400 drop-shadow-[0_0_10px_rgba(244,63,94,0.4)]' : 'text-zinc-600']">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m16 4 4 4-4 4"/><path d="M20 8H4v12"/></svg>
|
||||||
|
<span>AI logic</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="activeTab = 'profile'" :class="['tab-item', activeTab === 'profile' ? 'text-rose-400 drop-shadow-[0_0_10px_rgba(244,63,94,0.4)]' : 'text-zinc-600']">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
<span>DNA</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="activeTab = 'history'" :class="['tab-item', activeTab === 'history' ? 'text-rose-400 drop-shadow-[0_0_10px_rgba(244,63,94,0.4)]' : 'text-zinc-600']">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||||
|
<span>Logs</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
.tab-item {
|
||||||
width: 400px;
|
display: flex;
|
||||||
height: 600px;
|
flex-direction: column;
|
||||||
|
items-center: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
transition-property: all;
|
||||||
|
transition-duration: 400ms;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.tab-item:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
.tab-item span {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
transition-property: all;
|
||||||
|
transition-duration: 400ms;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar {
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, inject, computed, type Ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
// Injected reactive refs from sidebar-mount.ts
|
||||||
tweetData?: {
|
const currentTweetData = inject<Ref<any>>('currentTweetData', ref(null))
|
||||||
id: string;
|
const isVisible = inject<Ref<boolean>>('sidebarVisible', ref(false))
|
||||||
author: string;
|
|
||||||
content: string;
|
// Computed getter for template convenience
|
||||||
stats: {
|
const tweetData = computed(() => currentTweetData.value)
|
||||||
replies: string;
|
|
||||||
retweets: string;
|
|
||||||
likes: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const isVisible = ref(false) // Start hidden, wait for trigger
|
|
||||||
const selectedStrategy = ref('Insightful')
|
const selectedStrategy = ref('Insightful')
|
||||||
const generatedReplies = ref<Array<{strategy: string, content: string}>>([])
|
const generatedReplies = ref<Array<{strategy: string, content: string}>>([])
|
||||||
const isGenerating = ref(false)
|
const isGenerating = ref(false)
|
||||||
@@ -29,30 +22,21 @@ const defaultStrategies = [
|
|||||||
|
|
||||||
const strategies = ref([...defaultStrategies])
|
const strategies = ref([...defaultStrategies])
|
||||||
|
|
||||||
onMounted(() => {
|
// Fetch custom strategies on mount
|
||||||
// Listen for toggle messages directly in the component
|
chrome.runtime.sendMessage({ type: 'FETCH_CUSTOM_STRATEGIES' }, (response) => {
|
||||||
chrome.runtime.onMessage.addListener((message) => {
|
if (response && response.success && response.data) {
|
||||||
if (message.type === 'TOGGLE_SIDEBAR') {
|
const customStrategies = response.data.map((s: any) => ({
|
||||||
isVisible.value = !isVisible.value
|
id: s.strategy_key,
|
||||||
} else if (message.type === 'SHOW_INSIGHT') {
|
label: s.label,
|
||||||
isVisible.value = true
|
icon: '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>'
|
||||||
}
|
}))
|
||||||
})
|
strategies.value = [...defaultStrategies, ...customStrategies]
|
||||||
|
}
|
||||||
chrome.runtime.sendMessage({ type: 'FETCH_CUSTOM_STRATEGIES' }, (response) => {
|
|
||||||
if (response && response.success && response.data) {
|
|
||||||
const customStrategies = response.data.map((s: any) => ({
|
|
||||||
id: s.strategy_key,
|
|
||||||
label: s.label,
|
|
||||||
icon: '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>'
|
|
||||||
}))
|
|
||||||
strategies.value = [...defaultStrategies, ...customStrategies]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const generate = () => {
|
const generate = () => {
|
||||||
if (!props.tweetData) return
|
if (!tweetData.value) return
|
||||||
|
|
||||||
isGenerating.value = true
|
isGenerating.value = true
|
||||||
generatedReplies.value = []
|
generatedReplies.value = []
|
||||||
@@ -68,7 +52,7 @@ const generate = () => {
|
|||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
type: 'GENERATE_REPLY',
|
type: 'GENERATE_REPLY',
|
||||||
payload: {
|
payload: {
|
||||||
tweetContent: props.tweetData!.content,
|
tweetContent: tweetData.value!.content,
|
||||||
strategy: selectedStrategy.value,
|
strategy: selectedStrategy.value,
|
||||||
identity: finalIdentity
|
identity: finalIdentity
|
||||||
}
|
}
|
||||||
@@ -93,7 +77,7 @@ const copyToClipboard = async (reply: any) => {
|
|||||||
showProfileTip.value = true
|
showProfileTip.value = true
|
||||||
setTimeout(() => { showProfileTip.value = false }, 7000)
|
setTimeout(() => { showProfileTip.value = false }, 7000)
|
||||||
|
|
||||||
if (!props.tweetData || !props.tweetData.id) return;
|
if (!tweetData.value || !tweetData.value.id) return;
|
||||||
|
|
||||||
chrome.storage.local.get(['jwt_token'], async (result) => {
|
chrome.storage.local.get(['jwt_token'], async (result) => {
|
||||||
if (result.jwt_token) {
|
if (result.jwt_token) {
|
||||||
@@ -107,7 +91,7 @@ const copyToClipboard = async (reply: any) => {
|
|||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
tweet_id: props.tweetData?.id,
|
tweet_id: tweetData.value?.id,
|
||||||
strategy_type: reply.strategy || 'General',
|
strategy_type: reply.strategy || 'General',
|
||||||
content: reply.content,
|
content: reply.content,
|
||||||
language: 'en'
|
language: 'en'
|
||||||
@@ -122,131 +106,96 @@ const copyToClipboard = async (reply: any) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<transition
|
<div v-if="isVisible" class="ir-panel">
|
||||||
enter-active-class="transition duration-500 ease-out transform"
|
|
||||||
enter-from-class="translate-x-full opacity-0 scale-95"
|
<!-- Header -->
|
||||||
enter-to-class="translate-x-0 opacity-100 scale-100"
|
<div class="ir-header">
|
||||||
leave-active-class="transition duration-400 ease-in transform"
|
<div class="ir-brand">
|
||||||
leave-from-class="translate-x-0 opacity-100 scale-100"
|
<div class="ir-logo">
|
||||||
leave-to-class="translate-x-full opacity-0 scale-95"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>
|
||||||
>
|
</div>
|
||||||
<div v-if="isVisible" class="fixed right-6 top-1/2 -translate-y-1/2 w-[380px] max-h-[90vh] flex flex-col glass-card rounded-[24px] shadow-[0_32px_64px_rgba(0,0,0,0.5)] text-[#f8fafc] font-sans z-[2147483647] overflow-hidden border border-white/5 selection:bg-rose-500/30">
|
<div>
|
||||||
|
<div class="ir-brand-name">InsightReply</div>
|
||||||
<!-- Premium Header -->
|
<div class="ir-brand-sub">Spatial Copilot</div>
|
||||||
<div class="px-6 py-5 flex justify-between items-center bg-white/[0.02] border-b border-white/5">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="p-2 rounded-lg bg-rose-500/10 border border-rose-500/20">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="text-rose-500"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-sm font-bold tracking-tight block">InsightReply</span>
|
|
||||||
<span class="text-[9px] text-zinc-500 uppercase tracking-[0.2em] font-bold">Spatial Copilot</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button @click="isVisible = false" class="p-2 rounded-full hover:bg-white/10 transition-apple group">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-zinc-500 group-hover:text-white transition-apple"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="ir-close-btn" @click="isVisible = false">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="p-6 flex-1 overflow-y-auto space-y-8 custom-scrollbar">
|
<div class="ir-content">
|
||||||
|
|
||||||
<!-- Context Module -->
|
<!-- Tweet Context -->
|
||||||
<div v-if="tweetData" class="space-y-3">
|
<div v-if="tweetData">
|
||||||
<h3 class="text-[10px] font-bold text-zinc-500 uppercase tracking-widest flex items-center gap-2 px-1">
|
<div class="ir-label">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" class="text-rose-500"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||||
Current Tweet
|
Current Tweet
|
||||||
</h3>
|
</div>
|
||||||
<div class="bg-black/20 rounded-2xl p-4 border border-white/[0.03] relative group">
|
<div class="ir-tweet-card">
|
||||||
<div class="text-[13px] text-zinc-400 leading-relaxed font-medium italic">
|
<div class="ir-tweet-text">"{{ tweetData.content }}"</div>
|
||||||
"{{ tweetData.content }}"
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Strategy Grid -->
|
<!-- Strategy Grid -->
|
||||||
<div class="space-y-4">
|
<div>
|
||||||
<h3 class="text-[10px] font-bold text-zinc-500 uppercase tracking-widest flex items-center gap-2 px-1">
|
<div class="ir-label">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" class="text-rose-500"><path d="m16 4 4 4-4 4"/><path d="M20 8H4v12"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="m16 4 4 4-4 4"/><path d="M20 8H4v12"/></svg>
|
||||||
Generation Strategy
|
Generation Strategy
|
||||||
</h3>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-2.5">
|
<div class="ir-strategy-grid">
|
||||||
<button
|
<button
|
||||||
v-for="s in strategies"
|
v-for="s in strategies"
|
||||||
:key="s.id"
|
:key="s.id"
|
||||||
@click="selectedStrategy = s.id"
|
@click="selectedStrategy = s.id"
|
||||||
:class="[
|
:class="['ir-strategy-btn', selectedStrategy === s.id ? 'active' : '']"
|
||||||
'flex items-center gap-4 p-4 rounded-2xl border transition-apple group text-sm font-semibold active:scale-[0.98]',
|
|
||||||
selectedStrategy === s.id
|
|
||||||
? 'bg-rose-500/10 border-rose-500/30 text-rose-50 text-white shadow-[0_8px_16px_rgba(136,19,55,0.15)]'
|
|
||||||
: 'bg-white/[0.02] border-white/5 hover:bg-white/[0.05] text-zinc-400'
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<div :class="['p-2 rounded-lg transition-apple', selectedStrategy === s.id ? 'bg-rose-500/20 text-rose-400' : 'bg-black/20 text-zinc-500 group-hover:text-zinc-300']">
|
<div class="ir-strategy-icon">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" v-html="s.icon"></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" v-html="s.icon"></svg>
|
||||||
</div>
|
</div>
|
||||||
<span class="flex-1 text-left tracking-tight">{{ s.label }}</span>
|
<span>{{ s.label }}</span>
|
||||||
<div v-if="selectedStrategy === s.id" class="w-2 h-2 rounded-full bg-rose-500 shadow-[0_0_8px_#f43f5e]"></div>
|
<div v-if="selectedStrategy === s.id" class="ir-strategy-dot"></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result Module -->
|
<!-- Reply Results -->
|
||||||
<div v-if="generatedReplies.length > 0" class="space-y-4 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
<div v-if="generatedReplies.length > 0" class="ir-fade-in">
|
||||||
<h3 class="text-[10px] font-bold text-zinc-500 uppercase tracking-widest px-1">AI Suggestions</h3>
|
<div class="ir-label">AI Suggestions</div>
|
||||||
|
|
||||||
<div v-for="(reply, idx) in generatedReplies" :key="idx" class="bg-white/[0.03] rounded-2xl p-5 border border-white/5 space-y-4 relative group hover:border-rose-500/20 transition-apple">
|
<div v-for="(reply, idx) in generatedReplies" :key="idx" class="ir-reply-card" style="margin-bottom: 12px;">
|
||||||
<div class="flex justify-between items-center">
|
<div class="ir-reply-header">
|
||||||
<span class="text-[9px] font-bold text-rose-400 bg-rose-400/10 px-2.5 py-1 rounded-full uppercase tracking-widest">
|
<span class="ir-reply-tag">{{ reply.strategy || 'Insight' }}</span>
|
||||||
{{ reply.strategy || 'Insight' }}
|
<button class="ir-copy-btn" @click="copyToClipboard(reply)">
|
||||||
</span>
|
|
||||||
<button @click="copyToClipboard(reply)" class="opacity-0 group-hover:opacity-100 transition-apple text-[10px] font-bold text-zinc-500 hover:text-white flex items-center gap-2 px-2 py-1 rounded-lg hover:bg-white/5">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||||
COPY
|
COPY
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[13px] leading-relaxed whitespace-pre-wrap text-zinc-200 font-medium tracking-tight">
|
<div class="ir-reply-content">{{ reply.content }}</div>
|
||||||
{{ reply.content }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sticky Action Footer -->
|
<!-- Footer -->
|
||||||
<div class="p-6 bg-white/[0.02] border-t border-white/5 space-y-4 shadow-[0_-12px_24px_rgba(0,0,0,0.2)]">
|
<div class="ir-footer">
|
||||||
<transition enter-active-class="transition duration-500 transform ease-out" enter-from-class="opacity-0 translate-y-4" enter-to-class="opacity-100 translate-y-0" leave-active-class="transition duration-300 transform ease-in" leave-from-class="opacity-100 translate-y-0" leave-to-class="opacity-0 translate-y-4">
|
<div v-if="showProfileTip" class="ir-tip">
|
||||||
<div v-if="showProfileTip" class="bg-rose-500/10 border border-rose-500/20 text-rose-400 text-[11px] p-4 rounded-2xl flex gap-3 items-start relative font-medium leading-relaxed">
|
<span>✨</span>
|
||||||
<span class="text-base leading-none">✨</span>
|
<p><strong>Optimize Conversion:</strong> Make sure your X Bio prominently features your product link!</p>
|
||||||
<p><strong>Optimize Conversion:</strong> Make sure your X Bio prominently features your product link!</p>
|
<button class="ir-tip-close" @click="showProfileTip = false">✕</button>
|
||||||
<button @click="showProfileTip = false" class="absolute top-3 right-3 opacity-50 hover:opacity-100">✕</button>
|
</div>
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="generate"
|
@click="generate"
|
||||||
:disabled="isGenerating"
|
:disabled="isGenerating"
|
||||||
class="w-full py-4 bg-gradient-to-br from-rose-600 to-rose-500 hover:from-rose-500 hover:to-rose-400 disabled:from-zinc-900 disabled:to-zinc-900 disabled:text-zinc-600 disabled:cursor-not-allowed text-white rounded-[20px] text-[15px] font-bold transition-all shadow-[0_12px_24px_rgba(136,19,55,0.25)] flex items-center justify-center gap-3 active:scale-[0.97]"
|
class="ir-generate-btn"
|
||||||
>
|
>
|
||||||
<div v-if="isGenerating" class="w-5 h-5 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
<div v-if="isGenerating" class="ir-spinner"></div>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="text-rose-100"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
|
||||||
{{ isGenerating ? 'AI Thinking...' : 'Generate Pro Reply' }}
|
{{ isGenerating ? 'AI Thinking...' : 'Generate Pro Reply' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.custom-scrollbar::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,15 +1,423 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp, ref } from 'vue'
|
||||||
import Sidebar from './Sidebar.vue'
|
import Sidebar from './Sidebar.vue'
|
||||||
import '../assets/tailwind.css' // We might need to handle this specially for Shadow DOM
|
|
||||||
|
|
||||||
const MOUNT_ID = 'insight-reply-sidebar-root'
|
const MOUNT_ID = 'insight-reply-sidebar-root'
|
||||||
|
|
||||||
function initSidebar(tweetData?: any) {
|
// Shared reactive tweet data that the Sidebar component will receive
|
||||||
|
const currentTweetData = ref<any>(null)
|
||||||
|
const sidebarVisible = ref(false)
|
||||||
|
|
||||||
|
let isMounted = false
|
||||||
|
let isMounting = false
|
||||||
|
|
||||||
|
// All CSS is embedded here so it works inside Shadow DOM
|
||||||
|
// No dependency on Tailwind or external stylesheets
|
||||||
|
const SIDEBAR_CSS = `
|
||||||
|
/* ===== Reset & Base ===== */
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Sidebar Panel ===== */
|
||||||
|
.ir-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #09090b;
|
||||||
|
border-left: 1px solid rgba(255,255,255,0.08);
|
||||||
|
box-shadow: -12px 0 40px rgba(0,0,0,0.6);
|
||||||
|
color: #f8fafc;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
|
||||||
|
z-index: 2147483647;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: ir-slide-in 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-panel.ir-closing {
|
||||||
|
animation: ir-slide-out 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ir-slide-in {
|
||||||
|
from { transform: translateX(100%); }
|
||||||
|
to { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
@keyframes ir-slide-out {
|
||||||
|
from { transform: translateX(0); }
|
||||||
|
to { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Header ===== */
|
||||||
|
.ir-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, #e11d48, #be123c);
|
||||||
|
box-shadow: 0 4px 12px rgba(225,29,72,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-logo svg {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-brand-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-brand-sub {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #71717a;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-close-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(255,255,255,0.06);
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #a1a1aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-close-btn:hover {
|
||||||
|
background: rgba(225,29,72,0.15);
|
||||||
|
color: #fb7185;
|
||||||
|
border-color: rgba(225,29,72,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Scrollable Content ===== */
|
||||||
|
.ir-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-content::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.ir-content::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.ir-content:hover::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255,255,255,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Section Labels ===== */
|
||||||
|
.ir-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #71717a;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
padding: 0 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-label svg {
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Tweet Context ===== */
|
||||||
|
.ir-tweet-card {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-tweet-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #a1a1aa;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Strategy Buttons ===== */
|
||||||
|
.ir-strategy-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-strategy-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.06);
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #a1a1aa;
|
||||||
|
font-family: inherit;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-strategy-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-strategy-btn.active {
|
||||||
|
background: rgba(225,29,72,0.08);
|
||||||
|
border-color: rgba(225,29,72,0.25);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-strategy-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
color: #71717a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-strategy-btn.active .ir-strategy-icon {
|
||||||
|
background: rgba(225,29,72,0.15);
|
||||||
|
color: #fb7185;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-strategy-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e11d48;
|
||||||
|
box-shadow: 0 0 8px #f43f5e;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Reply Results ===== */
|
||||||
|
.ir-reply-card {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-reply-card:hover {
|
||||||
|
border-color: rgba(225,29,72,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-reply-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-reply-tag {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fb7185;
|
||||||
|
background: rgba(251,113,133,0.1);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 100px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-copy-btn {
|
||||||
|
opacity: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #71717a;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-reply-card:hover .ir-copy-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-copy-btn:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-reply-content {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: #d4d4d8;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Footer ===== */
|
||||||
|
.ir-footer {
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.06);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-tip {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
background: rgba(225,29,72,0.06);
|
||||||
|
border: 1px solid rgba(225,29,72,0.15);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #fb7185;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-tip-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fb7185;
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-tip-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-generate-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 18px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #e11d48, #be123c);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 8px 20px rgba(225,29,72,0.25);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-generate-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #f43f5e, #e11d48);
|
||||||
|
box-shadow: 0 12px 28px rgba(225,29,72,0.35);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-generate-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-generate-btn:disabled {
|
||||||
|
background: #18181b;
|
||||||
|
color: #52525b;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir-spinner {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid rgba(255,255,255,0.2);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: ir-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ir-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Fade-In for Results ===== */
|
||||||
|
.ir-fade-in {
|
||||||
|
animation: ir-fadein 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ir-fadein {
|
||||||
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
async function initSidebar() {
|
||||||
|
if (isMounted || isMounting) return
|
||||||
|
|
||||||
let host = document.getElementById(MOUNT_ID)
|
let host = document.getElementById(MOUNT_ID)
|
||||||
|
if (host) return
|
||||||
|
|
||||||
if (host) return;
|
isMounting = true
|
||||||
|
|
||||||
// 1. Create Host Element
|
// 1. Create Host Element - positioned for right side only
|
||||||
host = document.createElement('div')
|
host = document.createElement('div')
|
||||||
host.id = MOUNT_ID
|
host.id = MOUNT_ID
|
||||||
host.style.cssText = `
|
host.style.cssText = `
|
||||||
@@ -17,7 +425,7 @@ function initSidebar(tweetData?: any) {
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
width: 400px;
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`
|
`
|
||||||
@@ -26,7 +434,12 @@ function initSidebar(tweetData?: any) {
|
|||||||
// 2. Create Shadow Root
|
// 2. Create Shadow Root
|
||||||
const shadowRoot = host.attachShadow({ mode: 'open' })
|
const shadowRoot = host.attachShadow({ mode: 'open' })
|
||||||
|
|
||||||
// 3. Create Container
|
// 3. Inject self-contained CSS directly (no external files needed)
|
||||||
|
const styleEl = document.createElement('style')
|
||||||
|
styleEl.textContent = SIDEBAR_CSS
|
||||||
|
shadowRoot.appendChild(styleEl)
|
||||||
|
|
||||||
|
// 4. Create Container
|
||||||
const container = document.createElement('div')
|
const container = document.createElement('div')
|
||||||
container.id = 'app'
|
container.id = 'app'
|
||||||
container.style.cssText = `
|
container.style.cssText = `
|
||||||
@@ -35,30 +448,47 @@ function initSidebar(tweetData?: any) {
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
width: 100%;
|
||||||
`
|
`
|
||||||
shadowRoot.appendChild(container)
|
shadowRoot.appendChild(container)
|
||||||
|
|
||||||
// 4. Inject Styles
|
// 5. Create Vue App with reactive provide
|
||||||
const injectStyles = () => {
|
const app = createApp(Sidebar)
|
||||||
const styles = document.querySelectorAll('style, link[rel="stylesheet"]')
|
app.provide('currentTweetData', currentTweetData)
|
||||||
styles.forEach(style => {
|
app.provide('sidebarVisible', sidebarVisible)
|
||||||
shadowRoot.appendChild(style.cloneNode(true))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
injectStyles()
|
|
||||||
|
|
||||||
// 5. Create Vue App
|
|
||||||
const app = createApp(Sidebar, { tweetData })
|
|
||||||
app.mount(container)
|
app.mount(container)
|
||||||
|
|
||||||
|
isMounted = true
|
||||||
|
isMounting = false
|
||||||
|
console.log('[InsightReply] Sidebar mounted successfully')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure it mounts on load if needed, but primarily triggered by messages
|
async function showSidebar(tweetData?: any) {
|
||||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
await initSidebar()
|
||||||
if (message.type === 'SHOW_INSIGHT' || message.type === 'TOGGLE_SIDEBAR') {
|
|
||||||
initSidebar(message.payload);
|
if (tweetData) {
|
||||||
// The component itself listens for TOGGLE_SIDEBAR to show/hide
|
currentTweetData.value = tweetData
|
||||||
sendResponse({ received: true });
|
console.log('[InsightReply] Tweet data updated:', tweetData.content?.substring(0, 60))
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
sidebarVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleSidebar() {
|
||||||
|
await initSidebar()
|
||||||
|
sidebarVisible.value = !sidebarVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for messages from background script
|
||||||
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
|
console.log('[InsightReply SidebarMount] Received message:', message.type)
|
||||||
|
|
||||||
|
if (message.type === 'SHOW_INSIGHT') {
|
||||||
|
showSidebar(message.payload)
|
||||||
|
sendResponse({ received: true })
|
||||||
|
} else if (message.type === 'TOGGLE_SIDEBAR') {
|
||||||
|
toggleSidebar()
|
||||||
|
sendResponse({ received: true })
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|||||||
@@ -84,35 +84,30 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-12">
|
<div class="space-y-8 animate-in fade-in duration-700">
|
||||||
|
|
||||||
<header class="flex justify-between items-end">
|
<div v-if="!showForm" class="flex justify-end">
|
||||||
<div class="space-y-1">
|
|
||||||
<h2 class="text-2xl font-bold tracking-tight">Competitor Radar</h2>
|
|
||||||
<p class="text-sm text-zinc-500 font-medium">Identify and engage in conversations happening around your alternatives.</p>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
v-if="!showForm"
|
|
||||||
@click="showForm = true"
|
@click="showForm = true"
|
||||||
class="px-5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-[14px] text-sm font-bold transition-apple shadow-lg shadow-blue-900/20 active:scale-95 flex items-center gap-2"
|
class="w-full py-4 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl text-sm font-bold transition-apple shadow-lg shadow-blue-900/20 active:scale-95 flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||||
Add Target
|
Add Target
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLoading" class="py-20 flex justify-center">
|
<div v-if="isLoading" class="py-20 flex justify-center">
|
||||||
<div class="w-8 h-8 border-2 border-white/10 border-t-blue-500 rounded-full animate-spin"></div>
|
<div class="w-8 h-8 border-2 border-white/10 border-t-blue-500 rounded-full animate-spin"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-8">
|
<div v-else class="space-y-6">
|
||||||
<!-- Create Form Modal View -->
|
<!-- Create Form Modal View -->
|
||||||
<transition enter-active-class="transition duration-500 ease-out transform" enter-from-class="opacity-0 scale-95 -translate-y-4" enter-to-class="opacity-100 scale-100 translate-y-0">
|
<transition enter-active-class="transition duration-500 ease-out transform" enter-from-class="opacity-0 scale-95 -translate-y-4" enter-to-class="opacity-100 scale-100 translate-y-0">
|
||||||
<form v-if="showForm" @submit.prevent="createCompetitor" class="bg-white/[0.03] border border-white/5 rounded-[32px] p-8 space-y-8 shadow-2xl relative">
|
<form v-if="showForm" @submit.prevent="createCompetitor" class="bg-white/[0.03] border border-white/5 rounded-[32px] p-6 space-y-6 shadow-2xl relative">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h3 class="text-lg font-bold">New Radar Target</h3>
|
<h3 class="text-base font-bold">New Radar Target</h3>
|
||||||
<button type="button" @click="showForm = false" class="p-2 rounded-full hover:bg-white/5 transition-apple">
|
<button type="button" @click="showForm = false" class="p-2 rounded-full hover:bg-white/5 transition-apple">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -120,7 +115,7 @@ onMounted(() => {
|
|||||||
{{ errorMsg }}
|
{{ errorMsg }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Competitor Name</label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Competitor Name</label>
|
||||||
<input v-model="form.competitor_name" required class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/40 placeholder:text-zinc-700" placeholder="e.g. Acme Corp" />
|
<input v-model="form.competitor_name" required class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/40 placeholder:text-zinc-700" placeholder="e.g. Acme Corp" />
|
||||||
@@ -130,7 +125,7 @@ onMounted(() => {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<select v-model="form.platform" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/40 placeholder:text-zinc-700 appearance-none cursor-pointer">
|
<select v-model="form.platform" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/40 placeholder:text-zinc-700 appearance-none cursor-pointer">
|
||||||
<option value="twitter">Twitter / X</option>
|
<option value="twitter">Twitter / X</option>
|
||||||
<option value="reddit">Reddit (Coming Soon)</option>
|
<option value="reddit">Reddit (Soon)</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-600">
|
<div class="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-600">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||||
@@ -139,9 +134,9 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">X Handle <span class="text-zinc-700">(optional)</span></label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">X Handle</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<span class="absolute inset-y-0 left-5 flex items-center text-zinc-600 pointer-events-none text-sm font-bold">@</span>
|
<span class="absolute inset-y-0 left-5 flex items-center text-zinc-600 pointer-events-none text-sm font-bold">@</span>
|
||||||
<input v-model="form.target_handle" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/40 placeholder:text-zinc-700 pl-10" placeholder="handle" />
|
<input v-model="form.target_handle" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/40 placeholder:text-zinc-700 pl-10" placeholder="handle" />
|
||||||
@@ -149,16 +144,14 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Monitor Keywords</label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Monitor Keywords</label>
|
||||||
<input v-model="form.keywords" required class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/40 placeholder:text-zinc-700" placeholder="e.g. acme sucks, alternative to acme" />
|
<input v-model="form.keywords" required class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/40 placeholder:text-zinc-700" placeholder="e.g. acme sucks..." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<button type="submit" :disabled="isSubmitting" class="w-full py-4 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl text-sm font-bold transition-apple shadow-xl shadow-blue-900/30 flex items-center justify-center gap-3 active:scale-95 disabled:opacity-50">
|
||||||
<button type="submit" :disabled="isSubmitting" class="px-8 py-3.5 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl text-[15px] font-bold transition-apple shadow-xl shadow-blue-900/30 flex items-center gap-3 active:scale-95 disabled:opacity-50">
|
<div v-if="isSubmitting" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
||||||
<div v-if="isSubmitting" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
{{ isSubmitting ? 'Calibrating...' : 'Initialize Radar' }}
|
||||||
{{ isSubmitting ? 'Calibrating...' : 'Initialize Radar' }}
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
@@ -170,44 +163,37 @@ onMounted(() => {
|
|||||||
<p class="text-zinc-500 font-bold tracking-tight">Radar is currently silent. No competitors added.</p>
|
<p class="text-zinc-500 font-bold tracking-tight">Radar is currently silent. No competitors added.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<div v-for="c in competitors" :key="c.id" class="group bg-white/[0.02] border border-white/5 rounded-[32px] p-6 flex flex-col justify-between transition-apple hover:bg-white/[0.04] hover:border-white/10 hover:shadow-2xl relative overflow-hidden">
|
<div v-for="c in competitors" :key="c.id" class="group bg-white/[0.02] border border-white/5 rounded-[24px] p-4 flex flex-col justify-between transition-apple hover:bg-white/[0.04] relative overflow-hidden">
|
||||||
|
|
||||||
<div class="flex justify-between items-start mb-6 z-10">
|
<div class="flex justify-between items-start mb-4 z-10">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-12 h-12 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-500 group-hover:scale-110 transition-apple">
|
<div class="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-500 group-hover:scale-110 transition-apple shrink-0">
|
||||||
<svg v-if="c.platform === 'twitter'" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>
|
<svg v-if="c.platform === 'twitter'" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>
|
||||||
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M2 12h2"/><path d="M20 12h2"/></svg>
|
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M2 12h2"/><path d="M20 12h2"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="min-w-0">
|
||||||
<h4 class="text-[17px] font-black tracking-tight group-hover:text-blue-400 transition-apple">{{ c.competitor_name }}</h4>
|
<h4 class="text-sm font-black tracking-tight group-hover:text-blue-400 transition-apple truncate">{{ c.competitor_name }}</h4>
|
||||||
<p v-if="c.target_handle" class="text-[11px] font-bold text-zinc-600 mt-0.5 tracking-tight font-mono">@{{ c.target_handle }}</p>
|
<p v-if="c.target_handle" class="text-[9px] font-bold text-zinc-600 mt-0.5 tracking-tight font-mono">@{{ c.target_handle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="group/pulse flex items-center gap-2 px-3 py-1 bg-emerald-500/10 rounded-full border border-emerald-500/20">
|
<div class="flex items-center gap-1.5 px-2 py-0.5 bg-emerald-500/10 rounded-full border border-emerald-500/20">
|
||||||
<div class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div>
|
<div class="w-1 h-1 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||||
<span class="text-[9px] font-black text-emerald-400 uppercase tracking-widest">Tracking</span>
|
<span class="text-[7px] font-black text-emerald-400 uppercase tracking-widest">Active</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3 z-10">
|
<div class="space-y-2 z-10">
|
||||||
<span class="text-[9px] font-black text-zinc-700 uppercase tracking-[0.2em] flex items-center gap-2">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
<span v-for="kw in c.keywords.split(',')" :key="kw" class="px-2 py-0.5 bg-black/40 text-zinc-500 text-[10px] font-bold rounded border border-white/5 transition-apple group-hover:border-blue-500/30 group-hover:text-zinc-300">
|
||||||
Search Logic
|
|
||||||
</span>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<span v-for="kw in c.keywords.split(',')" :key="kw" class="px-3 py-1 bg-black/40 text-zinc-400 text-[11px] font-bold rounded-lg border border-white/5 transition-apple group-hover:border-blue-500/30 group-hover:text-zinc-200">
|
|
||||||
{{ kw.trim() }}
|
{{ kw.trim() }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button @click="deleteCompetitor(c.id)" class="absolute top-2 right-2 p-3 text-zinc-800 hover:text-rose-500 transition-apple opacity-0 group-hover:opacity-100 z-20 active:scale-90">
|
<button @click="deleteCompetitor(c.id)" class="absolute bottom-2 right-2 p-3 text-zinc-800 hover:text-rose-500 transition-apple active:scale-90 z-20">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><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>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><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>
|
</button>
|
||||||
|
|
||||||
<!-- Abstract Decor -->
|
|
||||||
<div class="absolute -right-10 -bottom-10 w-32 h-32 bg-blue-500/5 blur-3xl rounded-full group-hover:bg-blue-500/10 transition-apple"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ const loadHotTweets = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (response.status === 429) {
|
||||||
|
error.value = '请求频率达到上限,请稍后再试。Free 用户每日限额 10 次 API 调用。'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Opportunity discovery failed (${response.status})`)
|
throw new Error(`Opportunity discovery failed (${response.status})`)
|
||||||
}
|
}
|
||||||
@@ -46,7 +51,7 @@ const loadHotTweets = async () => {
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
tweets.value = data || []
|
tweets.value = data || []
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error.value = err.message || 'Quantum network sync error.'
|
error.value = err.message || 'Network connection error.'
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -63,22 +68,19 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full flex flex-col space-y-10 animate-in fade-in duration-700">
|
<div class="h-full flex flex-col space-y-6 animate-in fade-in duration-700">
|
||||||
<header class="flex items-center justify-between">
|
<header class="flex items-center justify-between">
|
||||||
<div class="space-y-1">
|
<div class="flex items-center gap-3">
|
||||||
<h2 class="text-2xl font-bold tracking-tight flex items-center gap-3">
|
<h2 class="text-sm font-black uppercase tracking-widest text-zinc-400">Opportunities</h2>
|
||||||
Growth Opportunities
|
<div class="px-2 py-0.5 bg-orange-500/10 text-orange-500 border border-orange-500/20 rounded-full text-[8px] font-black uppercase tracking-widest animate-pulse font-mono">Live</div>
|
||||||
<div class="px-2.5 py-1 bg-orange-500/10 text-orange-500 border border-orange-500/20 rounded-full text-[9px] font-black uppercase tracking-widest animate-pulse">Live Radar</div>
|
|
||||||
</h2>
|
|
||||||
<p class="text-sm text-zinc-500 font-medium">Threads where your product DNA has the highest relevance and conversion potential.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="loadHotTweets"
|
@click="loadHotTweets"
|
||||||
class="p-3 bg-white/[0.03] border border-white/10 rounded-2xl hover:bg-white/[0.08] transition-apple active:scale-95 group shadow-xl"
|
class="p-2 bg-white/[0.03] border border-white/10 rounded-xl hover:bg-white/[0.08] transition-apple active:scale-95 group shadow-xl"
|
||||||
title="Refresh Radar"
|
title="Refresh Radar"
|
||||||
>
|
>
|
||||||
<svg :class="['w-5 h-5 text-zinc-400 group-hover:text-white transition-apple', loading ? 'animate-spin' : '']" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
<svg :class="['w-4 h-4 text-zinc-500 group-hover:text-white transition-apple', loading ? 'animate-spin' : '']" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||||
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/>
|
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -106,49 +108,46 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feed Grid -->
|
<!-- Feed Grid -->
|
||||||
<div v-else class="grid gap-6 md:grid-cols-2">
|
<div v-else class="grid gap-4 grid-cols-1">
|
||||||
<div v-for="tweet in tweets" :key="tweet.id" class="group bg-white/[0.02] border border-white/5 hover:border-orange-500/20 hover:bg-white/[0.04] transition-apple rounded-[32px] overflow-hidden p-8 flex flex-col justify-between shadow-xl relative hover:shadow-orange-950/20">
|
<div v-for="tweet in tweets" :key="tweet.id" class="group bg-white/[0.02] border border-white/5 hover:border-orange-500/20 hover:bg-white/[0.04] transition-apple rounded-[24px] overflow-hidden p-5 flex flex-col justify-between shadow-xl relative">
|
||||||
|
|
||||||
<!-- Heat score visualizer -->
|
|
||||||
<div class="absolute top-0 right-0 w-32 h-32 bg-orange-500/5 blur-[40px] rounded-full -mr-10 -mt-10 group-hover:bg-orange-500/15 transition-apple"></div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<header class="flex justify-between items-start mb-6">
|
<header class="flex justify-between items-start mb-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 rounded-full bg-black/40 border border-white/5 flex items-center justify-center text-[11px] font-black font-mono text-zinc-400 uppercase">
|
<div class="w-8 h-8 rounded-full bg-black/40 border border-white/5 flex items-center justify-center text-[9px] font-black font-mono text-zinc-500 uppercase">
|
||||||
{{ tweet.author_handle.slice(0, 2) }}
|
{{ tweet.author_handle.slice(0, 2) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="font-black text-[15px] text-white tracking-tight group-hover:text-orange-400 transition-apple">@{{ tweet.author_handle }}</span>
|
<span class="font-black text-sm text-white tracking-tight group-hover:text-orange-400 transition-apple">@{{ tweet.author_handle }}</span>
|
||||||
<span class="text-[9px] text-zinc-600 font-black uppercase tracking-widest">{{ new Date(tweet.posted_at).toLocaleDateString() }}</span>
|
<span class="text-[8px] text-zinc-600 font-black uppercase tracking-widest">{{ new Date(tweet.posted_at).toLocaleDateString() }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2.5 px-3 py-1 bg-rose-500/10 border border-rose-500/20 rounded-full">
|
<div class="flex items-center gap-1.5 px-2 py-0.5 bg-rose-500/10 border border-rose-500/20 rounded-full">
|
||||||
<div class="w-1.5 h-1.5 rounded-full bg-rose-500 shadow-[0_0_8px_#f43f5e]"></div>
|
<div class="w-1 h-1 rounded-full bg-rose-500"></div>
|
||||||
<span class="text-[9px] font-black text-rose-400 uppercase tracking-[0.2em]">{{ tweet.heat_score }}</span>
|
<span class="text-[8px] font-black text-rose-400 uppercase tracking-widest">{{ tweet.heat_score }}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p class="text-zinc-400 text-[13px] leading-relaxed mb-6 font-medium line-clamp-4 relative group-hover:text-zinc-300 transition-apple">
|
<p class="text-zinc-400 text-[12px] leading-relaxed mb-4 font-medium line-clamp-3 relative group-hover:text-zinc-300 transition-apple">
|
||||||
"{{ tweet.content }}"
|
"{{ tweet.content }}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="flex items-center justify-between border-t border-white/5 pt-6 mt-4">
|
<footer class="flex items-center justify-between border-t border-white/5 pt-4 mt-2">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-3">
|
||||||
<div class="flex items-center gap-1.5 text-zinc-600 group-hover:text-zinc-400 transition-apple font-bold text-[11px]">
|
<div class="flex items-center gap-1 text-zinc-600 group-hover:text-zinc-500 transition-apple font-bold text-[9px]">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||||
{{ tweet.reply_count }}
|
{{ tweet.reply_count }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 text-zinc-600 group-hover:text-zinc-400 transition-apple font-bold text-[11px]">
|
<div class="flex items-center gap-1 text-zinc-600 group-hover:text-zinc-500 transition-apple font-bold text-[9px]">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>
|
||||||
{{ tweet.like_count }}
|
{{ tweet.like_count }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="openTweet(tweet.author_handle, tweet.x_tweet_id)"
|
@click="openTweet(tweet.author_handle, tweet.x_tweet_id)"
|
||||||
class="px-6 py-2.5 bg-zinc-800 hover:bg-orange-500 text-white text-[11px] font-black uppercase tracking-[0.1em] rounded-xl transition-apple shadow-lg active:scale-95"
|
class="px-4 py-2 bg-zinc-800 hover:bg-orange-500 text-white text-[9px] font-black uppercase tracking-widest rounded-lg transition-apple shadow-lg active:scale-95"
|
||||||
>
|
>
|
||||||
Engage ↗
|
Engage ↗
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -97,13 +97,8 @@ onMounted(() => {
|
|||||||
<div class="w-8 h-8 border-2 border-white/10 border-t-rose-500 rounded-full animate-spin"></div>
|
<div class="w-8 h-8 border-2 border-white/10 border-t-rose-500 rounded-full animate-spin"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-12 animate-in fade-in duration-700">
|
<div v-else class="space-y-8 animate-in fade-in duration-700">
|
||||||
<header class="space-y-1">
|
<form @submit.prevent="saveProfile" class="space-y-8">
|
||||||
<h2 class="text-2xl font-bold tracking-tight">Product DNA</h2>
|
|
||||||
<p class="text-sm text-zinc-500 font-medium">Shape your AI's core knowledge about what you're building.</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form @submit.prevent="saveProfile" class="space-y-10">
|
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<transition enter-active-class="transition duration-300 transform" enter-from-class="opacity-0 -translate-y-2" enter-to-class="opacity-100 translate-y-0">
|
<transition enter-active-class="transition duration-300 transform" enter-from-class="opacity-0 -translate-y-2" enter-to-class="opacity-100 translate-y-0">
|
||||||
@@ -119,10 +114,8 @@ onMounted(() => {
|
|||||||
{{ savedMessage }}
|
{{ savedMessage }}
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
<div class="space-y-6">
|
||||||
<!-- Basic Info Section -->
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<section class="space-y-6">
|
|
||||||
<div class="grid grid-cols-2 gap-8">
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Product Name</label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Product Name</label>
|
||||||
<input v-model="form.product_name" required class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700" placeholder="e.g. InsightReply" />
|
<input v-model="form.product_name" required class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700" placeholder="e.g. InsightReply" />
|
||||||
@@ -138,7 +131,7 @@ onMounted(() => {
|
|||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Target Persona</label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Target Persona</label>
|
||||||
<input v-model="form.target_audience" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700" placeholder="e.g. Founders & Indie Hackers on X" />
|
<input v-model="form.target_audience" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700" placeholder="e.g. Founders & Indie Hackers on X" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<!-- Knowledge Base Section -->
|
<!-- Knowledge Base Section -->
|
||||||
<section class="space-y-8 pt-4">
|
<section class="space-y-8 pt-4">
|
||||||
@@ -164,16 +157,15 @@ onMounted(() => {
|
|||||||
<!-- System Logic Section -->
|
<!-- System Logic Section -->
|
||||||
<section class="pt-8 border-t border-white/5 space-y-6">
|
<section class="pt-8 border-t border-white/5 space-y-6">
|
||||||
<header class="space-y-1">
|
<header class="space-y-1">
|
||||||
<h3 class="text-sm font-bold text-zinc-400">Advanced: Neutral Intelligence</h3>
|
<h3 class="text-xs font-bold text-zinc-400 uppercase tracking-[0.1em]">Engine Overrides</h3>
|
||||||
<p class="text-[11px] text-zinc-600 font-medium">Override the primary engine models if you have specific performance needs.</p>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">LLM Provider</label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">LLM Provider</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<select v-model="form.default_llm_provider" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700 appearance-none cursor-pointer">
|
<select v-model="form.default_llm_provider" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700 appearance-none cursor-pointer">
|
||||||
<option value="">Global Default (Automated)</option>
|
<option value="">Global Default</option>
|
||||||
<option v-for="p in providers" :key="p" :value="p">{{ p.charAt(0).toUpperCase() + p.slice(1) }}</option>
|
<option v-for="p in providers" :key="p" :value="p">{{ p.charAt(0).toUpperCase() + p.slice(1) }}</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-600">
|
<div class="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-600">
|
||||||
@@ -182,22 +174,22 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Engine Model Override</label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Model Override</label>
|
||||||
<input v-model="form.default_llm_model" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700" placeholder="e.g. gpt-4o, claude-3-5-sonnet" />
|
<input v-model="form.default_llm_model" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700" placeholder="e.g. gpt-4o" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Action -->
|
<!-- Action -->
|
||||||
<footer class="pt-10 flex justify-end">
|
<footer class="pt-4 pb-8">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
class="px-10 py-4 bg-rose-500 hover:bg-rose-400 text-white rounded-2xl text-[15px] font-bold transition-apple shadow-xl shadow-rose-900/40 flex items-center gap-3 active:scale-95 disabled:opacity-50"
|
class="w-full py-4 bg-rose-500 hover:bg-rose-400 text-white rounded-2xl text-sm font-bold transition-apple shadow-xl shadow-rose-900/40 flex items-center justify-center gap-3 active:scale-95 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<div v-if="isSaving" class="w-5 h-5 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
<div v-if="isSaving" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="text-rose-100"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="text-rose-100"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
{{ isSaving ? 'Committing Changes...' : 'Synchronize Product DNA' }}
|
{{ isSaving ? 'Committing...' : 'Save Product DNA' }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -83,46 +83,41 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-12">
|
<div class="space-y-8 animate-in fade-in duration-700">
|
||||||
|
|
||||||
<header class="flex justify-between items-end">
|
<div v-if="!showForm" class="flex justify-end">
|
||||||
<div class="space-y-1">
|
|
||||||
<h2 class="text-2xl font-bold tracking-tight">AI Content Strategies</h2>
|
|
||||||
<p class="text-sm text-zinc-500 font-medium">Fine-tune how your AI assistant approaches engagement for different personas.</p>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
v-if="!showForm"
|
|
||||||
@click="showForm = true"
|
@click="showForm = true"
|
||||||
class="px-5 py-2.5 bg-rose-500 hover:bg-rose-400 text-white rounded-[14px] text-sm font-bold transition-apple shadow-lg shadow-rose-900/20 active:scale-95 flex items-center gap-2"
|
class="w-full py-4 bg-rose-500 hover:bg-rose-400 text-white rounded-2xl text-sm font-bold transition-apple shadow-lg shadow-rose-900/20 active:scale-95 flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||||
New Strategy
|
New Prompt Angle
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLoading" class="py-20 flex justify-center">
|
<div v-if="isLoading" class="py-20 flex justify-center">
|
||||||
<div class="w-8 h-8 border-2 border-white/10 border-t-rose-500 rounded-full animate-spin"></div>
|
<div class="w-8 h-8 border-2 border-white/10 border-t-rose-500 rounded-full animate-spin"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-8">
|
<div v-else class="space-y-6">
|
||||||
<!-- Create Form Modal View -->
|
<!-- Create Form Modal View -->
|
||||||
<transition enter-active-class="transition duration-500 ease-out transform" enter-from-class="opacity-0 scale-95 -translate-y-4" enter-to-class="opacity-100 scale-100 translate-y-0">
|
<transition enter-active-class="transition duration-500 ease-out transform" enter-from-class="opacity-0 scale-95 -translate-y-4" enter-to-class="opacity-100 scale-100 translate-y-0">
|
||||||
<form v-if="showForm" @submit.prevent="createStrategy" class="bg-white/[0.03] border border-white/5 rounded-[32px] p-8 space-y-8 shadow-2xl relative">
|
<form v-if="showForm" @submit.prevent="createStrategy" class="bg-white/[0.03] border border-white/5 rounded-[32px] p-6 space-y-6 shadow-2xl relative">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h3 class="text-lg font-bold">New Prompt Angle</h3>
|
<h3 class="text-base font-bold">Configure Angle</h3>
|
||||||
<button type="button" @click="showForm = false" class="p-2 rounded-full hover:bg-white/5 transition-apple">
|
<button type="button" @click="showForm = false" class="p-2 rounded-full hover:bg-white/5 transition-apple">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="errorMsg" class="p-4 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs font-bold rounded-xl animate-pulse">
|
<div v-if="errorMsg" class="p-4 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs font-bold rounded-xl">
|
||||||
{{ errorMsg }}
|
{{ errorMsg }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Internal Reference ID</label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Internal key</label>
|
||||||
<input v-model="form.strategy_key" required class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700" placeholder="e.g. vc_advisor" />
|
<input v-model="form.strategy_key" required class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700 font-mono" placeholder="e.g. vc_advisor" />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Display Label</label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Display Label</label>
|
||||||
@@ -131,16 +126,14 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Prompt Instructions & constraints</label>
|
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Instructions</label>
|
||||||
<textarea v-model="form.description" required rows="4" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700 resize-none py-4" placeholder="Be authoritative yet encouraging. Focus on market metrics. Avoid safe 'that's great' comments."></textarea>
|
<textarea v-model="form.description" required rows="4" class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold transition-apple focus:outline-none focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500/40 placeholder:text-zinc-700 resize-none py-4" placeholder="Be authoritative yet encouraging..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<button type="submit" :disabled="isSubmitting" class="w-full py-4 bg-rose-500 hover:bg-rose-400 text-white rounded-2xl text-sm font-bold transition-apple shadow-xl shadow-rose-900/30 flex items-center justify-center gap-3 active:scale-95 disabled:opacity-50">
|
||||||
<button type="submit" :disabled="isSubmitting" class="px-8 py-3.5 bg-rose-500 hover:bg-rose-400 text-white rounded-2xl text-[15px] font-bold transition-apple shadow-xl shadow-rose-900/30 flex items-center gap-3 active:scale-95 disabled:opacity-50">
|
<div v-if="isSubmitting" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
||||||
<div v-if="isSubmitting" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
{{ isSubmitting ? 'Architecting...' : 'Deploy Strategy' }}
|
||||||
{{ isSubmitting ? 'Architecting...' : 'Save Strategy' }}
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
@@ -153,21 +146,21 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<div v-for="s in strategies" :key="s.id" class="group bg-white/[0.02] border border-white/5 rounded-[28px] p-6 flex justify-between items-center transition-apple hover:bg-white/[0.04] hover:border-white/10 hover:shadow-2xl">
|
<div v-for="s in strategies" :key="s.id" class="group bg-white/[0.02] border border-white/5 rounded-[24px] p-4 flex justify-between items-center transition-apple hover:bg-white/[0.04]">
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-4">
|
||||||
<div class="w-14 h-14 bg-rose-500/5 border border-rose-500/10 rounded-2xl flex items-center justify-center text-rose-500 group-hover:scale-110 transition-apple">
|
<div class="w-10 h-10 bg-rose-500/5 border border-rose-500/10 rounded-xl flex items-center justify-center text-rose-500 transition-apple shrink-0">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="min-w-0">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-[17px] font-black tracking-tight group-hover:text-rose-400 transition-apple">{{ s.label }}</span>
|
<span class="text-sm font-black tracking-tight truncate group-hover:text-rose-400 transition-apple">{{ s.label }}</span>
|
||||||
<span class="text-[9px] font-black font-mono text-zinc-500 bg-black/40 px-2 py-0.5 rounded-md border border-white/5 uppercase tracking-tighter">{{ s.strategy_key }}</span>
|
<span class="text-[8px] font-black font-mono text-zinc-600 bg-black/40 px-1.5 py-0.5 rounded border border-white/5 uppercase tracking-tighter">{{ s.strategy_key }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[13px] text-zinc-500 leading-relaxed font-medium max-w-xl group-hover:text-zinc-400 transition-apple">{{ s.description }}</p>
|
<p class="text-[11px] text-zinc-500 leading-relaxed font-medium truncate group-hover:text-zinc-400 transition-apple">{{ s.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button @click="deleteStrategy(s.id)" class="p-4 rounded-2xl text-zinc-700 hover:text-rose-500 hover:bg-rose-500/10 transition-apple opacity-0 group-hover:opacity-100 active:scale-95">
|
<button @click="deleteStrategy(s.id)" class="p-3 rounded-xl text-zinc-700 hover:text-rose-500 transition-apple active:scale-95">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><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>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ func (h *AIHandler) Generate(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
userID := ctx.Value("userID").(string)
|
userID := ctx.Value("userID").(string)
|
||||||
|
log.Printf("[AIHandler] Generate request from userID=%s strategy=%q provider=%q model=%q", userID, body.Strategy, body.Provider, body.Model)
|
||||||
|
|
||||||
// Fetch Product Profile Context
|
// Fetch Product Profile Context
|
||||||
var productContext string
|
var productContext string
|
||||||
@@ -79,9 +81,11 @@ func (h *AIHandler) Generate(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
replyString, err := h.svc.GenerateReply(ctx, body.TweetContent, productContext, body.Identity, body.Provider, body.Model)
|
replyString, err := h.svc.GenerateReply(ctx, body.TweetContent, productContext, body.Identity, body.Provider, body.Model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[AIHandler] ERROR GenerateReply for userID=%s: %v", userID, err)
|
||||||
SendError(w, http.StatusBadGateway, 5002, "Failed to generate AI reply: "+err.Error())
|
SendError(w, http.StatusBadGateway, 5002, "Failed to generate AI reply: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("[AIHandler] GenerateReply success for userID=%s, reply length=%d", userID, len(replyString))
|
||||||
|
|
||||||
// Clean up potential markdown wrappers from LLM output
|
// Clean up potential markdown wrappers from LLM output
|
||||||
cleanReply := strings.TrimSpace(replyString)
|
cleanReply := strings.TrimSpace(replyString)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/zs/InsightReply/internal/repository"
|
"github.com/zs/InsightReply/internal/repository"
|
||||||
@@ -17,15 +18,18 @@ func NewTweetHandler(repo *repository.TweetRepository) *TweetHandler {
|
|||||||
|
|
||||||
// GetHotTweets returns the top heating tweets spanning across all tracking targets
|
// GetHotTweets returns the top heating tweets spanning across all tracking targets
|
||||||
func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) {
|
func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) {
|
||||||
// Standardize to take the top 50 hottest tweets that haven't been manually marked as processed
|
log.Printf("[TweetHandler] GetHotTweets called from %s", r.RemoteAddr)
|
||||||
|
|
||||||
tweets, err := h.repo.GetTopHeatingTweets(50)
|
tweets, err := h.repo.GetTopHeatingTweets(50)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[TweetHandler] ERROR GetTopHeatingTweets: %v", err)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to retrieve hot tweets"})
|
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to retrieve hot tweets"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[TweetHandler] GetHotTweets returning %d tweets", len(tweets))
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(tweets)
|
json.NewEncoder(w).Encode(tweets)
|
||||||
}
|
}
|
||||||
@@ -34,15 +38,18 @@ func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (h *TweetHandler) GetSearchTweets(w http.ResponseWriter, r *http.Request) {
|
func (h *TweetHandler) GetSearchTweets(w http.ResponseWriter, r *http.Request) {
|
||||||
keyword := r.URL.Query().Get("keyword")
|
keyword := r.URL.Query().Get("keyword")
|
||||||
handle := r.URL.Query().Get("handle")
|
handle := r.URL.Query().Get("handle")
|
||||||
|
log.Printf("[TweetHandler] SearchTweets called: keyword=%q handle=%q", keyword, handle)
|
||||||
|
|
||||||
tweets, err := h.repo.SearchTweets(keyword, handle, 50)
|
tweets, err := h.repo.SearchTweets(keyword, handle, 50)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[TweetHandler] ERROR SearchTweets: %v", err)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to search tweets"})
|
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to search tweets"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[TweetHandler] SearchTweets returning %d tweets", len(tweets))
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(tweets)
|
json.NewEncoder(w).Encode(tweets)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -49,6 +50,7 @@ func RateLimit(db *gorm.DB) func(http.Handler) http.Handler {
|
|||||||
// For now, fallback to generic tight limit for anonymous usage
|
// For now, fallback to generic tight limit for anonymous usage
|
||||||
ipLimiter := getLimiter(r.RemoteAddr, "Free")
|
ipLimiter := getLimiter(r.RemoteAddr, "Free")
|
||||||
if !ipLimiter.Allow() {
|
if !ipLimiter.Allow() {
|
||||||
|
log.Printf("[RateLimit] 429 for anonymous IP=%s path=%s", r.RemoteAddr, r.URL.Path)
|
||||||
http.Error(w, `{"code":429, "message":"Too Many Requests: Rate limit exceeded"}`, http.StatusTooManyRequests)
|
http.Error(w, `{"code":429, "message":"Too Many Requests: Rate limit exceeded"}`, http.StatusTooManyRequests)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -72,6 +74,7 @@ func RateLimit(db *gorm.DB) func(http.Handler) http.Handler {
|
|||||||
|
|
||||||
limiter := getLimiter(userID, tier)
|
limiter := getLimiter(userID, tier)
|
||||||
if !limiter.Allow() {
|
if !limiter.Allow() {
|
||||||
|
log.Printf("[RateLimit] 429 for userID=%s tier=%s path=%s", userID, tier, r.URL.Path)
|
||||||
http.Error(w, `{"code":429, "message":"Too Many Requests: Daily quota or rate limit exceeded"}`, http.StatusTooManyRequests)
|
http.Error(w, `{"code":429, "message":"Too Many Requests: Daily quota or rate limit exceeded"}`, http.StatusTooManyRequests)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user