|
@@ -29,56 +29,20 @@
|
|
|
<span><strong>{{ $t('settings.meta.errorTitle') }}:</strong> {{ oauthError }}</span>
|
|
<span><strong>{{ $t('settings.meta.errorTitle') }}:</strong> {{ oauthError }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- Step 1: App credentials -->
|
|
|
|
|
- <div class="p-5 border-b border-gray-800/60">
|
|
|
|
|
- <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Step 1 — Facebook Developer App</p>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="metaAppConfigured" class="flex items-center justify-between">
|
|
|
|
|
- <div class="flex items-center gap-2 text-sm text-green-400">
|
|
|
|
|
- <span>✓</span>
|
|
|
|
|
- <span>{{ $t('settings.meta.appConfigured') }}</span>
|
|
|
|
|
- <span class="text-gray-600 font-mono text-xs">({{ platformsStore.metaCredentials.appId }})</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <button @click="editingApp = !editingApp" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">
|
|
|
|
|
- Edit
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="!metaAppConfigured || editingApp" class="space-y-3 mt-2">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.meta.appIdLabel') }}</label>
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="appId"
|
|
|
|
|
- type="text"
|
|
|
|
|
- :placeholder="$t('settings.meta.appIdPlaceholder')"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.meta.appSecretLabel') }}</label>
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="appSecret"
|
|
|
|
|
- type="password"
|
|
|
|
|
- :placeholder="metaAppConfigured ? platformsStore.metaCredentials.appSecretHint : $t('settings.meta.appSecretPlaceholder')"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="flex items-center justify-between">
|
|
|
|
|
- <p class="text-xs text-gray-600">
|
|
|
|
|
- {{ $t('settings.meta.getAppHelp') }}
|
|
|
|
|
- <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener" class="text-blue-400 hover:text-blue-300 underline">
|
|
|
|
|
- {{ $t('settings.meta.devPortal') }}
|
|
|
|
|
- </a>
|
|
|
|
|
- </p>
|
|
|
|
|
- <button
|
|
|
|
|
- @click="saveApp"
|
|
|
|
|
- :disabled="!appId || !appSecret || platformsStore.metaLoading"
|
|
|
|
|
- class="px-4 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- {{ platformsStore.metaLoading ? $t('settings.meta.saving') : $t('settings.meta.saveApp') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <!-- Step 1: App credentials (configure in Global Settings) -->
|
|
|
|
|
+ <div class="p-5 border-b border-gray-800/60 flex items-center justify-between">
|
|
|
|
|
+ <div v-if="metaAppConfigured" class="flex items-center gap-2 text-sm text-green-400">
|
|
|
|
|
+ <span>✓</span>
|
|
|
|
|
+ <span>{{ $t('settings.meta.appConfigured') }}</span>
|
|
|
|
|
+ <span class="text-gray-600 font-mono text-xs">({{ platformsStore.metaCredentials.appId }})</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div v-else class="text-sm text-gray-500">{{ $t('settings.meta.appNotConfigured') }}</div>
|
|
|
|
|
+ <router-link
|
|
|
|
|
+ to="/global-settings#meta-app"
|
|
|
|
|
+ class="text-xs px-2.5 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-gray-200 transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ $t('settings.configureInGlobal') }} →
|
|
|
|
|
+ </router-link>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Step 2: OAuth connect -->
|
|
<!-- Step 2: OAuth connect -->
|
|
@@ -87,22 +51,52 @@
|
|
|
|
|
|
|
|
<!-- Already connected — show summary + manage -->
|
|
<!-- Already connected — show summary + manage -->
|
|
|
<div v-if="fbConnected || igConnected" class="space-y-3">
|
|
<div v-if="fbConnected || igConnected" class="space-y-3">
|
|
|
- <div v-if="fbPages.length" class="space-y-1.5">
|
|
|
|
|
- <p class="text-xs text-gray-500">{{ $t('settings.meta.connectedPages') }}</p>
|
|
|
|
|
- <div v-for="page in fbPages" :key="page.id" class="flex items-center gap-2 bg-gray-800/60 rounded-lg px-3 py-2">
|
|
|
|
|
- <img v-if="page.picture" :src="page.picture" class="w-6 h-6 rounded-full" />
|
|
|
|
|
- <span v-else class="w-6 h-6 rounded-full bg-blue-700 flex items-center justify-center text-xs font-bold">f</span>
|
|
|
|
|
- <span class="text-sm">{{ page.name }}</span>
|
|
|
|
|
- <span class="ml-auto w-2 h-2 rounded-full bg-green-400"></span>
|
|
|
|
|
|
|
+ <div v-if="platformsStore.allFbPages.length" class="space-y-1.5">
|
|
|
|
|
+ <p class="text-xs text-gray-500">{{ $t('settings.meta.selectPages') }}</p>
|
|
|
|
|
+ <label
|
|
|
|
|
+ v-for="page in platformsStore.allFbPages"
|
|
|
|
|
+ :key="page.id"
|
|
|
|
|
+ class="flex items-center gap-3 bg-gray-800/60 hover:bg-gray-800 rounded-lg px-3 py-2 cursor-pointer transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ <input type="checkbox" :value="page.id" v-model="workspaceFbPageIds" class="w-4 h-4 accent-blue-500" />
|
|
|
|
|
+ <img v-if="page.picture" :src="page.picture" class="w-6 h-6 rounded-full shrink-0" />
|
|
|
|
|
+ <span v-else class="w-6 h-6 rounded-full bg-blue-700 flex items-center justify-center text-xs font-bold shrink-0">f</span>
|
|
|
|
|
+ <span class="text-sm flex-1">{{ page.name }}</span>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div class="flex items-center justify-between pt-1">
|
|
|
|
|
+ <span v-if="fbPagesSaved" class="text-xs text-green-400">{{ $t('settings.meta.selectionSaved') }}</span>
|
|
|
|
|
+ <span v-else />
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="saveFbPageSelection"
|
|
|
|
|
+ :disabled="platformsStore.metaLoading"
|
|
|
|
|
+ class="text-xs px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-40 rounded-lg font-medium transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ $t('settings.meta.saveSelection') }}
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div v-if="igAccounts.length" class="space-y-1.5">
|
|
|
|
|
- <p class="text-xs text-gray-500">{{ $t('settings.meta.connectedAccounts') }}</p>
|
|
|
|
|
- <div v-for="account in igAccounts" :key="account.id" class="flex items-center gap-2 bg-gray-800/60 rounded-lg px-3 py-2">
|
|
|
|
|
- <img v-if="account.avatar" :src="account.avatar" class="w-6 h-6 rounded-full" />
|
|
|
|
|
- <span v-else class="w-6 h-6 rounded-full bg-pink-700 flex items-center justify-center text-xs font-bold">I</span>
|
|
|
|
|
- <span class="text-sm">@{{ account.username }}</span>
|
|
|
|
|
- <span class="ml-auto w-2 h-2 rounded-full bg-green-400"></span>
|
|
|
|
|
|
|
+ <div v-if="platformsStore.allIgAccounts.length" class="space-y-1.5">
|
|
|
|
|
+ <p class="text-xs text-gray-500">{{ $t('settings.meta.selectAccounts') }}</p>
|
|
|
|
|
+ <label
|
|
|
|
|
+ v-for="account in platformsStore.allIgAccounts"
|
|
|
|
|
+ :key="account.id"
|
|
|
|
|
+ class="flex items-center gap-3 bg-gray-800/60 hover:bg-gray-800 rounded-lg px-3 py-2 cursor-pointer transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ <input type="checkbox" :value="account.id" v-model="workspaceIgAccountIds" class="w-4 h-4 accent-pink-500" />
|
|
|
|
|
+ <img v-if="account.avatar" :src="account.avatar" class="w-6 h-6 rounded-full shrink-0" />
|
|
|
|
|
+ <span v-else class="w-6 h-6 rounded-full bg-pink-700 flex items-center justify-center text-xs font-bold shrink-0">I</span>
|
|
|
|
|
+ <span class="text-sm flex-1">@{{ account.username }}</span>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div class="flex items-center justify-between pt-1">
|
|
|
|
|
+ <span v-if="igAccountsSaved" class="text-xs text-green-400">{{ $t('settings.meta.selectionSaved') }}</span>
|
|
|
|
|
+ <span v-else />
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="saveIgAccountSelection"
|
|
|
|
|
+ :disabled="platformsStore.metaLoading"
|
|
|
|
|
+ class="text-xs px-3 py-1.5 bg-pink-600 hover:bg-pink-700 disabled:opacity-40 rounded-lg font-medium transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ $t('settings.meta.saveSelection') }}
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<!-- Token expiry warning banner -->
|
|
<!-- Token expiry warning banner -->
|
|
@@ -188,56 +182,20 @@
|
|
|
<span><strong>{{ $t('settings.pinterest.errorTitle') }}:</strong> {{ pinterestOauthError }}</span>
|
|
<span><strong>{{ $t('settings.pinterest.errorTitle') }}:</strong> {{ pinterestOauthError }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- Step 1: App credentials -->
|
|
|
|
|
- <div class="p-5 border-b border-gray-800/60">
|
|
|
|
|
- <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Step 1 — Pinterest Developer App</p>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="pinterestAppConfigured" class="flex items-center justify-between">
|
|
|
|
|
- <div class="flex items-center gap-2 text-sm text-green-400">
|
|
|
|
|
- <span>✓</span>
|
|
|
|
|
- <span>{{ $t('settings.pinterest.appConfigured') }}</span>
|
|
|
|
|
- <span class="text-gray-600 font-mono text-xs">({{ platformsStore.pinterestCredentials.clientId }})</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <button @click="editingPinterestApp = !editingPinterestApp" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">
|
|
|
|
|
- Edit
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="!pinterestAppConfigured || editingPinterestApp" class="space-y-3 mt-2">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.pinterest.clientIdLabel') }}</label>
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="pinterestClientId"
|
|
|
|
|
- type="text"
|
|
|
|
|
- :placeholder="$t('settings.pinterest.clientIdPlaceholder')"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-red-500"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.pinterest.clientSecretLabel') }}</label>
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="pinterestClientSecret"
|
|
|
|
|
- type="password"
|
|
|
|
|
- :placeholder="pinterestAppConfigured ? platformsStore.pinterestCredentials.clientSecretHint : $t('settings.pinterest.clientSecretPlaceholder')"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-red-500"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="flex items-center justify-between">
|
|
|
|
|
- <p class="text-xs text-gray-600">
|
|
|
|
|
- {{ $t('settings.pinterest.getAppHelp') }}
|
|
|
|
|
- <a href="https://developers.pinterest.com/apps/" target="_blank" rel="noopener" class="text-red-400 hover:text-red-300 underline">
|
|
|
|
|
- {{ $t('settings.pinterest.devPortal') }}
|
|
|
|
|
- </a>
|
|
|
|
|
- </p>
|
|
|
|
|
- <button
|
|
|
|
|
- @click="savePinterestApp"
|
|
|
|
|
- :disabled="!pinterestClientId || !pinterestClientSecret || platformsStore.pinterestLoading"
|
|
|
|
|
- class="px-4 py-1.5 bg-red-600 hover:bg-red-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- {{ platformsStore.pinterestLoading ? $t('settings.pinterest.saving') : $t('settings.pinterest.saveApp') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <!-- Step 1: App credentials (configure in Global Settings) -->
|
|
|
|
|
+ <div class="p-5 border-b border-gray-800/60 flex items-center justify-between">
|
|
|
|
|
+ <div v-if="pinterestAppConfigured" class="flex items-center gap-2 text-sm text-green-400">
|
|
|
|
|
+ <span>✓</span>
|
|
|
|
|
+ <span>{{ $t('settings.pinterest.appConfigured') }}</span>
|
|
|
|
|
+ <span class="text-gray-600 font-mono text-xs">({{ platformsStore.pinterestCredentials.clientId }})</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div v-else class="text-sm text-gray-500">{{ $t('settings.meta.appNotConfigured') }}</div>
|
|
|
|
|
+ <router-link
|
|
|
|
|
+ to="/global-settings#pinterest-app"
|
|
|
|
|
+ class="text-xs px-2.5 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-gray-200 transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ $t('settings.configureInGlobal') }} →
|
|
|
|
|
+ </router-link>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Step 2: OAuth connect + boards -->
|
|
<!-- Step 2: OAuth connect + boards -->
|
|
@@ -328,56 +286,20 @@
|
|
|
<span><strong>{{ $t('settings.tiktok.errorTitle') }}:</strong> {{ tiktokOauthError }}</span>
|
|
<span><strong>{{ $t('settings.tiktok.errorTitle') }}:</strong> {{ tiktokOauthError }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- Step 1: App credentials -->
|
|
|
|
|
- <div class="p-5 border-b border-gray-800/60">
|
|
|
|
|
- <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Step 1 — TikTok Developer App</p>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="tiktokAppConfigured" class="flex items-center justify-between">
|
|
|
|
|
- <div class="flex items-center gap-2 text-sm text-green-400">
|
|
|
|
|
- <span>✓</span>
|
|
|
|
|
- <span>{{ $t('settings.tiktok.appConfigured') }}</span>
|
|
|
|
|
- <span class="text-gray-600 font-mono text-xs">({{ platformsStore.tiktokCredentials.clientKey }})</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <button @click="editingTikTokApp = !editingTikTokApp" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">
|
|
|
|
|
- Edit
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="!tiktokAppConfigured || editingTikTokApp" class="space-y-3 mt-2">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.tiktok.clientKeyLabel') }}</label>
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="tiktokClientKey"
|
|
|
|
|
- type="text"
|
|
|
|
|
- :placeholder="$t('settings.tiktok.clientKeyPlaceholder')"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-pink-500"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.tiktok.clientSecretLabel') }}</label>
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="tiktokClientSecret"
|
|
|
|
|
- type="password"
|
|
|
|
|
- :placeholder="tiktokAppConfigured ? platformsStore.tiktokCredentials.clientSecretHint : $t('settings.tiktok.clientSecretPlaceholder')"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-pink-500"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="flex items-center justify-between">
|
|
|
|
|
- <p class="text-xs text-gray-600">
|
|
|
|
|
- {{ $t('settings.tiktok.getAppHelp') }}
|
|
|
|
|
- <a href="https://developers.tiktok.com/" target="_blank" rel="noopener" class="text-pink-400 hover:text-pink-300 underline">
|
|
|
|
|
- {{ $t('settings.tiktok.devPortal') }}
|
|
|
|
|
- </a>
|
|
|
|
|
- </p>
|
|
|
|
|
- <button
|
|
|
|
|
- @click="saveTikTokApp"
|
|
|
|
|
- :disabled="!tiktokClientKey || !tiktokClientSecret || platformsStore.tiktokLoading"
|
|
|
|
|
- class="px-4 py-1.5 bg-pink-600 hover:bg-pink-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- {{ platformsStore.tiktokLoading ? $t('settings.tiktok.saving') : $t('settings.tiktok.saveApp') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <!-- Step 1: App credentials (configure in Global Settings) -->
|
|
|
|
|
+ <div class="p-5 border-b border-gray-800/60 flex items-center justify-between">
|
|
|
|
|
+ <div v-if="tiktokAppConfigured" class="flex items-center gap-2 text-sm text-green-400">
|
|
|
|
|
+ <span>✓</span>
|
|
|
|
|
+ <span>{{ $t('settings.tiktok.appConfigured') }}</span>
|
|
|
|
|
+ <span class="text-gray-600 font-mono text-xs">({{ platformsStore.tiktokCredentials.clientKey }})</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div v-else class="text-sm text-gray-500">{{ $t('settings.meta.appNotConfigured') }}</div>
|
|
|
|
|
+ <router-link
|
|
|
|
|
+ to="/global-settings#tiktok-app"
|
|
|
|
|
+ class="text-xs px-2.5 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-gray-200 transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ $t('settings.configureInGlobal') }} →
|
|
|
|
|
+ </router-link>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Step 2: OAuth connect -->
|
|
<!-- Step 2: OAuth connect -->
|
|
@@ -1160,261 +1082,6 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
- AI INTEGRATION — Ollama configuration card
|
|
|
|
|
- ════════════════════════════════════════════════════════════════════ -->
|
|
|
|
|
- <div class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
|
|
|
|
|
-
|
|
|
|
|
- <!-- Header -->
|
|
|
|
|
- <div class="p-5 border-b border-gray-800 flex items-center gap-3">
|
|
|
|
|
- <div class="w-9 h-9 rounded-full bg-violet-700 flex items-center justify-center text-white text-sm font-bold shrink-0">AI</div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <p class="font-semibold">{{ $t('ai.sectionTitle') }}</p>
|
|
|
|
|
- <p class="text-xs text-gray-500 mt-0.5">{{ $t('ai.sectionSubtitle') }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="ml-auto flex items-center gap-2 shrink-0">
|
|
|
|
|
- <span v-if="aiStore.config.provider === 'ollama'" class="text-xs px-2 py-0.5 rounded-full font-medium bg-violet-900/50 text-violet-300 border border-violet-700">
|
|
|
|
|
- {{ $t('ai.active') }}
|
|
|
|
|
- </span>
|
|
|
|
|
- <span v-if="aiConnected !== null" class="text-xs px-2 py-0.5 rounded-full font-medium" :class="aiConnected ? 'bg-green-900/50 text-green-400 border border-green-700' : 'bg-red-900/40 text-red-400 border border-red-800'">
|
|
|
|
|
- {{ aiConnected ? $t('ai.connected') : $t('ai.connectionFailed') }}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="p-5 space-y-4">
|
|
|
|
|
-
|
|
|
|
|
- <!-- Endpoint -->
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.endpointLabel') }}</label>
|
|
|
|
|
- <div class="flex gap-2">
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="aiEndpoint"
|
|
|
|
|
- type="text"
|
|
|
|
|
- :placeholder="$t('ai.endpointPlaceholder')"
|
|
|
|
|
- class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-violet-500"
|
|
|
|
|
- />
|
|
|
|
|
- <button
|
|
|
|
|
- @click="testAiConnection"
|
|
|
|
|
- :disabled="aiStore.modelsLoading || !aiEndpoint"
|
|
|
|
|
- class="px-3 py-2 bg-gray-700 hover:bg-gray-600 disabled:opacity-40 border border-gray-600 rounded-lg text-xs font-medium transition-colors whitespace-nowrap"
|
|
|
|
|
- >
|
|
|
|
|
- {{ aiStore.modelsLoading ? $t('ai.testing') : $t('ai.testConnection') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <p class="text-xs text-gray-600 mt-1">{{ $t('ai.endpointHint') }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Model selector -->
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.modelLabel') }}</label>
|
|
|
|
|
- <select
|
|
|
|
|
- v-model="aiModel"
|
|
|
|
|
- :disabled="!aiModels.length"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-violet-500 disabled:opacity-40"
|
|
|
|
|
- >
|
|
|
|
|
- <option value="">{{ $t('ai.modelPlaceholder') }}</option>
|
|
|
|
|
- <option v-for="m in aiModels" :key="m" :value="m">{{ m }}</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- <p v-if="aiConnected === false" class="text-xs text-red-400 mt-1">{{ $t('ai.noModels') }}</p>
|
|
|
|
|
- <p v-else-if="aiModels.length" class="text-xs text-gray-600 mt-1">
|
|
|
|
|
- {{ $t('ai.modelsAvailable', aiModels.length) }}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Vision model -->
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.visionModelLabel') }}</label>
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="aiVisionModel"
|
|
|
|
|
- type="text"
|
|
|
|
|
- :placeholder="$t('ai.visionModelPlaceholder')"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-violet-500"
|
|
|
|
|
- />
|
|
|
|
|
- <p class="text-xs text-gray-600 mt-1">{{ $t('ai.visionModelHint') }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Save -->
|
|
|
|
|
- <div class="flex items-center justify-end gap-3">
|
|
|
|
|
- <span v-if="aiSaved" class="text-xs text-green-400">{{ $t('ai.saved') }}</span>
|
|
|
|
|
- <button
|
|
|
|
|
- @click="saveAiConfig"
|
|
|
|
|
- :disabled="aiStore.saving || !aiEndpoint"
|
|
|
|
|
- class="px-4 py-2 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- {{ aiStore.saving ? $t('ai.saving') : $t('ai.saveConfig') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
- AI PROVIDERS — OpenAI, Groq, Gemini cards
|
|
|
|
|
- ════════════════════════════════════════════════════════════════════ -->
|
|
|
|
|
- <template v-for="providerName in ['openai', 'groq', 'gemini']" :key="providerName">
|
|
|
|
|
- <div class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
|
|
|
|
|
-
|
|
|
|
|
- <!-- Header -->
|
|
|
|
|
- <div class="p-5 border-b border-gray-800 flex items-center gap-3">
|
|
|
|
|
- <div
|
|
|
|
|
- class="w-9 h-9 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0"
|
|
|
|
|
- :class="providerName === 'openai' ? 'bg-emerald-700' : providerName === 'groq' ? 'bg-orange-700' : 'bg-blue-700'"
|
|
|
|
|
- >
|
|
|
|
|
- {{ providerName === 'openai' ? 'OAI' : providerName === 'groq' ? 'GRQ' : 'GEM' }}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <p class="font-semibold">{{ $t(`ai.${providerName}.sectionTitle`) }}</p>
|
|
|
|
|
- <p class="text-xs text-gray-500 mt-0.5">{{ $t(`ai.${providerName}.sectionSubtitle`) }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="ml-auto flex items-center gap-2 shrink-0">
|
|
|
|
|
- <span v-if="aiStore.config.provider === providerName" class="text-xs px-2 py-0.5 rounded-full font-medium bg-violet-900/50 text-violet-300 border border-violet-700">
|
|
|
|
|
- {{ $t('ai.active') }}
|
|
|
|
|
- </span>
|
|
|
|
|
- <span v-else-if="getProvider(providerName)?.configured" class="text-xs px-2 py-0.5 rounded-full font-medium bg-green-900/50 text-green-400 border border-green-700">
|
|
|
|
|
- ✓ {{ $t('ai.apiKeyConfigured') }}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="p-5 space-y-4">
|
|
|
|
|
-
|
|
|
|
|
- <!-- Configured state -->
|
|
|
|
|
- <div v-if="getProvider(providerName)?.configured && !providerForms[providerName].editing">
|
|
|
|
|
- <div class="flex items-center justify-between text-sm">
|
|
|
|
|
- <div class="space-y-1">
|
|
|
|
|
- <p class="text-xs text-gray-400">{{ $t('ai.apiKeyLabel') }}: <span class="font-mono text-gray-300">{{ getProvider(providerName)?.apiKeyHint }}</span></p>
|
|
|
|
|
- <p v-if="providerForms[providerName].saved" class="text-xs text-green-400">{{ $t('ai.providerSaved') }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="flex gap-2">
|
|
|
|
|
- <button @click="providerForms[providerName].editing = true" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">
|
|
|
|
|
- Edit
|
|
|
|
|
- </button>
|
|
|
|
|
- <button
|
|
|
|
|
- v-if="aiStore.config.provider !== providerName"
|
|
|
|
|
- @click="setActiveProvider(providerName)"
|
|
|
|
|
- :disabled="aiStore.saving"
|
|
|
|
|
- class="text-xs px-2.5 py-1 bg-violet-700 hover:bg-violet-600 disabled:opacity-40 rounded-md text-white transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- {{ $t('ai.setActive') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- <button @click="disconnectCloudProvider(providerName)" class="text-xs px-2.5 py-1 bg-red-900/40 hover:bg-red-900/60 border border-red-800 rounded-md text-red-400 hover:text-red-300 transition-colors">
|
|
|
|
|
- {{ $t('ai.disconnect') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Model selector for configured provider -->
|
|
|
|
|
- <div class="mt-3">
|
|
|
|
|
- <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.modelLabel') }}</label>
|
|
|
|
|
- <select
|
|
|
|
|
- v-model="providerForms[providerName].model"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-violet-500"
|
|
|
|
|
- >
|
|
|
|
|
- <option v-for="m in (PROVIDER_MODELS[providerName] || [])" :key="m" :value="m">{{ m }}</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="flex justify-end mt-3">
|
|
|
|
|
- <button
|
|
|
|
|
- @click="saveCloudProvider(providerName, aiStore.config.provider === providerName)"
|
|
|
|
|
- :disabled="providerForms[providerName].saving"
|
|
|
|
|
- class="px-4 py-2 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- {{ providerForms[providerName].saving ? $t('ai.saving') : $t('ai.saveProvider') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Unconfigured / editing state -->
|
|
|
|
|
- <div v-else class="space-y-3">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.apiKeyLabel') }}</label>
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="providerForms[providerName].apiKey"
|
|
|
|
|
- type="password"
|
|
|
|
|
- :placeholder="$t('ai.apiKeyPlaceholder')"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-violet-500"
|
|
|
|
|
- />
|
|
|
|
|
- <p class="text-xs text-gray-600 mt-1">{{ $t(`ai.${providerName}.getKeyHint`) }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.modelLabel') }}</label>
|
|
|
|
|
- <select
|
|
|
|
|
- v-model="providerForms[providerName].model"
|
|
|
|
|
- class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-violet-500"
|
|
|
|
|
- >
|
|
|
|
|
- <option v-for="m in (PROVIDER_MODELS[providerName] || [])" :key="m" :value="m">{{ m }}</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="flex items-center justify-end gap-2">
|
|
|
|
|
- <button v-if="providerForms[providerName].editing" @click="providerForms[providerName].editing = false" class="text-xs px-3 py-2 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 transition-colors">
|
|
|
|
|
- Cancel
|
|
|
|
|
- </button>
|
|
|
|
|
- <button
|
|
|
|
|
- @click="saveCloudProvider(providerName, false)"
|
|
|
|
|
- :disabled="providerForms[providerName].saving || !providerForms[providerName].apiKey"
|
|
|
|
|
- class="px-3 py-2 bg-gray-700 hover:bg-gray-600 disabled:opacity-40 border border-gray-600 rounded-lg text-xs font-medium transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- {{ providerForms[providerName].saving ? $t('ai.saving') : $t('ai.saveProvider') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- <button
|
|
|
|
|
- @click="saveCloudProvider(providerName, true)"
|
|
|
|
|
- :disabled="providerForms[providerName].saving || !providerForms[providerName].apiKey"
|
|
|
|
|
- class="px-4 py-2 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- {{ providerForms[providerName].saving ? $t('ai.saving') : $t('ai.connectAndActivate') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
-
|
|
|
|
|
- <!-- ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
- GOOGLE PLACES — local competitor discovery
|
|
|
|
|
- ════════════════════════════════════════════════════════════════════ -->
|
|
|
|
|
- <div class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
|
|
|
|
|
- <div class="p-5 border-b border-gray-800 flex items-center gap-3">
|
|
|
|
|
- <div class="w-9 h-9 rounded-full bg-green-700 flex items-center justify-center shrink-0">
|
|
|
|
|
- <i class="fa-solid fa-location-dot text-white text-sm"></i>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <p class="font-semibold">{{ $t('settings.googlePlaces.sectionTitle') }}</p>
|
|
|
|
|
- <p class="text-xs text-gray-500 mt-0.5">{{ $t('settings.googlePlaces.sectionSubtitle') }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="p-5 space-y-4">
|
|
|
|
|
- <div v-if="placesConfigured" class="flex items-center justify-between">
|
|
|
|
|
- <span class="text-sm text-gray-300">{{ $t('settings.googlePlaces.keyConfigured', { hint: placesKeyHint }) }}</span>
|
|
|
|
|
- <button @click="removePlacesKey" class="text-xs px-3 py-1.5 border border-red-800/60 text-red-400 hover:bg-red-900/20 rounded-lg transition-colors">
|
|
|
|
|
- {{ $t('settings.googlePlaces.disconnect') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div v-else class="space-y-3">
|
|
|
|
|
- <p class="text-xs text-gray-500">{{ $t('settings.googlePlaces.getKeyHint') }}</p>
|
|
|
|
|
- <div class="flex gap-2">
|
|
|
|
|
- <input
|
|
|
|
|
- v-model="placesApiKey"
|
|
|
|
|
- type="password"
|
|
|
|
|
- :placeholder="$t('settings.googlePlaces.keyPlaceholder')"
|
|
|
|
|
- class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-green-500"
|
|
|
|
|
- />
|
|
|
|
|
- <button
|
|
|
|
|
- @click="savePlacesKey"
|
|
|
|
|
- :disabled="!placesApiKey.trim() || placesSaving"
|
|
|
|
|
- class="px-4 py-2 bg-green-700 hover:bg-green-600 disabled:opacity-40 rounded-lg text-sm font-medium text-white transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- {{ placesSaving ? $t('settings.googlePlaces.saving') : $t('settings.googlePlaces.save') }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════════════
|
|
<!-- ═══════════════════════════════════════════════════════════════════
|
|
|
WORKSPACE MANAGEMENT
|
|
WORKSPACE MANAGEMENT
|
|
|
════════════════════════════════════════════════════════════════════ -->
|
|
════════════════════════════════════════════════════════════════════ -->
|
|
@@ -1522,7 +1189,6 @@ import { useRoute } from 'vue-router'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
import axios from 'axios'
|
|
import axios from 'axios'
|
|
|
import { usePlatformsStore, PLATFORM_META } from '../stores/platforms'
|
|
import { usePlatformsStore, PLATFORM_META } from '../stores/platforms'
|
|
|
-import { useAiStore, PROVIDER_MODELS } from '../stores/ai'
|
|
|
|
|
import { useHashtagStore, type HashtagGroup } from '../stores/hashtags'
|
|
import { useHashtagStore, type HashtagGroup } from '../stores/hashtags'
|
|
|
import { useWorkspaceStore, type Workspace } from '../stores/workspace'
|
|
import { useWorkspaceStore, type Workspace } from '../stores/workspace'
|
|
|
import { COMMON_TIMEZONES } from '../utils/timezone'
|
|
import { COMMON_TIMEZONES } from '../utils/timezone'
|
|
@@ -1533,7 +1199,6 @@ const { t } = useI18n()
|
|
|
const route = useRoute()
|
|
const route = useRoute()
|
|
|
const router = useRouter()
|
|
const router = useRouter()
|
|
|
const platformsStore = usePlatformsStore()
|
|
const platformsStore = usePlatformsStore()
|
|
|
-const aiStore = useAiStore()
|
|
|
|
|
const hashtagStore = useHashtagStore()
|
|
const hashtagStore = useHashtagStore()
|
|
|
const workspaceStore = useWorkspaceStore()
|
|
const workspaceStore = useWorkspaceStore()
|
|
|
|
|
|
|
@@ -1608,20 +1273,8 @@ async function switchTo(id: string) {
|
|
|
|
|
|
|
|
// ─── App credential form state ──────────────────────────────────────────────
|
|
// ─── App credential form state ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
-const appId = ref('')
|
|
|
|
|
-const appSecret = ref('')
|
|
|
|
|
-const editingApp = ref(false)
|
|
|
|
|
-
|
|
|
|
|
const metaAppConfigured = computed(() => platformsStore.metaCredentials.configured)
|
|
const metaAppConfigured = computed(() => platformsStore.metaCredentials.configured)
|
|
|
|
|
|
|
|
-async function saveApp() {
|
|
|
|
|
- await platformsStore.saveMetaApp(appId.value, appSecret.value)
|
|
|
|
|
- if (!platformsStore.metaError) {
|
|
|
|
|
- editingApp.value = false
|
|
|
|
|
- appSecret.value = ''
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// ─── Connected platforms derived from statuses ───────────────────────────────
|
|
// ─── Connected platforms derived from statuses ───────────────────────────────
|
|
|
|
|
|
|
|
const fbStatus = computed(() => platformsStore.getStatus('facebook'))
|
|
const fbStatus = computed(() => platformsStore.getStatus('facebook'))
|
|
@@ -1668,9 +1321,6 @@ const oauthError = ref<string | null>(null)
|
|
|
|
|
|
|
|
// ─── Pinterest ────────────────────────────────────────────────────────────────
|
|
// ─── Pinterest ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
-const pinterestClientId = ref('')
|
|
|
|
|
-const pinterestClientSecret = ref('')
|
|
|
|
|
-const editingPinterestApp = ref(false)
|
|
|
|
|
const pinterestOauthError = ref<string | null>(null)
|
|
const pinterestOauthError = ref<string | null>(null)
|
|
|
const pinterestBoardsSaved = ref(false)
|
|
const pinterestBoardsSaved = ref(false)
|
|
|
const selectedBoardIds = ref<string[]>([])
|
|
const selectedBoardIds = ref<string[]>([])
|
|
@@ -1678,14 +1328,6 @@ const selectedBoardIds = ref<string[]>([])
|
|
|
const pinterestAppConfigured = computed(() => platformsStore.pinterestCredentials.configured)
|
|
const pinterestAppConfigured = computed(() => platformsStore.pinterestCredentials.configured)
|
|
|
const pinterestConnected = computed(() => platformsStore.allPinterestBoards.length > 0)
|
|
const pinterestConnected = computed(() => platformsStore.allPinterestBoards.length > 0)
|
|
|
|
|
|
|
|
-async function savePinterestApp() {
|
|
|
|
|
- await platformsStore.savePinterestApp(pinterestClientId.value, pinterestClientSecret.value)
|
|
|
|
|
- if (!platformsStore.pinterestError) {
|
|
|
|
|
- editingPinterestApp.value = false
|
|
|
|
|
- pinterestClientSecret.value = ''
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
async function savePinterestBoards() {
|
|
async function savePinterestBoards() {
|
|
|
await platformsStore.savePinterestBoards(selectedBoardIds.value)
|
|
await platformsStore.savePinterestBoards(selectedBoardIds.value)
|
|
|
if (!platformsStore.pinterestError) {
|
|
if (!platformsStore.pinterestError) {
|
|
@@ -1702,27 +1344,39 @@ function confirmPinterestDisconnect() {
|
|
|
|
|
|
|
|
// ─── TikTok ──────────────────────────────────────────────────────────────────
|
|
// ─── TikTok ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
-const tiktokClientKey = ref('')
|
|
|
|
|
-const tiktokClientSecret = ref('')
|
|
|
|
|
-const editingTikTokApp = ref(false)
|
|
|
|
|
const tiktokOauthError = ref<string | null>(null)
|
|
const tiktokOauthError = ref<string | null>(null)
|
|
|
|
|
|
|
|
const tiktokAppConfigured = computed(() => platformsStore.tiktokCredentials.configured)
|
|
const tiktokAppConfigured = computed(() => platformsStore.tiktokCredentials.configured)
|
|
|
|
|
|
|
|
-async function saveTikTokApp() {
|
|
|
|
|
- await platformsStore.saveTikTokApp(tiktokClientKey.value, tiktokClientSecret.value)
|
|
|
|
|
- if (!platformsStore.tiktokError) {
|
|
|
|
|
- editingTikTokApp.value = false
|
|
|
|
|
- tiktokClientSecret.value = ''
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function confirmTikTokDisconnect() {
|
|
function confirmTikTokDisconnect() {
|
|
|
if (window.confirm(t('settings.tiktok.disconnectConfirm'))) {
|
|
if (window.confirm(t('settings.tiktok.disconnectConfirm'))) {
|
|
|
platformsStore.disconnectTikTok().then(loadMetaConnections)
|
|
platformsStore.disconnectTikTok().then(loadMetaConnections)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ─── Workspace page/account selection ────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+const workspaceFbPageIds = ref<string[]>([])
|
|
|
|
|
+const workspaceIgAccountIds = ref<string[]>([])
|
|
|
|
|
+const fbPagesSaved = ref(false)
|
|
|
|
|
+const igAccountsSaved = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+async function saveFbPageSelection() {
|
|
|
|
|
+ await platformsStore.saveFbPageSelection(workspaceFbPageIds.value)
|
|
|
|
|
+ if (!platformsStore.metaError) {
|
|
|
|
|
+ fbPagesSaved.value = true
|
|
|
|
|
+ setTimeout(() => { fbPagesSaved.value = false }, 2500)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function saveIgAccountSelection() {
|
|
|
|
|
+ await platformsStore.saveIgAccountSelection(workspaceIgAccountIds.value)
|
|
|
|
|
+ if (!platformsStore.metaError) {
|
|
|
|
|
+ igAccountsSaved.value = true
|
|
|
|
|
+ setTimeout(() => { igAccountsSaved.value = false }, 2500)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ─── Hashtag Groups ──────────────────────────────────────────────────────────
|
|
// ─── Hashtag Groups ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
const addingHashtagGroup = ref(false)
|
|
const addingHashtagGroup = ref(false)
|
|
@@ -1949,41 +1603,6 @@ async function toggleProfile(key: string) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ─── Google Places ────────────────────────────────────────────────────────────
|
|
|
|
|
-
|
|
|
|
|
-const placesApiKey = ref('')
|
|
|
|
|
-const placesSaving = ref(false)
|
|
|
|
|
-const placesConfigured = ref(false)
|
|
|
|
|
-const placesKeyHint = ref<string | null>(null)
|
|
|
|
|
-
|
|
|
|
|
-async function loadPlacesConfig() {
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await axios.get('/api/credentials/google-places')
|
|
|
|
|
- placesConfigured.value = res.data.configured
|
|
|
|
|
- placesKeyHint.value = res.data.keyHint
|
|
|
|
|
- } catch { /* not configured */ }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function savePlacesKey() {
|
|
|
|
|
- if (!placesApiKey.value.trim()) return
|
|
|
|
|
- placesSaving.value = true
|
|
|
|
|
- try {
|
|
|
|
|
- await axios.post('/api/credentials/google-places', { apiKey: placesApiKey.value.trim() })
|
|
|
|
|
- placesConfigured.value = true
|
|
|
|
|
- placesKeyHint.value = `****${placesApiKey.value.trim().slice(-4)}`
|
|
|
|
|
- placesApiKey.value = ''
|
|
|
|
|
- } finally {
|
|
|
|
|
- placesSaving.value = false
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function removePlacesKey() {
|
|
|
|
|
- if (!confirm(t('settings.googlePlaces.disconnectConfirm'))) return
|
|
|
|
|
- await axios.delete('/api/credentials/google-places')
|
|
|
|
|
- placesConfigured.value = false
|
|
|
|
|
- placesKeyHint.value = null
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
async function runFiveForces(key: string) {
|
|
async function runFiveForces(key: string) {
|
|
|
fiveForcesRunning.value = key
|
|
fiveForcesRunning.value = key
|
|
|
try {
|
|
try {
|
|
@@ -2033,88 +1652,6 @@ async function saveProfile(key: string) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ─── AI Configuration ─────────────────────────────────────────────────────────
|
|
|
|
|
-
|
|
|
|
|
-const aiEndpoint = ref('')
|
|
|
|
|
-const aiModel = ref('')
|
|
|
|
|
-const aiVisionModel = ref('')
|
|
|
|
|
-const aiModels = computed(() => aiStore.models)
|
|
|
|
|
-const aiConnected = ref<boolean | null>(null)
|
|
|
|
|
-const aiSaved = ref(false)
|
|
|
|
|
-
|
|
|
|
|
-async function testAiConnection() {
|
|
|
|
|
- const ok = await aiStore.fetchModels(aiEndpoint.value)
|
|
|
|
|
- aiConnected.value = ok
|
|
|
|
|
- if (ok && !aiModel.value && aiStore.models.length) {
|
|
|
|
|
- aiModel.value = aiStore.models[0]
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function saveAiConfig() {
|
|
|
|
|
- const ok = await aiStore.saveProvider('ollama', { endpoint: aiEndpoint.value, model: aiModel.value, visionModel: aiVisionModel.value, setActive: true })
|
|
|
|
|
- if (ok) {
|
|
|
|
|
- aiSaved.value = true
|
|
|
|
|
- setTimeout(() => { aiSaved.value = false }, 2500)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ─── Cloud AI providers (OpenAI, Groq, Gemini) ───────────────────────────────
|
|
|
|
|
-
|
|
|
|
|
-interface ProviderFormState {
|
|
|
|
|
- apiKey: string
|
|
|
|
|
- model: string
|
|
|
|
|
- editing: boolean
|
|
|
|
|
- saving: boolean
|
|
|
|
|
- saved: boolean
|
|
|
|
|
- testResult: boolean | null
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function makeProviderState(): ProviderFormState {
|
|
|
|
|
- return { apiKey: '', model: '', editing: false, saving: false, saved: false, testResult: null }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const providerForms = ref<Record<string, ProviderFormState>>({
|
|
|
|
|
- openai: makeProviderState(),
|
|
|
|
|
- groq: makeProviderState(),
|
|
|
|
|
- gemini: makeProviderState(),
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-function getProvider(name: string) {
|
|
|
|
|
- return aiStore.providers.find((p) => p.name === name)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function saveCloudProvider(name: string, setActive = false) {
|
|
|
|
|
- const form = providerForms.value[name]
|
|
|
|
|
- form.saving = true
|
|
|
|
|
- const ok = await aiStore.saveProvider(name, { apiKey: form.apiKey || undefined, model: form.model || undefined, setActive })
|
|
|
|
|
- form.saving = false
|
|
|
|
|
- if (ok) {
|
|
|
|
|
- form.saved = true
|
|
|
|
|
- form.editing = false
|
|
|
|
|
- form.apiKey = ''
|
|
|
|
|
- setTimeout(() => { form.saved = false }, 2500)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function setActiveProvider(name: string) {
|
|
|
|
|
- const provider = getProvider(name)
|
|
|
|
|
- if (!provider?.configured) return
|
|
|
|
|
- await aiStore.saveProvider(name, { setActive: true })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function disconnectCloudProvider(name: string) {
|
|
|
|
|
- if (!confirm(t('ai.disconnectConfirm'))) return
|
|
|
|
|
- await aiStore.deleteProvider(name)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function seedProviderForms() {
|
|
|
|
|
- for (const p of aiStore.providers) {
|
|
|
|
|
- if (p.name === 'ollama') continue
|
|
|
|
|
- const form = providerForms.value[p.name]
|
|
|
|
|
- if (form) form.model = p.model || ''
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// ─── On mount ────────────────────────────────────────────────────────────────
|
|
// ─── On mount ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
@@ -2149,19 +1686,14 @@ onMounted(async () => {
|
|
|
platformsStore.fetchTikTokCredentials(),
|
|
platformsStore.fetchTikTokCredentials(),
|
|
|
loadMetaConnections(),
|
|
loadMetaConnections(),
|
|
|
platformsStore.fetchTokenExpiry(),
|
|
platformsStore.fetchTokenExpiry(),
|
|
|
- aiStore.fetchConfig(),
|
|
|
|
|
- aiStore.fetchProviders(),
|
|
|
|
|
hashtagStore.fetchGroups(),
|
|
hashtagStore.fetchGroups(),
|
|
|
- loadPlacesConfig(),
|
|
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
// Seed board checkboxes from current selection
|
|
// Seed board checkboxes from current selection
|
|
|
selectedBoardIds.value = platformsStore.allPinterestBoards.filter((b) => b.selected).map((b) => b.id)
|
|
selectedBoardIds.value = platformsStore.allPinterestBoards.filter((b) => b.selected).map((b) => b.id)
|
|
|
|
|
|
|
|
- // Seed local form from fetched config
|
|
|
|
|
- aiEndpoint.value = aiStore.config.endpoint
|
|
|
|
|
- aiModel.value = aiStore.config.model
|
|
|
|
|
- aiVisionModel.value = aiStore.config.visionModel
|
|
|
|
|
- seedProviderForms()
|
|
|
|
|
|
|
+ // Seed workspace page/account selection from current saved state
|
|
|
|
|
+ workspaceFbPageIds.value = platformsStore.allFbPages.filter((p) => p.selected).map((p) => p.id)
|
|
|
|
|
+ workspaceIgAccountIds.value = platformsStore.allIgAccounts.filter((a) => a.selected).map((a) => a.id)
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|