[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))
|
||||
CRON_HOURS = max(MIN_CRON_HOURS, DEFAULT_CRON_HOURS)
|
||||
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))
|
||||
OLLAMA_API_TIMEOUT_SECONDS = int(os.getenv("OLLAMA_API_TIMEOUT_SECONDS", 10))
|
||||
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(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
||||
|
@@ -209,7 +209,7 @@ class NewsFetcher:
|
||||
"format": "json",
|
||||
"options": {
|
||||
"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
|
||||
|
||||
# LLM model to use for summarization
|
||||
LLM_MODEL=qwen2:7b-instruct-q4_K_M
|
||||
LLM_MODEL=phi3:3.8b-mini-128k-instruct-q4_0
|
||||
LLM_MODEL=mistral-nemo:12b
|
||||
LLM_MODEL=cnjack/mistral-samll-3.1:24b-it-q4_K_S
|
||||
LLM_MODEL=qwen2:7b-instruct-q4_K_M # ca 7-9GB (typisch 8GB)
|
||||
LLM_MODEL=phi3:3.8b-mini-128k-instruct-q4_0 # ca 6-8GB (langer kontext)
|
||||
LLM_MODEL=mistral-nemo:12b # ca 16-24+GB
|
||||
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
|
||||
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 -->
|
||||
<div v-else class="grid gap-4 sm:gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
<article
|
||||
v-for="article in news.articles"
|
||||
:key="article.id"
|
||||
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">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<template v-for="article in news.articles"
|
||||
:key="article.id">
|
||||
<article
|
||||
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">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<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 }}
|
||||
</span>
|
||||
<time
|
||||
:datetime="new Date(article.published * 1000).toISOString()"
|
||||
:title="new Date(article.published * 1000).toLocaleString(userLocale.value, {
|
||||
<time
|
||||
:datetime="new Date(article.published * 1000).toISOString()"
|
||||
:title="new Date(article.published * 1000).toLocaleString(userLocale, {
|
||||
dateStyle: 'full',
|
||||
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"
|
||||
>
|
||||
{{ formatDate(article.published) }}
|
||||
</time>
|
||||
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) }}
|
||||
</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>
|
||||
|
||||
<!-- 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 }}
|
||||
<!-- 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>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Loading State & Load More Trigger -->
|
||||
@@ -100,9 +102,9 @@
|
||||
|
||||
<!-- Article Modal -->
|
||||
<ArticleModal
|
||||
:is-open="isModalOpen"
|
||||
:article="selectedArticle"
|
||||
@close="closeModal"
|
||||
:is-open="isModalOpen"
|
||||
:article="selectedArticle"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</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 loadMoreTrigger = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
observer.value = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
loadMoreArticles();
|
||||
}
|
||||
},
|
||||
{threshold: 0.5}
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
loadMoreArticles();
|
||||
}
|
||||
},
|
||||
{threshold: 0.5}
|
||||
);
|
||||
|
||||
if (loadMoreTrigger.value) {
|
||||
|
@@ -6470,9 +6470,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"owly-news-summariser@workspace:.":
|
||||
"owly-news@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "owly-news-summariser@workspace:."
|
||||
resolution: "owly-news@workspace:."
|
||||
dependencies:
|
||||
"@tailwindcss/vite": "npm:^4.1.11"
|
||||
"@tsconfig/node22": "npm:^22.0.2"
|
||||
|
Reference in New Issue
Block a user