[update] added validation for article data in NewsList.vue
, removed unused Cypress config, expanded LLM models in example.env
, adjusted context size and max article length in backend configuration, and updated workspace naming in yarn.lock
This commit is contained in:
@@ -8,11 +8,11 @@ MIN_CRON_HOURS = float(os.getenv("MIN_CRON_HOURS", 0.5))
|
|||||||
DEFAULT_CRON_HOURS = float(os.getenv("CRON_HOURS", MIN_CRON_HOURS))
|
DEFAULT_CRON_HOURS = float(os.getenv("CRON_HOURS", MIN_CRON_HOURS))
|
||||||
CRON_HOURS = max(MIN_CRON_HOURS, DEFAULT_CRON_HOURS)
|
CRON_HOURS = max(MIN_CRON_HOURS, DEFAULT_CRON_HOURS)
|
||||||
SYNC_COOLDOWN_MINUTES = int(os.getenv("SYNC_COOLDOWN_MINUTES", 30))
|
SYNC_COOLDOWN_MINUTES = int(os.getenv("SYNC_COOLDOWN_MINUTES", 30))
|
||||||
LLM_MODEL = os.getenv("LLM_MODEL", "mistral-nemo:12b")
|
LLM_MODEL = os.getenv("LLM_MODEL", "phi3:3.8b-mini-128k-instruct-q4_0")
|
||||||
LLM_TIMEOUT_SECONDS = int(os.getenv("LLM_TIMEOUT_SECONDS", 180))
|
LLM_TIMEOUT_SECONDS = int(os.getenv("LLM_TIMEOUT_SECONDS", 180))
|
||||||
OLLAMA_API_TIMEOUT_SECONDS = int(os.getenv("OLLAMA_API_TIMEOUT_SECONDS", 10))
|
OLLAMA_API_TIMEOUT_SECONDS = int(os.getenv("OLLAMA_API_TIMEOUT_SECONDS", 10))
|
||||||
ARTICLE_FETCH_TIMEOUT = int(os.getenv("ARTICLE_FETCH_TIMEOUT", 30))
|
ARTICLE_FETCH_TIMEOUT = int(os.getenv("ARTICLE_FETCH_TIMEOUT", 30))
|
||||||
MAX_ARTICLE_LENGTH = int(os.getenv("MAX_ARTICLE_LENGTH", 10_000))
|
MAX_ARTICLE_LENGTH = int(os.getenv("MAX_ARTICLE_LENGTH", 40_000))
|
||||||
|
|
||||||
frontend_path = os.path.join(
|
frontend_path = os.path.join(
|
||||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
||||||
|
@@ -209,7 +209,7 @@ class NewsFetcher:
|
|||||||
"format": "json",
|
"format": "json",
|
||||||
"options": {
|
"options": {
|
||||||
"num_gpu": 1, # Force GPU usage
|
"num_gpu": 1, # Force GPU usage
|
||||||
"num_ctx": 128_000, # Context size
|
"num_ctx": 64_000, # Context size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,10 +11,11 @@ MIN_CRON_HOURS=0.5
|
|||||||
SYNC_COOLDOWN_MINUTES=30
|
SYNC_COOLDOWN_MINUTES=30
|
||||||
|
|
||||||
# LLM model to use for summarization
|
# LLM model to use for summarization
|
||||||
LLM_MODEL=qwen2:7b-instruct-q4_K_M
|
LLM_MODEL=qwen2:7b-instruct-q4_K_M # ca 7-9GB (typisch 8GB)
|
||||||
LLM_MODEL=phi3:3.8b-mini-128k-instruct-q4_0
|
LLM_MODEL=phi3:3.8b-mini-128k-instruct-q4_0 # ca 6-8GB (langer kontext)
|
||||||
LLM_MODEL=mistral-nemo:12b
|
LLM_MODEL=mistral-nemo:12b # ca 16-24+GB
|
||||||
LLM_MODEL=cnjack/mistral-samll-3.1:24b-it-q4_K_S
|
LLM_MODEL=cnjack/mistral-samll-3.1:24b-it-q4_K_S # ca 22GB
|
||||||
|
LLM_MODEL=yarn-mistral:7b-64k-q4_K_M # ca 11GB
|
||||||
|
|
||||||
# Timeout in seconds for LLM requests
|
# Timeout in seconds for LLM requests
|
||||||
LLM_TIMEOUT_SECONDS=180
|
LLM_TIMEOUT_SECONDS=180
|
||||||
|
Binary file not shown.
@@ -1,8 +0,0 @@
|
|||||||
import { defineConfig } from 'cypress'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
e2e: {
|
|
||||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
|
||||||
baseUrl: 'http://localhost:4173',
|
|
||||||
},
|
|
||||||
})
|
|
@@ -14,75 +14,77 @@
|
|||||||
|
|
||||||
<!-- Articles Grid -->
|
<!-- Articles Grid -->
|
||||||
<div v-else class="grid gap-4 sm:gap-6 md:grid-cols-2 xl:grid-cols-3">
|
<div v-else class="grid gap-4 sm:gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||||
<article
|
<template v-for="article in news.articles"
|
||||||
v-for="article in news.articles"
|
:key="article.id">
|
||||||
:key="article.id"
|
<article
|
||||||
class="flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md dark:hover:shadow-lg dark:hover:shadow-gray-800/50 transition-all duration-200 overflow-hidden group"
|
v-if="isValidArticleContent(article)"
|
||||||
>
|
class="flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md dark:hover:shadow-lg dark:hover:shadow-gray-800/50 transition-all duration-200 overflow-hidden group"
|
||||||
<!-- Article Header -->
|
>
|
||||||
<div class="flex-1 p-4 sm:p-6">
|
<!-- Article Header -->
|
||||||
<div class="flex items-start justify-between mb-3">
|
<div class="flex-1 p-4 sm:p-6">
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">
|
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">
|
||||||
{{ article.country }}
|
{{ article.country }}
|
||||||
</span>
|
</span>
|
||||||
<time
|
<time
|
||||||
:datetime="new Date(article.published * 1000).toISOString()"
|
:datetime="new Date(article.published * 1000).toISOString()"
|
||||||
:title="new Date(article.published * 1000).toLocaleString(userLocale.value, {
|
:title="new Date(article.published * 1000).toLocaleString(userLocale, {
|
||||||
dateStyle: 'full',
|
dateStyle: 'full',
|
||||||
timeStyle: 'long'
|
timeStyle: 'long'
|
||||||
})"
|
})"
|
||||||
class="text-xs text-gray-500 flex-shrink-0 ml-2 cursor-help hover:text-green-600 dark:hover:text-green-400 transition-colors relative group"
|
class="text-xs text-gray-500 flex-shrink-0 ml-2 cursor-help hover:text-green-600 dark:hover:text-green-400 transition-colors relative group"
|
||||||
>
|
>
|
||||||
{{ formatDate(article.published) }}
|
{{ formatDate(article.published) }}
|
||||||
</time>
|
</time>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<h3
|
||||||
|
class="text-base sm:text-lg font-semibold text-gray-900 dark:text-white mb-3 line-clamp-2 group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors">
|
||||||
|
<a :href="article.url" target="_blank" rel="noopener noreferrer">
|
||||||
|
{{ article.title }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Summary -->
|
||||||
|
<p
|
||||||
|
class="text-sm sm:text-base text-gray-700 dark:text-gray-300 line-clamp-5 leading-relaxed">
|
||||||
|
{{ article.summary }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Article Footer -->
|
||||||
<h3
|
<div
|
||||||
class="text-base sm:text-lg font-semibold text-gray-900 dark:text-white mb-3 line-clamp-2 group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors">
|
class="flex justify-between items-center gap-4 p-4 sm:p-6">
|
||||||
<a :href="article.url" target="_blank" rel="noopener noreferrer">
|
<button
|
||||||
{{ article.title }}
|
@click="openModal(article)"
|
||||||
|
class="flex-1 inline-flex items-center justify-center cursor-pointer px-4 py-2 text-sm font-medium rounded-lg bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50 transition-colors"
|
||||||
|
>
|
||||||
|
Full summary
|
||||||
|
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="article.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="flex-1 inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50 transition-colors"
|
||||||
|
>
|
||||||
|
Full article
|
||||||
|
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</div>
|
||||||
|
</article>
|
||||||
<!-- Summary -->
|
</template>
|
||||||
<p
|
|
||||||
class="text-sm sm:text-base text-gray-700 dark:text-gray-300 line-clamp-5 leading-relaxed">
|
|
||||||
{{ article.summary }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Article Footer -->
|
|
||||||
<div
|
|
||||||
class="flex justify-between items-center gap-4 p-4 sm:p-6">
|
|
||||||
<button
|
|
||||||
@click="openModal(article)"
|
|
||||||
class="flex-1 inline-flex items-center justify-center cursor-pointer px-4 py-2 text-sm font-medium rounded-lg bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50 transition-colors"
|
|
||||||
>
|
|
||||||
Full summary
|
|
||||||
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a
|
|
||||||
:href="article.url"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="flex-1 inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50 transition-colors"
|
|
||||||
>
|
|
||||||
Full article
|
|
||||||
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State & Load More Trigger -->
|
<!-- Loading State & Load More Trigger -->
|
||||||
@@ -100,9 +102,9 @@
|
|||||||
|
|
||||||
<!-- Article Modal -->
|
<!-- Article Modal -->
|
||||||
<ArticleModal
|
<ArticleModal
|
||||||
:is-open="isModalOpen"
|
:is-open="isModalOpen"
|
||||||
:article="selectedArticle"
|
:article="selectedArticle"
|
||||||
@close="closeModal"
|
@close="closeModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -129,17 +131,48 @@ const loadMoreArticles = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Article {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
url: string;
|
||||||
|
published: number;
|
||||||
|
country: string;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INVALID_MARKERS = ['---', '...', '…', 'Title', 'Summary', 'Titel', 'Zusammenfassung'] as const;
|
||||||
|
const REQUIRED_TEXT_FIELDS = ['title', 'summary', 'url'] as const;
|
||||||
|
|
||||||
|
const isValidArticleContent = (article: Article): boolean => {
|
||||||
|
const hasEmptyRequiredFields = REQUIRED_TEXT_FIELDS.some(
|
||||||
|
field => article[field as keyof Pick<Article, typeof REQUIRED_TEXT_FIELDS[number]>].length === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasEmptyRequiredFields) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasInvalidMarkers = REQUIRED_TEXT_FIELDS.some(field =>
|
||||||
|
INVALID_MARKERS.some(marker =>
|
||||||
|
article[field as keyof Pick<Article, typeof REQUIRED_TEXT_FIELDS[number]>].includes(marker)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return !hasInvalidMarkers;
|
||||||
|
};
|
||||||
|
|
||||||
const observer = ref<IntersectionObserver | null>(null);
|
const observer = ref<IntersectionObserver | null>(null);
|
||||||
const loadMoreTrigger = ref<HTMLElement | null>(null);
|
const loadMoreTrigger = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
observer.value = new IntersectionObserver(
|
observer.value = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (entries[0].isIntersecting) {
|
if (entries[0].isIntersecting) {
|
||||||
loadMoreArticles();
|
loadMoreArticles();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{threshold: 0.5}
|
{threshold: 0.5}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loadMoreTrigger.value) {
|
if (loadMoreTrigger.value) {
|
||||||
|
@@ -6470,9 +6470,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"owly-news-summariser@workspace:.":
|
"owly-news@workspace:.":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "owly-news-summariser@workspace:."
|
resolution: "owly-news@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
"@tailwindcss/vite": "npm:^4.1.11"
|
"@tailwindcss/vite": "npm:^4.1.11"
|
||||||
"@tsconfig/node22": "npm:^22.0.2"
|
"@tsconfig/node22": "npm:^22.0.2"
|
||||||
|
Reference in New Issue
Block a user