Skip to content
Merged

Dev #20

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/actions/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ export async function validateFeedAction(input: ValidateFeedInput) {

try {
const feedInfo = await parseFeedUrl(normalizedUrl);
if (!feedInfo) {
return {
valid: false,
error: 'Unable to fetch feed information',
};
}
return {
valid: true,
feedInfo: {
Expand Down
20 changes: 14 additions & 6 deletions app/admin/dashboard/components/tabs/LLMConfigTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
useTestLLMConfig,
useDeleteAllEmbeddings,
useUpdateSummarizationConfig,
useUpdateEmbeddingProvider,
} from "@/hooks/queries/use-admin";

// Loading Spinner Component
Expand Down Expand Up @@ -70,6 +71,7 @@ export function LLMConfigTab() {
const testConfig = useTestLLMConfig();
const deleteEmbeddings = useDeleteAllEmbeddings();
const updateSummarizationConfig = useUpdateSummarizationConfig();
const updateEmbeddingProvider = useUpdateEmbeddingProvider();

// Summarization toggle state
const [isSummarizationToggling, setIsSummarizationToggling] = useState(false);
Expand Down Expand Up @@ -135,6 +137,7 @@ export function LLMConfigTab() {
setSaveMessage(null);

try {
// Save LLM config
await updateConfig.mutateAsync({
provider,
apiKey: apiKey || undefined,
Expand All @@ -144,9 +147,14 @@ export function LLMConfigTab() {
digestModel: digestModel || undefined,
});

// Save embedding provider if it changed
if (embeddingConfig && embeddingProvider !== embeddingConfig.provider) {
await updateEmbeddingProvider.mutateAsync(embeddingProvider);
}

setSaveMessage({ type: "success", text: "Configuration saved successfully" });
toast.success("LLM configuration saved");

// Clear API key input
setApiKey("");

Expand Down Expand Up @@ -515,10 +523,10 @@ export function LLMConfigTab() {
<div className="border-t border-purple-300 dark:border-purple-700 pt-4">
<button
onClick={handleDeleteEmbeddings}
disabled={pendingDeleteEmbeddings || deleteEmbeddings.isPending}
disabled={deleteEmbeddings.isPending}
className={`rounded-lg px-6 py-2 font-medium text-white ${
pendingDeleteEmbeddings
? "bg-red-700 hover:bg-red-800"
pendingDeleteEmbeddings
? "bg-red-700 hover:bg-red-800"
: "bg-red-600 hover:bg-red-700"
} disabled:opacity-50`}
>
Expand All @@ -542,10 +550,10 @@ export function LLMConfigTab() {
</button>
<button
onClick={handleSave}
disabled={updateConfig.isPending || testConfig.isPending}
disabled={updateConfig.isPending || testConfig.isPending || updateEmbeddingProvider.isPending}
className="rounded-lg bg-blue-600 px-6 py-2 font-medium text-white hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-600"
>
{updateConfig.isPending ? "Saving..." : "Save Configuration"}
{(updateConfig.isPending || updateEmbeddingProvider.isPending) ? "Saving..." : "Save Configuration"}
</button>
</div>

Expand Down
2 changes: 2 additions & 0 deletions app/api/admin/embeddings/config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const GET = createHandler(
config: {
provider: embeddingConfig.provider,
model: embeddingConfig.model,
modelSource: embeddingConfig.modelSource,
dimensions,
batchSize: embeddingConfig.batchSize,
apiKey: envConfig.apiKey,
Expand All @@ -92,6 +93,7 @@ export const GET = createHandler(
},
autoGenerate: embeddingConfig.autoGenerate,
autoGenerateSource: embeddingConfig.autoGenerateSource,
providerSource: embeddingConfig.providerSource,
envDefault: env.EMBEDDING_AUTO_GENERATE,
usingUserConfig: userId ? openaiTest.success : false,
message: "OpenAI always available - admin controls enable/disable, users provide credentials",
Expand Down
6 changes: 4 additions & 2 deletions app/api/admin/embeddings/provider/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@ export const GET = createHandler(
},
config: {
model: config.model,
modelSource: config.modelSource,
batchSize: config.batchSize,
autoGenerate: config.autoGenerate,
autoGenerateSource: config.autoGenerateSource,
},
usingUserConfig: userId ? openaiTest.success : false,
message: openaiTest.success
? "OpenAI working with provided credentials"
message: openaiTest.success
? "OpenAI working with provided credentials"
: "OpenAI available - users can provide API keys in preferences",
};
},
Expand Down
2 changes: 2 additions & 0 deletions app/api/admin/memory/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { memoryMonitor, formatBytes, getMemoryUsagePercent } from "@/lib/memory-
import { prisma } from "@/lib/db";
import { z } from "zod";

export const dynamic = "force-dynamic";

/**
* GET /api/admin/memory
* Returns current memory statistics and history
Expand Down
42 changes: 11 additions & 31 deletions app/api/articles/[id]/related/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { logger } from "@/lib/logger";
import { findRelatedArticles } from "@/lib/services/semantic-search-service";
import { createHandler } from "@/lib/api-handler";
import { apiResponse } from "@/lib/api-response";

Check warning on line 9 in app/api/articles/[id]/related/route.ts

View workflow job for this annotation

GitHub Actions / Build & Test

'apiResponse' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 9 in app/api/articles/[id]/related/route.ts

View workflow job for this annotation

GitHub Actions / Build & Test

'apiResponse' is defined but never used. Allowed unused vars must match /^_/u

export const dynamic = "force-dynamic";

Expand All @@ -28,36 +28,16 @@

logger.info("Finding related articles", { articleId: id, limit, minScore });

try {
const results = await findRelatedArticles(id, {
limit,
minScore,
excludeSameFeed,
});

return {
articleId: id,
results,
count: results.length,
};
} catch (error) {
// Handle "no embedding" error gracefully
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error("Failed to find related articles", {
error: error instanceof Error ? { message: error.message, stack: error.stack } : error,
articleId: id
});

if (errorMessage.includes("no embedding")) {
return apiResponse({
articleId: id,
results: [],
count: 0,
message: "Article has no embedding. Generate embeddings to enable related articles.",
});
}

throw error;
}
const results = await findRelatedArticles(id, {
limit,
minScore,
excludeSameFeed,
});

return {
articleId: id,
results,
count: results.length,
};
});

4 changes: 2 additions & 2 deletions app/api/feeds/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ export const GET = createHandler(
*/
export const POST = createHandler(
async ({ body, session }) => {
const { url, name, categoryIds } = body;
const { url, name, categoryIds, settings } = body;

let feed;
let isNewFeed = false;

try {
// Try to create the feed
feed = await validateAndCreateFeed(url, name, categoryIds);
feed = await validateAndCreateFeed(url, name, categoryIds, settings);
isNewFeed = true;
} catch (error) {
// If feed already exists, get it instead
Expand Down
6 changes: 6 additions & 0 deletions app/api/feeds/validate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export const POST = createHandler(
// Get feed info
try {
const feedInfo = await parseFeedUrl(normalizedUrl);
if (!feedInfo) {
return apiResponse({
valid: false,
error: "Unable to fetch feed information",
});
}
return apiResponse({
valid: true,
feedInfo: {
Expand Down
2 changes: 2 additions & 0 deletions app/api/proxy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { rewriteUrls, extractBaseUrl } from "@/lib/url-rewriter";
import { NextResponse } from "next/server";
import { z } from "zod";

export const dynamic = "force-dynamic";

// Simple in-memory cache
const cache = new Map<string, { content: string; contentType: string; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
Expand Down
2 changes: 2 additions & 0 deletions app/api/saved-searches/[id]/articles/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { createHandler } from "@/lib/api-handler";
import { z } from "zod";
import * as savedSearchService from "@/lib/services/saved-search-service";

export const dynamic = "force-dynamic";

// Query schema for filtering and pagination
const articlesQuerySchema = z.object({
limit: z.coerce.number().int().min(1).max(100).optional().default(50).catch(50),
Expand Down
2 changes: 2 additions & 0 deletions app/api/saved-searches/[id]/rematch/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { createHandler } from "@/lib/api-handler";
import { rematchSavedSearch } from "@/lib/services/saved-search-matcher";
import { getSavedSearchById } from "@/lib/services/saved-search-service";

export const dynamic = "force-dynamic";

/**
* POST /api/saved-searches/[id]/rematch
* Trigger rematch for a saved search
Expand Down
2 changes: 2 additions & 0 deletions app/api/saved-searches/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import * as savedSearchService from "@/lib/services/saved-search-service";
import { rematchSavedSearch } from "@/lib/services/saved-search-matcher";
import { logger } from "@/lib/logger";

export const dynamic = "force-dynamic";

// Validation schema for updating a saved search
const updateSavedSearchSchema = z.object({
name: z.string().min(1).max(100).optional(),
Expand Down
2 changes: 2 additions & 0 deletions app/api/saved-searches/insights/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { createHandler } from '@/lib/api-handler';
import { prisma } from '@/lib/db';
import { logger } from '@/lib/logger';

export const dynamic = "force-dynamic";

interface SavedSearchInsight {
id: string;
name: string;
Expand Down
2 changes: 2 additions & 0 deletions app/api/saved-searches/preview/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { createHandler } from "@/lib/api-handler";
import { z } from "zod";
import * as savedSearchService from "@/lib/services/saved-search-service";

export const dynamic = "force-dynamic";

// Validation schema for preview request
const previewSearchSchema = z.object({
query: z.string().min(1),
Expand Down
2 changes: 2 additions & 0 deletions app/api/saved-searches/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import * as savedSearchService from "@/lib/services/saved-search-service";
import { matchNewArticles } from "@/lib/services/saved-search-matcher";
import { logger } from "@/lib/logger";

export const dynamic = "force-dynamic";

// Validation schema for creating a saved search
const createSavedSearchSchema = z.object({
name: z.string().min(1).max(100),
Expand Down
2 changes: 2 additions & 0 deletions app/api/saved-searches/templates/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
getTemplateById,
} from '@/lib/services/search-templates-service';

export const dynamic = "force-dynamic";

const querySchema = z.object({
category: z.enum(['technology', 'news', 'research', 'jobs', 'custom']).optional(),
keyword: z.string().optional(),
Expand Down
2 changes: 2 additions & 0 deletions app/api/user/notifications/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
deleteNotification,
} from "@/lib/services/notification-service";

export const dynamic = "force-dynamic";

/**
* PATCH /api/user/notifications/[id]
* Mark a notification as read
Expand Down
14 changes: 11 additions & 3 deletions app/components/articles/ArticleViewTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ export function ArticleViewTracker({ articleId, estimatedTime, onReadStatusChang

const viewStartTime = useRef<number | null>(null);
const hasTrackedView = useRef(false);
// Use ref to always have latest estimatedTime in cleanup function
const estimatedTimeRef = useRef(estimatedTime);

const autoMarkAsRead = preferences?.autoMarkAsRead ?? false;
const autoMarkAsRead = preferences?.autoMarkAsRead ?? true;

// Keep ref in sync with prop
useEffect(() => {
estimatedTimeRef.current = estimatedTime;
}, [estimatedTime]);

// Track view on mount
useEffect(() => {
Expand Down Expand Up @@ -59,13 +66,14 @@ export function ArticleViewTracker({ articleId, estimatedTime, onReadStatusChang
if (timeSpent < 0) return;

// Don't track if estimatedTime is not yet calculated (0 or negative)
if (estimatedTime <= 0) return;
const currentEstimatedTime = estimatedTimeRef.current;
if (currentEstimatedTime <= 0) return;

trackExit.mutate({
articleId,
data: {
timeSpent,
estimatedTime,
estimatedTime: currentEstimatedTime,
},
});
};
Expand Down
Loading
Loading