From 253db4165edda1cc26af360b9ceaaf99036727b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=85=20Daniel=20Danielecki?= Date: Mon, 4 May 2026 13:28:23 +0200 Subject: [PATCH 1/3] refactor: streamline AI prediction and stock of the day routes with new LLM config resolver - Replace redundant BYOK provider logic in both routes with a centralized LLM configuration resolver. - Introduce `resolveMarketRouteLLMConfig` to handle LLM credential resolution based on user tier and requested provider. - Update UserProfileMenu to allow both LOCAL and BYOK tiers for the Ollama provider. - Enhance error handling and logging for cases where LLM credentials are not available. --- .../market/ai-prediction/[symbol]/route.ts | 66 ++++------------- app/api/market/stock-of-the-day/route.ts | 64 ++++------------- components/UserProfileMenu.tsx | 20 +++++- lib/resolve-market-ai-llm-config.ts | 72 +++++++++++++++++++ 4 files changed, 118 insertions(+), 104 deletions(-) create mode 100644 lib/resolve-market-ai-llm-config.ts diff --git a/app/api/market/ai-prediction/[symbol]/route.ts b/app/api/market/ai-prediction/[symbol]/route.ts index 674c5e6..b0f359e 100644 --- a/app/api/market/ai-prediction/[symbol]/route.ts +++ b/app/api/market/ai-prediction/[symbol]/route.ts @@ -3,13 +3,7 @@ import { aiMarketInsightsService } from "@/services/ai-market-insights.service"; import { logger } from "@/lib/logger"; import { getAuthenticatedUser } from "@/lib/server-auth"; import { subscriptionService } from "@/services/subscription.service"; -import { appwriteAIKeyStoreService } from "@/services/appwrite-ai-key-store.service"; -import type { AIProvider } from "@/types"; -import type { BYOKProvider } from "@/services/api-key-manager.service"; - -function isBYOKProvider(value: string): value is BYOKProvider { - return ["OPENAI", "GEMINI", "MISTRAL", "DEEPSEEK"].includes(value); -} +import { resolveMarketRouteLLMConfig } from "@/lib/resolve-market-ai-llm-config"; export async function GET( request: NextRequest, @@ -43,54 +37,22 @@ export async function GET( const requestedProviderRaw = request.headers.get("x-ai-provider")?.trim().toUpperCase() ?? ""; - const model = process.env.AI_MODEL; - - let llmConfig: - | { - provider: AIProvider; - apiKey?: string; - model?: string; - } - | undefined; - if (tier === "LOCAL") { - llmConfig = { - provider: "OLLAMA", - model: process.env.OLLAMA_MODEL ?? model, - }; - } else if (tier === "BYOK") { - const requestedProvider = isBYOKProvider(requestedProviderRaw) - ? requestedProviderRaw - : null; - const provider = - requestedProvider ?? - (await appwriteAIKeyStoreService.getPreferredProvider(auth.id)); - if (!provider) { - return NextResponse.json( - { - success: false, - error: - "No BYOK API key found. Save a provider key in profile first.", - }, - { status: 403 } - ); - } - const apiKey = await appwriteAIKeyStoreService.getDecryptedKey( - auth.id, - provider - ); - if (!apiKey) { - return NextResponse.json( - { - success: false, - error: `No API key stored for provider ${provider}. Change provider in profile or save a key for ${provider}.`, - }, - { status: 403 } - ); - } - llmConfig = { provider, apiKey, model }; + const resolved = await resolveMarketRouteLLMConfig({ + tier, + userId: auth.id, + requestedProviderRaw, + }); + if (!resolved.ok) { + logger.warn("AI prediction: no LLM credentials; using heuristic only", { + userId: auth.id, + tier, + symbol, + detail: resolved.error, + }); } + const llmConfig = resolved.ok ? resolved.llmConfig : undefined; const data = await aiMarketInsightsService.generatePrediction( symbol, llmConfig diff --git a/app/api/market/stock-of-the-day/route.ts b/app/api/market/stock-of-the-day/route.ts index db3e400..4fe2ba9 100644 --- a/app/api/market/stock-of-the-day/route.ts +++ b/app/api/market/stock-of-the-day/route.ts @@ -3,13 +3,7 @@ import { aiMarketInsightsService } from "@/services/ai-market-insights.service"; import { logger } from "@/lib/logger"; import { getAuthenticatedUser } from "@/lib/server-auth"; import { subscriptionService } from "@/services/subscription.service"; -import { appwriteAIKeyStoreService } from "@/services/appwrite-ai-key-store.service"; -import type { AIProvider } from "@/types"; -import type { BYOKProvider } from "@/services/api-key-manager.service"; - -function isBYOKProvider(value: string): value is BYOKProvider { - return ["OPENAI", "GEMINI", "MISTRAL", "DEEPSEEK"].includes(value); -} +import { resolveMarketRouteLLMConfig } from "@/lib/resolve-market-ai-llm-config"; export async function GET(request: NextRequest) { try { @@ -31,54 +25,24 @@ export async function GET(request: NextRequest) { const requestedProviderRaw = request.headers.get("x-ai-provider")?.trim().toUpperCase() ?? ""; - const model = process.env.AI_MODEL; - let llmConfig: - | { - provider: AIProvider; - apiKey?: string; - model?: string; + const resolved = await resolveMarketRouteLLMConfig({ + tier, + userId: auth.id, + requestedProviderRaw, + }); + if (!resolved.ok) { + logger.warn( + "Stock of the day: no LLM credentials; using heuristic only", + { + userId: auth.id, + tier, + detail: resolved.error, } - | undefined; - - if (tier === "LOCAL") { - llmConfig = { - provider: "OLLAMA", - model: process.env.OLLAMA_MODEL ?? model, - }; - } else if (tier === "BYOK") { - const requestedProvider = isBYOKProvider(requestedProviderRaw) - ? requestedProviderRaw - : null; - const provider = - requestedProvider ?? - (await appwriteAIKeyStoreService.getPreferredProvider(auth.id)); - if (!provider) { - return NextResponse.json( - { - success: false, - error: - "No BYOK API key found. Save a provider key in profile first.", - }, - { status: 403 } - ); - } - const apiKey = await appwriteAIKeyStoreService.getDecryptedKey( - auth.id, - provider ); - if (!apiKey) { - return NextResponse.json( - { - success: false, - error: `No API key stored for provider ${provider}. Change provider in profile or save a key for ${provider}.`, - }, - { status: 403 } - ); - } - llmConfig = { provider, apiKey, model }; } + const llmConfig = resolved.ok ? resolved.llmConfig : undefined; const data = await aiMarketInsightsService.getStockOfTheDay(llmConfig); return NextResponse.json({ diff --git a/components/UserProfileMenu.tsx b/components/UserProfileMenu.tsx index 1d4879c..c9deab7 100644 --- a/components/UserProfileMenu.tsx +++ b/components/UserProfileMenu.tsx @@ -27,8 +27,8 @@ const PROVIDER_OPTIONS: Array<{ { id: "OLLAMA", name: "Ollama (Local)", - subtitle: "Run locally on your machine", - allowedTiers: ["LOCAL"], + subtitle: "Run on your machine (no provider API key)", + allowedTiers: ["LOCAL", "BYOK"], }, { id: "OPENAI", @@ -161,6 +161,22 @@ export function UserProfileMenu() { } }, []); + useEffect(() => { + const allowedForTier = PROVIDER_OPTIONS.filter((p) => + p.allowedTiers.includes(tier) + ); + if (allowedForTier.length === 0) return; + const currentOk = allowedForTier.some( + (p) => p.id === selectedExplanationProvider + ); + if (currentOk) return; + const next = allowedForTier[0].id; + setSelectedExplanationProvider(next); + if (typeof window !== "undefined") { + localStorage.setItem("explanations_provider", next); + } + }, [tier, selectedExplanationProvider]); + useEffect(() => { if (typeof window === "undefined") return; const params = new URLSearchParams(window.location.search); diff --git a/lib/resolve-market-ai-llm-config.ts b/lib/resolve-market-ai-llm-config.ts new file mode 100644 index 0000000..1b8d33e --- /dev/null +++ b/lib/resolve-market-ai-llm-config.ts @@ -0,0 +1,72 @@ +import { appwriteAIKeyStoreService } from "@/services/appwrite-ai-key-store.service"; +import type { BYOKProvider } from "@/services/api-key-manager.service"; +import type { AIProvider } from "@/types"; + +export type MarketRouteLLMConfig = { + provider: AIProvider; + apiKey?: string; + model?: string; +}; + +function isBYOKCloudProvider(value: string): value is BYOKProvider { + return ["OPENAI", "GEMINI", "MISTRAL", "DEEPSEEK"].includes(value); +} + +/** + * Resolves LLM config for market AI routes (BYOK can use Ollama without a stored API key). + */ +export async function resolveMarketRouteLLMConfig(opts: { + tier: string; + userId: string; + requestedProviderRaw: string; +}): Promise< + | { ok: true; llmConfig: MarketRouteLLMConfig | undefined } + | { ok: false; error: string } +> { + const model = process.env.AI_MODEL; + const ollamaModel = process.env.OLLAMA_MODEL ?? model; + + if (opts.tier === "LOCAL") { + return { + ok: true, + llmConfig: { provider: "OLLAMA", model: ollamaModel }, + }; + } + + if (opts.tier === "BYOK") { + const header = opts.requestedProviderRaw.trim().toUpperCase(); + if (header === "OLLAMA") { + return { + ok: true, + llmConfig: { provider: "OLLAMA", model: ollamaModel }, + }; + } + + const requested = isBYOKCloudProvider(header) ? header : null; + const provider = + requested ?? + (await appwriteAIKeyStoreService.getPreferredProvider(opts.userId)); + if (!provider) { + return { + ok: false, + error: + "No cloud API key on file. Add an OpenAI, Gemini, Mistral, or DeepSeek key under Profile, or select Ollama (Local) to use your machine without a provider key.", + }; + } + + const apiKey = await appwriteAIKeyStoreService.getDecryptedKey( + opts.userId, + provider + ); + if (!apiKey) { + return { + ok: false, + error: `No API key stored for provider ${provider}. Change provider in profile or save a key for ${provider}.`, + }; + } + + return { ok: true, llmConfig: { provider, apiKey, model } }; + } + + return { ok: true, llmConfig: undefined }; +} From 059106603e5494c804b51ee8ca27ffc381f7562b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=85=20Daniel=20Danielecki?= Date: Mon, 4 May 2026 14:08:53 +0200 Subject: [PATCH 2/3] feat: enhance Yahoo Finance integration and improve error handling - Add YAHOO_FINANCE_API_URL to .env.example for configurable API endpoint. - Update YahooFinanceService to alternate between query1 and query2 endpoints for improved reliability. - Enhance error handling in HomePageClient and stock panels to provide clearer messages based on API responses. - Include pricing tier information in AIPredictionPanel and StockOfTheDayPanel for better user guidance. --- .env.example | 2 + app/(main)/home-page-client.tsx | 7 ++- app/(main)/stock-of-the-day/page.tsx | 1 + components/AIPredictionPanel.tsx | 7 ++- components/StockOfTheDayPanel.tsx | 9 ++- components/UserProfileMenu.tsx | 17 +++++- lib/ai-subscription-ux.ts | 13 +++++ lib/env.ts | 5 +- services/yahoo-finance.service.ts | 87 ++++++++++++++++++++++------ 9 files changed, 121 insertions(+), 27 deletions(-) create mode 100644 lib/ai-subscription-ux.ts diff --git a/.env.example b/.env.example index 467269d..443c458 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,5 @@ STRIPE_WEBHOOK_SECRET= VERCEL_ORG_ID= VERCEL_PROJECT_ID= VERCEL_TOKEN= +# Optional; default is query2 (often more stable from servers than query1). +YAHOO_FINANCE_API_URL= diff --git a/app/(main)/home-page-client.tsx b/app/(main)/home-page-client.tsx index 17ffa51..142dd29 100644 --- a/app/(main)/home-page-client.tsx +++ b/app/(main)/home-page-client.tsx @@ -258,7 +258,10 @@ export function HomePageClient() { `/api/market/symbol/${selectedSymbol}` ); if (!symbolResponse.ok) { - throw new Error("Failed to fetch symbol data"); + const body = (await symbolResponse.json().catch(() => ({}))) as { + error?: string; + }; + throw new Error(body.error ?? "Failed to fetch symbol data"); } const symbolResult = await symbolResponse.json(); setSymbolData(symbolResult.data); @@ -531,6 +534,7 @@ export function HomePageClient() { loading={aiPredictionLoading} locked={!hasAIAccess} error={aiPredictionError} + pricingTier={effectiveTier} /> )} @@ -569,6 +573,7 @@ export function HomePageClient() { loading={stockOfTheDayLoading} locked={!hasAIAccess} error={stockOfTheDayError} + pricingTier={effectiveTier} /> diff --git a/app/(main)/stock-of-the-day/page.tsx b/app/(main)/stock-of-the-day/page.tsx index 4c111bf..7da8ffa 100644 --- a/app/(main)/stock-of-the-day/page.tsx +++ b/app/(main)/stock-of-the-day/page.tsx @@ -102,6 +102,7 @@ export default function StockOfTheDayPage() { loading={loading} locked={!hasAIAccess} error={loadError} + pricingTier={pricingTier} /> ); diff --git a/components/AIPredictionPanel.tsx b/components/AIPredictionPanel.tsx index fc6a677..9921cbf 100644 --- a/components/AIPredictionPanel.tsx +++ b/components/AIPredictionPanel.tsx @@ -1,7 +1,8 @@ "use client"; import Link from "next/link"; -import type { AIPredictionReport } from "@/types"; +import type { AIPredictionReport, PricingTier } from "@/types"; +import { getAiSubscriptionGateMessage } from "@/lib/ai-subscription-ux"; import { isMissingByokApiKeyMessage } from "@/lib/missing-byok-api-key"; interface AIPredictionPanelProps { @@ -9,6 +10,7 @@ interface AIPredictionPanelProps { loading: boolean; locked: boolean; error?: string | null; + pricingTier?: PricingTier | null; } function RecommendationBadge({ @@ -38,6 +40,7 @@ export function AIPredictionPanel({ loading, locked, error, + pricingTier, }: AIPredictionPanelProps) { const politicalFactors = prediction?.politicalFactors ?? []; const financialTrendFactors = prediction?.financialTrendFactors ?? []; @@ -155,7 +158,7 @@ export function AIPredictionPanel({ {locked && (

- AI prediction is available only for AI subscriptions. + {getAiSubscriptionGateMessage(pricingTier ?? undefined)}

@@ -102,8 +106,7 @@ export function StockOfTheDayPanel({ {locked && (

- AI section locked. Enable any AI subscription to reveal - today's pick. + {getAiSubscriptionGateMessage(pricingTier ?? undefined)}

p.allowedTiers.includes(tier) ); - if (allowedForTier.length === 0) return; + if (allowedForTier.length === 0) { + if (typeof window !== "undefined") { + localStorage.removeItem("explanations_provider"); + } + setSelectedExplanationProvider((prev) => + prev === "OPENAI" ? prev : "OPENAI" + ); + return; + } const currentOk = allowedForTier.some( (p) => p.id === selectedExplanationProvider ); @@ -347,6 +355,13 @@ export function UserProfileMenu() {

Explanations Provider

+ {!["LOCAL", "BYOK", "HOSTED_AI"].includes(tier) && ( +

+ AI explanations (Ollama, cloud keys, or Ditectrev AI) unlock + after you subscribe to an AI plan — Ads-free and Free tiers do + not include server-side AI. +

+ )}
{PROVIDER_OPTIONS.map((provider) => { const allowed = provider.allowedTiers.includes(tier); diff --git a/lib/ai-subscription-ux.ts b/lib/ai-subscription-ux.ts new file mode 100644 index 0000000..946ba39 --- /dev/null +++ b/lib/ai-subscription-ux.ts @@ -0,0 +1,13 @@ +import type { PricingTier } from "@/types"; + +/** + * Copy for AI-gated panels when the user has no AI-enabled subscription. + */ +export function getAiSubscriptionGateMessage( + tier: PricingTier | null | undefined +): string { + if (tier === "FREE" || tier === "ADS_FREE") { + return "Your current plan does not include AI. Upgrade to Local AI for Ollama on your machine, Bring Your Own Key for OpenAI/Gemini/Mistral/DeepSeek, or Ditectrev AI for managed AI."; + } + return "Enable a Local AI, Bring Your Own Key, or Ditectrev AI subscription to unlock this section."; +} diff --git a/lib/env.ts b/lib/env.ts index 64851ec..d8dd2b6 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -28,10 +28,13 @@ interface EnvConfig { }; } +const defaultYahooFinanceUrl = "https://query2.finance.yahoo.com"; + export const env: EnvConfig = { apis: { cnnDatavizUrl: "https://production.dataviz.cnn.io", - yahooFinanceUrl: "https://query1.finance.yahoo.com", + yahooFinanceUrl: + process.env.YAHOO_FINANCE_API_URL?.trim() || defaultYahooFinanceUrl, }, cache: { ttlSeconds: 300, diff --git a/services/yahoo-finance.service.ts b/services/yahoo-finance.service.ts index b43c81a..7b3e007 100644 --- a/services/yahoo-finance.service.ts +++ b/services/yahoo-finance.service.ts @@ -31,7 +31,23 @@ export class YahooFinanceService { private crumbExpiry: number = 0; constructor() { - this.baseUrl = env.apis.yahooFinanceUrl; + this.baseUrl = env.apis.yahooFinanceUrl.replace(/\/$/, ""); + } + + /** Yahoo exposes query1/query2; some datacenters get 5xx on one host only. */ + private alternateYahooOrigin(): string | null { + if (this.baseUrl.includes("query1.finance.yahoo.com")) { + return "https://query2.finance.yahoo.com"; + } + if (this.baseUrl.includes("query2.finance.yahoo.com")) { + return "https://query1.finance.yahoo.com"; + } + return null; + } + + private yahooChartBases(): string[] { + const alt = this.alternateYahooOrigin(); + return alt ? [this.baseUrl, alt] : [this.baseUrl]; } /** @@ -127,15 +143,31 @@ export class YahooFinanceService { async getSymbolQuote(symbol: string): Promise { return retryWithBackoff(async () => { try { - const response = await fetch( - `${this.baseUrl}/v8/finance/quote?symbols=${symbol}`, - { - headers: { - Accept: "application/json", - "User-Agent": "Mozilla/5.0", - }, - } - ); + const ua = "Mozilla/5.0 (compatible; StockExchangeApp/1.0)"; + let response: Response | undefined; + for (const base of this.yahooChartBases()) { + response = await fetch( + `${base}/v8/finance/quote?symbols=${encodeURIComponent(symbol)}`, + { + headers: { + Accept: "application/json", + "User-Agent": ua, + }, + } + ); + if (response.ok) break; + const retryable = [429, 500, 502, 503, 504].includes(response.status); + if (!retryable) break; + logger.warn("Yahoo quote: transient error, trying alternate host", { + symbol, + status: response.status, + base, + }); + } + + if (!response) { + throw new Error("Yahoo Finance quote: empty response"); + } if (!response.ok) { throw new Error( @@ -171,15 +203,32 @@ export class YahooFinanceService { try { const { interval, period } = this.getTimeRangeParams(range); - const response = await fetch( - `${this.baseUrl}/v8/finance/chart/${symbol}?interval=${interval}&range=${period}`, - { - headers: { - Accept: "application/json", - "User-Agent": "Mozilla/5.0", - }, - } - ); + const ua = "Mozilla/5.0 (compatible; StockExchangeApp/1.0)"; + let response: Response | undefined; + for (const base of this.yahooChartBases()) { + response = await fetch( + `${base}/v8/finance/chart/${encodeURIComponent(symbol)}?interval=${interval}&range=${period}`, + { + headers: { + Accept: "application/json", + "User-Agent": ua, + }, + } + ); + if (response.ok) break; + const retryable = [429, 500, 502, 503, 504].includes(response.status); + if (!retryable) break; + logger.warn("Yahoo chart: transient error, trying alternate host", { + symbol, + range, + status: response.status, + base, + }); + } + + if (!response) { + throw new Error("Yahoo Finance chart: empty response"); + } if (!response.ok) { throw new Error( From 6ac65aaabc3ca39d20d9c92414a8360959d5d500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=85=20Daniel=20Danielecki?= Date: Tue, 5 May 2026 14:25:55 +0200 Subject: [PATCH 3/3] chore: update environment variables in deployment workflow - Add FINNHUB_BASE_URL, FINNHUB_API_KEY, OLLAMA_MODEL, and YAHOO_FINANCE_API_URL to the deployment workflow for better configuration management. - Remove optional comment for YAHOO_FINANCE_API_URL in .env.example to streamline the configuration process. - Update Google Tag Manager script implementation in layout.tsx for improved performance and security. --- .env.example | 1 - .github/workflows/deploy.yml | 14 ++++++++++++++ app/layout.tsx | 12 +++++++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 443c458..1818c99 100644 --- a/.env.example +++ b/.env.example @@ -25,5 +25,4 @@ STRIPE_WEBHOOK_SECRET= VERCEL_ORG_ID= VERCEL_PROJECT_ID= VERCEL_TOKEN= -# Optional; default is query2 (often more stable from servers than query1). YAHOO_FINANCE_API_URL= diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d916c53..bf17bbc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -180,6 +180,16 @@ jobs: vercel env add APPWRITE_COLLECTION_ID_AI_KEYS $ENV_TYPE "" --value "${{ secrets.APPWRITE_COLLECTION_ID_AI_KEYS }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} vercel env add APPWRITE_COLLECTION_ID_TRIAL_SESSIONS $ENV_TYPE "" --value "${{ secrets.APPWRITE_COLLECTION_ID_TRIAL_SESSIONS }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} vercel env add APPWRITE_COLLECTION_ID_SUBSCRIPTIONS $ENV_TYPE "" --value "${{ secrets.APPWRITE_COLLECTION_ID_SUBSCRIPTIONS }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} + if [ -n "${{ vars.FINNHUB_BASE_URL }}" ]; then + vercel env add FINNHUB_BASE_URL $ENV_TYPE "" --value "${{ vars.FINNHUB_BASE_URL }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} + fi + vercel env add FINNHUB_API_KEY $ENV_TYPE "" --value "${{ secrets.FINNHUB_API_KEY }}" --yes --force --sensitive --token=${{ secrets.VERCEL_TOKEN }} + if [ -n "${{ vars.OLLAMA_MODEL }}" ]; then + vercel env add OLLAMA_MODEL $ENV_TYPE "" --value "${{ vars.OLLAMA_MODEL }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} + fi + if [ -n "${{ vars.YAHOO_FINANCE_API_URL }}" ]; then + vercel env add YAHOO_FINANCE_API_URL $ENV_TYPE "" --value "${{ vars.YAHOO_FINANCE_API_URL }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} + fi if [ -n "${{ vars.STRIPE_PRICE_ADS_FREE }}" ]; then vercel env add STRIPE_PRICE_ADS_FREE $ENV_TYPE "" --value "${{ vars.STRIPE_PRICE_ADS_FREE }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} fi @@ -216,6 +226,10 @@ jobs: APPWRITE_COLLECTION_ID_AI_KEYS: ${{ secrets.APPWRITE_COLLECTION_ID_AI_KEYS }} APPWRITE_COLLECTION_ID_TRIAL_SESSIONS: ${{ secrets.APPWRITE_COLLECTION_ID_TRIAL_SESSIONS }} APPWRITE_COLLECTION_ID_SUBSCRIPTIONS: ${{ secrets.APPWRITE_COLLECTION_ID_SUBSCRIPTIONS }} + FINNHUB_BASE_URL: ${{ vars.FINNHUB_BASE_URL }} + FINNHUB_API_KEY: ${{ secrets.FINNHUB_API_KEY }} + OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }} + YAHOO_FINANCE_API_URL: ${{ vars.YAHOO_FINANCE_API_URL }} STRIPE_PRICE_ADS_FREE: ${{ vars.STRIPE_PRICE_ADS_FREE }} STRIPE_PRICE_LOCAL: ${{ vars.STRIPE_PRICE_LOCAL }} STRIPE_PRICE_BYOK: ${{ vars.STRIPE_PRICE_BYOK }} diff --git a/app/layout.tsx b/app/layout.tsx index a8eb17d..207447e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,5 @@ import type { Metadata } from "next"; import { Ibarra_Real_Nova, Merriweather } from "next/font/google"; -import Script from "next/script"; import "./globals.css"; import { Providers } from "./providers"; @@ -37,13 +36,16 @@ export default function RootLayout({ {gtmId ? ( - +})(window,document,'script','dataLayer','${gtmId}');`, + }} + /> ) : null}