feat(briefing): add News Preferences section with topic include/exclude inputs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGrou
|
|||||||
import { usePushStore } from "@/stores/push";
|
import { usePushStore } from "@/stores/push";
|
||||||
import type { User } from "@/types/auth";
|
import type { User } from "@/types/auth";
|
||||||
import PaginationBar from "@/components/PaginationBar.vue";
|
import PaginationBar from "@/components/PaginationBar.vue";
|
||||||
|
import TagInput from "@/components/TagInput.vue";
|
||||||
|
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -268,6 +269,17 @@ const newFeedUrl = ref('');
|
|||||||
const newFeedCategory = ref('');
|
const newFeedCategory = ref('');
|
||||||
const addingFeed = ref(false);
|
const addingFeed = ref(false);
|
||||||
const refreshingFeeds = ref(false);
|
const refreshingFeeds = ref(false);
|
||||||
|
const briefingIncludeTopics = ref<string[]>([]);
|
||||||
|
const briefingExcludeTopics = ref<string[]>([]);
|
||||||
|
|
||||||
|
function _parseTopics(raw: unknown): string[] {
|
||||||
|
try {
|
||||||
|
const val = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||||
|
return Array.isArray(val) ? val.map(String) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadBriefingTab() {
|
async function loadBriefingTab() {
|
||||||
briefingConfig.value = await getBriefingConfig();
|
briefingConfig.value = await getBriefingConfig();
|
||||||
@@ -276,6 +288,25 @@ async function loadBriefingTab() {
|
|||||||
if (!briefingConfig.value.timezone) {
|
if (!briefingConfig.value.timezone) {
|
||||||
briefingConfig.value.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
briefingConfig.value.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
}
|
}
|
||||||
|
const allSettings = await apiGet<Record<string, string>>('/api/settings').catch(() => ({} as Record<string, string>));
|
||||||
|
briefingIncludeTopics.value = _parseTopics(allSettings['briefing_include_topics'] ?? '[]');
|
||||||
|
briefingExcludeTopics.value = _parseTopics(allSettings['briefing_exclude_topics'] ?? '[]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveIncludeTopics(topics: string[]) {
|
||||||
|
briefingIncludeTopics.value = topics;
|
||||||
|
await apiPut('/api/settings', { briefing_include_topics: JSON.stringify(topics) });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveExcludeTopics(topics: string[]) {
|
||||||
|
briefingExcludeTopics.value = topics;
|
||||||
|
await apiPut('/api/settings', { briefing_exclude_topics: JSON.stringify(topics) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const _STANDARD_TOPICS = ['technology', 'science', 'politics', 'business', 'health', 'environment', 'local', 'entertainment', 'sports', 'other'];
|
||||||
|
async function fetchTopicSuggestions(q: string): Promise<string[]> {
|
||||||
|
if (!q) return _STANDARD_TOPICS;
|
||||||
|
return _STANDARD_TOPICS.filter((t) => t.startsWith(q.toLowerCase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function geocodeLocation(key: 'home' | 'work') {
|
async function geocodeLocation(key: 'home' | 'work') {
|
||||||
@@ -1763,6 +1794,41 @@ function formatUserDate(iso: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- News Preferences -->
|
||||||
|
<section class="settings-section full-width">
|
||||||
|
<h2>News Preferences</h2>
|
||||||
|
<p class="section-desc">
|
||||||
|
Tell the briefing what topics you care about. Topics are matched against
|
||||||
|
classified RSS items before each briefing runs.
|
||||||
|
</p>
|
||||||
|
<div class="settings-field">
|
||||||
|
<label class="field-label">Interested in</label>
|
||||||
|
<TagInput
|
||||||
|
:model-value="briefingIncludeTopics"
|
||||||
|
placeholder="e.g. technology, science, local"
|
||||||
|
:fetch-tags="fetchTopicSuggestions"
|
||||||
|
@update:model-value="saveIncludeTopics"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="settings-field" style="margin-top: 0.75rem">
|
||||||
|
<label class="field-label">Not interested in</label>
|
||||||
|
<TagInput
|
||||||
|
:model-value="briefingExcludeTopics"
|
||||||
|
placeholder="e.g. sports, celebrity"
|
||||||
|
:fetch-tags="fetchTopicSuggestions"
|
||||||
|
@update:model-value="saveExcludeTopics"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<details class="topic-vocab-hint" style="margin-top: 0.75rem">
|
||||||
|
<summary style="cursor: pointer; color: var(--color-text-muted); font-size: 0.82rem">Standard topic vocabulary</summary>
|
||||||
|
<p class="field-hint" style="margin-top: 0.35rem">
|
||||||
|
technology · science · politics · business · health · environment ·
|
||||||
|
local · entertainment · sports · other
|
||||||
|
</p>
|
||||||
|
<p class="field-hint">Custom terms are also accepted.</p>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<section class="settings-section full-width">
|
<section class="settings-section full-width">
|
||||||
<h2>Notifications</h2>
|
<h2>Notifications</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user