[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