From fc707660735ab58bed30410a005c1955bfca703c Mon Sep 17 00:00:00 2001 From: skull8888888 Date: Tue, 29 Oct 2024 21:32:46 -0700 Subject: [PATCH 1/6] updated welcome email (#112) --- frontend/lib/emails/welcome-email.tsx | 115 ++++++++++++++++++++------ 1 file changed, 92 insertions(+), 23 deletions(-) diff --git a/frontend/lib/emails/welcome-email.tsx b/frontend/lib/emails/welcome-email.tsx index da3ef539..c658f77f 100644 --- a/frontend/lib/emails/welcome-email.tsx +++ b/frontend/lib/emails/welcome-email.tsx @@ -1,36 +1,105 @@ import { Html, Text, Link, Preview } from '@react-email/components'; -export default function WelcomeEmail({}: {}) { +export default function WelcomeEmail({ }: {}) { return ( - {`We're thrilled to have you on board and can't wait to see what you'll achieve with our platform!`} - Welcome to Laminar! - - {`My name is Robert and I am the CEO of Laminar. We're thrilled to have you on board and can't wait to see what you'll achieve with our platform!`} - - - The easiest way to get started with Laminar is to read our - - {' docs'} - - . - - - If you have any questions, feel free to - - {' book some time '} - - with me. - - Best, - Robert, Co-founder & CEO of Laminar + Welcome to Laminar - start tracing, evaluating, and analyzing your LLM app in minutes +
+ Welcome to Laminar! 👋 + + I{"'"}m Robert, CEO of Laminar. Stoked to have you join our community! + + + With Laminar you can trace, evaluate, label, and analyze your LLM application. + Laminar is fully open source, so don{"'"}t forget to + + {' star ⭐ our repo on GitHub'} + + ! + + + To help you get started, our team has put together comprehensive documentation: + +
+ + â€ĸ + Tracing your LLM applications + + + + â€ĸ + Running evaluations + + + + â€ĸ + Creating and managing labels + + + + â€ĸ + Working with datasets + + +
+ + Got questions or running into issues? I'm here to help - just + + {' grab a slot on my calendar'} + + {' and we can pair on it.'} + + Happy coding! + Robert + Co-founder & CEO @ Laminar +
); } const text = { - fontFamily: "'Arial', 'Roboto', 'Helvetica', sans-serif", + fontFamily: "'Inter', 'Roboto', 'Helvetica', sans-serif", fontSize: '13px', fontWeight: '400', lineHeight: '19.5px' }; + +const container = { + margin: '0 auto', + padding: '20px', + maxWidth: '500px', +}; + +const heading = { + ...text, + fontSize: '24px', + fontWeight: '600', + marginBottom: '24px', +}; + +const link = { + color: '#2563eb', + textDecoration: 'none', +}; + +const signature = { + ...text, + marginTop: '24px', + fontWeight: '500', +}; + +const role = { + ...text, + color: '#6b7280', + fontSize: '12px', +}; + +const bulletList = { + marginLeft: '20px', + marginBottom: '16px', +}; + +const bulletPoint = { + ...text, + marginBottom: '8px', +}; From d6eb3c05fdc725e678caf8fdf1e20b08ef57d810 Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Tue, 29 Oct 2024 21:46:07 -0700 Subject: [PATCH 2/6] fixed build --- frontend/lib/emails/welcome-email.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/lib/emails/welcome-email.tsx b/frontend/lib/emails/welcome-email.tsx index c658f77f..3b7704e6 100644 --- a/frontend/lib/emails/welcome-email.tsx +++ b/frontend/lib/emails/welcome-email.tsx @@ -43,7 +43,7 @@ export default function WelcomeEmail({ }: {}) { - Got questions or running into issues? I'm here to help - just + Got questions or running into issues? I{"'"}m here to help - just {' grab a slot on my calendar'} From 426340aa7322129e22d48272853ca1b18da3b29b Mon Sep 17 00:00:00 2001 From: Dinmukhamed Mailibay <47117969+dinmukhamedm@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:11:50 -0700 Subject: [PATCH 3/6] minor fix to correctly export spans to datasets (#113) --- .../components/traces/export-spans-dialog.tsx | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/frontend/components/traces/export-spans-dialog.tsx b/frontend/components/traces/export-spans-dialog.tsx index bd2f687c..1b150a96 100644 --- a/frontend/components/traces/export-spans-dialog.tsx +++ b/frontend/components/traces/export-spans-dialog.tsx @@ -39,8 +39,8 @@ export default function ExportSpansDialog({ span }: ExportSpansDialogProps) { const { toast } = useToast(); - const [data, setData] = useState(span.input); - const [target, setTarget] = useState(span.output); + const [data, setData] = useState(toJsonObject(span.input, 'input')); + const [target, setTarget] = useState(toJsonObject(span.output, 'output')); const [isDataValid, setIsDataValid] = useState(true); const [isTargetValid, setIsTargetValid] = useState(true); @@ -161,11 +161,7 @@ export default function ExportSpansDialog({ span }: ExportSpansDialogProps) { className="max-h-[500px]" editable defaultMode={'json'} - value={JSON.stringify( - toJsonObject(span.input, 'input'), - null, - 2 - )} + value={JSON.stringify(data, null, 2)} onChange={handleDataChange} /> {!isDataValid && ( @@ -178,11 +174,7 @@ export default function ExportSpansDialog({ span }: ExportSpansDialogProps) { className="max-h-[500px]" editable defaultMode={'json'} - value={JSON.stringify( - toJsonObject(span.output, 'output'), - null, - 2 - )} + value={JSON.stringify(target, null, 2)} onChange={handleTargetChange} /> {!isTargetValid && ( @@ -195,11 +187,7 @@ export default function ExportSpansDialog({ span }: ExportSpansDialogProps) { className="max-h-[500px]" editable defaultMode={'json'} - value={JSON.stringify( - toJsonObject(metadata, 'metadata'), - null, - 2 - )} + value={JSON.stringify(metadata, null, 2)} onChange={handleMetadataChange} /> {!isMetadataValid && ( From 805080d61e0c4c6523fe52217707f789364549fe Mon Sep 17 00:00:00 2001 From: Dinmukhamed Mailibay <47117969+dinmukhamedm@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:27:16 -0700 Subject: [PATCH 4/6] Easier contribution experience for frontend-only changes (#114) * add default vars to environment example in frontend * add thin docker-compose for hot-reload frontend changes --- CONTRIBUTING.md | 63 +++++++++++++++++++------- docker-compose-local-dev-full.yml | 73 ++++++++++++++++++++++++++++++ docker-compose-local-dev.yml | 75 ++++++++++--------------------- docker-compose.yml | 4 +- frontend/.env.local.example | 30 ++++++++----- 5 files changed, 163 insertions(+), 82 deletions(-) create mode 100644 docker-compose-local-dev-full.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 915edb80..7e46ae9f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,22 +26,24 @@ Don't get overwhelmed by the number of docker-compose files. Here's a quick over - `docker-compose.yml` is the simplest one that spins up frontend, app-server, and postgres. Good for quickstarts. - `docker-compose-full.yml` is the one you want to use for running the full stack locally. This is the best for self-hosting. +- `docker-compose-local-dev-full.yml` full file for local development. To be used when you make changes + to the backend. It will only run the dependency services (postgres, qdrant, clickhouse, rabbitmq). + You will need to run `cargo r`, `pnpm run dev`, and `python server.py` manually. - `docker-compose-local-dev.yml` is the one you want to use for local development. It will only - run the dependency services (postgres, qdrant, clickhouse, rabbitmq). You will need to run - `cargo r`, `pnpm run dev`, and `python server.py` manually. + run postgres and app-server. Good for frontend changes. - `docker-compose-local-build.yml` will build the services from the source and run them in production mode. This is good for self-hosting with your own changes, or for testing the changes after developing on your own and before opening a PR. -| Service | docker-compose.yml | docker-compose-full.yml | docker-compose-local-dev.yml | docker-compose-local-build.yml | -|---------|-------------------|------------------------|----------------------------|------------------------------| -| postgres | ✅ | ✅ | ✅ | ✅ | -| qdrant | ❌ | ✅ | ✅ | ✅ | -| clickhouse | ❌ | ✅ | ✅ | ✅ | -| rabbitmq | ❌ | ✅ | ✅ | ✅ | -| app-server | ℹī¸ | ✅ | đŸ’ģ | 🔧 | -| frontend | ℹī¸ | ✅ | đŸ’ģ | 🔧 | -| semantic-search-service | ❌ | ✅ | đŸ’ģ | 🔧 | -| python-executor | ❌ | ✅ | đŸ’ģ | 🔧 | +| Service | docker-compose.yml | docker-compose-full.yml | docker-compose-local-dev-full.yml | docker-compose-local-dev.yml | docker-compose-local-build.yml | +|---------|-------------------|------------------------|------------------------------|----------------------------|------------------------------| +| postgres | ✅ | ✅ | ✅ | ✅ | ✅ | +| qdrant | ❌ | ✅ | ✅ | ❌ | ✅ | +| clickhouse | ❌ | ✅ | ✅ | ❌ | ✅ | +| rabbitmq | ❌ | ✅ | ✅ | ❌ | ✅ | +| app-server | ℹī¸ | ✅ | đŸ’ģ | ℹī¸ | 🔧 | +| frontend | ℹī¸ | ✅ | đŸ’ģ | đŸ’ģ | 🔧 | +| semantic-search-service | ❌ | ✅ | đŸ’ģ | ❌ | 🔧 | +| python-executor | ❌ | ✅ | đŸ’ģ | ❌ | 🔧 | - ✅ – service present, image is pulled from a container registry. - 🔧 – service present, image is built from the source. This may take a while. @@ -50,10 +52,39 @@ or for testing the changes after developing on your own and before opening a PR. - ❌ – service not present. -## Running Laminar locally +## Running Laminar locally for development -If you want to test your local changes, you can run code separately in -development mode. +Use this guide if you are changing frontend code only. +For making backend changes or changes across the full stack, +see [Advanced] section below. + +### 0. Configure environment variables + +```sh +cd frontend +cp .env.local.example .env.local +``` + +### 1. Spin up app-server and postgres + +```sh +docker compose -f docker-compose-local-dev.yml up +``` + +### 2. Run frontend in development mode + +```sh +cd frontend +pnpm run dev +``` + +Next.js is hot-reloadable in development mode, so any changes you make will be reflected +immediately. + +## [Advanced] Running full stack locally for development + +This guide is for when you are changing backend code, or when you want to run the full stack +locally for development. If you only want to change frontend code, see the section above. ### 0. Configure environment variables @@ -68,7 +99,7 @@ cp frontend/.env.local.example frontend/.env.local ### 1. Spin up dependency containers ```sh -docker compose -f docker-compose-local-dev.yml up +docker compose -f docker-compose-local-dev-full.yml up ``` This will spin up postgres, qdrant, clickhouse, and RabbitMQ. diff --git a/docker-compose-local-dev-full.yml b/docker-compose-local-dev-full.yml new file mode 100644 index 00000000..07371f11 --- /dev/null +++ b/docker-compose-local-dev-full.yml @@ -0,0 +1,73 @@ +# This compose definition does not build Laminar images, it is intended for local development. +# This file is meant to be used with running the Laminar services manually from each service's directory. +# Refer to CONTRIBUTING.md for more information on how to run the services locally. +name: lmnr + +services: + qdrant: + image: qdrant/qdrant + ports: + - "6333:6333" + - "6334:6334" + volumes: + - type: volume + source: qdrant-data + target: /data + + rabbitmq: + image: rabbitmq + ports: + - "5672:5672" + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 7s + timeout: 5s + retries: 3 + + clickhouse: + build: + context: ./clickhouse + container_name: clickhouse + hostname: clickhouse + ports: + - "8123:8123" + volumes: + - type: volume + source: clickhouse-data + target: /var/lib/clickhouse/ + - type: volume + source: clickhouse-logs + target: /var/log/clickhouse-server/ + cap_add: + - SYS_NICE + - NET_ADMIN + - IPC_LOCK + ulimits: + nofile: + soft: 262144 + hard: 262144 + + postgres: + image: postgres:16 + ports: + - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + healthcheck: + test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "${POSTGRES_DB}"] + interval: 2s + timeout: 5s + retries: 5 + +volumes: + qdrant-data: + clickhouse-data: + clickhouse-logs: + postgres-data: diff --git a/docker-compose-local-dev.yml b/docker-compose-local-dev.yml index 07371f11..0462cf72 100644 --- a/docker-compose-local-dev.yml +++ b/docker-compose-local-dev.yml @@ -1,55 +1,13 @@ -# This compose definition does not build Laminar images, it is intended for local development. -# This file is meant to be used with running the Laminar services manually from each service's directory. -# Refer to CONTRIBUTING.md for more information on how to run the services locally. +# This compose file is a lightweight version of docker-compose-local-dev-full.yml. +# It is intended to be used for local development on frontend only. +# It does not include ClickHouse, +# Qdrant, Semantic Search, Python executor, and RabbitMQ. +# It only includes postgres, and app-server. +# Run frontend manually with `ENVIRONMENT=LITE pnpm run dev`. + name: lmnr services: - qdrant: - image: qdrant/qdrant - ports: - - "6333:6333" - - "6334:6334" - volumes: - - type: volume - source: qdrant-data - target: /data - - rabbitmq: - image: rabbitmq - ports: - - "5672:5672" - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} - healthcheck: - test: rabbitmq-diagnostics -q ping - interval: 7s - timeout: 5s - retries: 3 - - clickhouse: - build: - context: ./clickhouse - container_name: clickhouse - hostname: clickhouse - ports: - - "8123:8123" - volumes: - - type: volume - source: clickhouse-data - target: /var/lib/clickhouse/ - - type: volume - source: clickhouse-logs - target: /var/log/clickhouse-server/ - cap_add: - - SYS_NICE - - NET_ADMIN - - IPC_LOCK - ulimits: - nofile: - soft: 262144 - hard: 262144 - postgres: image: postgres:16 ports: @@ -66,8 +24,21 @@ services: timeout: 5s retries: 5 + app-server: + image: ghcr.io/lmnr-ai/app-server + pull_policy: always + ports: + - "8000:8000" + - "8001:8001" + depends_on: + postgres: + condition: service_healthy + environment: + PORT: 8000 + GRPC_PORT: 8001 + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + SHARED_SECRET_TOKEN: ${SHARED_SECRET_TOKEN} + ENVIRONMENT: LITE # this disables runtime dependency on clickhouse, rabbitmq, semantic search, and python executor + volumes: - qdrant-data: - clickhouse-data: - clickhouse-logs: postgres-data: diff --git a/docker-compose.yml b/docker-compose.yml index 573f7249..ae03fd30 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: NEXTAUTH_URL: http://localhost:3000 NEXTAUTH_SECRET: some_secret NEXT_PUBLIC_URL: http://localhost:3000 - ENVIRONMENT: LITE # this disables runtime reliance on clickhouse + ENVIRONMENT: LITE # this disables runtime dependency on clickhouse app-server: image: ghcr.io/lmnr-ai/app-server @@ -55,7 +55,7 @@ services: GRPC_PORT: 8001 DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} SHARED_SECRET_TOKEN: ${SHARED_SECRET_TOKEN} - ENVIRONMENT: LITE # this disables runtime reliance on clickhouse, rabbitmq, semantic search, and python executor + ENVIRONMENT: LITE # this disables runtime dependency on clickhouse, rabbitmq, semantic search, and python executor volumes: postgres-data: diff --git a/frontend/.env.local.example b/frontend/.env.local.example index 78252606..17f79d8f 100644 --- a/frontend/.env.local.example +++ b/frontend/.env.local.example @@ -1,21 +1,27 @@ -NEXTAUTH_URL= -NEXTAUTH_SECRET= +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=next_secret_abc +BACKEND_URL=http://localhost:8000 +NEXT_OTEL_FETCH_DISABLED=1 +SHARED_SECRET_TOKEN=some_secret + +# these must match what you have in your docker-compose-local-dev.yml for postgres +# postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@{host}:5432/${POSTGRES_DB} +DATABASE_URL="postgres://postgres:postgres_passwordabc@localhost:5432/postgres" + +# replace with FULL if you are testing with a full stack +# ENVIRONMENT=FULL +ENVIRONMENT=LITE # this disables runtime dependency on clickhouse +# for realtime +SUPABASE_JWT_SECRET= +# for auth AUTH_GITHUB_ID= AUTH_GITHUB_SECRET= - AUTH_GOOGLE_ID= AUTH_GOOGLE_SECRET= -BACKEND_URL= - -SUPABASE_JWT_SECRET= - -NEXT_OTEL_FETCH_DISABLED=1 -SHARED_SECRET_TOKEN=some_secret - +# for s3 AWS_REGION= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= - -ENVIRONMENT=FULL +S3_IMGS_BUCKET= From d216cd17697bd18fa334c2761b4b96739c8af8c0 Mon Sep 17 00:00:00 2001 From: Dinmukhamed Mailibay <47117969+dinmukhamedm@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:37:23 -0700 Subject: [PATCH 5/6] remove join with events on a traces table (#115) --- app-server/src/db/trace.rs | 42 +++++---------------------------- app-server/src/routes/traces.rs | 4 ++-- 2 files changed, 8 insertions(+), 38 deletions(-) diff --git a/app-server/src/db/trace.rs b/app-server/src/db/trace.rs index 2a7f5d57..b26f4dc3 100644 --- a/app-server/src/db/trace.rs +++ b/app-server/src/db/trace.rs @@ -40,7 +40,7 @@ pub enum TraceType { #[derive(Serialize, sqlx::FromRow, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Trace { - pub id: Uuid, + id: Uuid, #[serde(default)] start_time: Option>, #[serde(default)] @@ -60,12 +60,12 @@ pub struct Trace { output_cost: f64, cost: f64, success: bool, - pub project_id: Uuid, + project_id: Uuid, } #[derive(Serialize, sqlx::FromRow)] #[serde(rename_all = "camelCase")] -pub struct TraceWithParentSpanAndEvents { +pub struct TraceWithTopSpan { id: Uuid, start_time: DateTime, end_time: Option>, @@ -91,9 +91,6 @@ pub struct TraceWithParentSpanAndEvents { top_span_name: Option, top_span_type: Option, top_span_path: Option, - - // 'events' is a list of partial event objects, using Option because of Coalesce - events: Option, } #[derive(FromRow, Debug)] @@ -282,26 +279,6 @@ fn add_text_join( Ok(()) } -const TRACE_EVENTS_EXPRESSION: &str = " - trace_events AS ( - SELECT - traces.id as trace_id, - jsonb_agg( - jsonb_build_object( - 'id', events.id, - 'typeId', events.template_id, - 'templateName', event_templates.name, - 'spanId', events.span_id - ) - ) as events - FROM events - JOIN event_templates ON events.template_id = event_templates.id - JOIN spans ON spans.span_id = events.span_id - JOIN traces ON traces.id = spans.trace_id - WHERE traces.start_time IS NOT NULL AND traces.end_time IS NOT NULL - GROUP BY traces.id - )"; - fn add_filters_to_traces_query(query: &mut QueryBuilder, filters: &Option>) { if let Some(filters) = filters { filters.iter().for_each(|filter| { @@ -416,11 +393,9 @@ pub async fn get_traces( filters: &Option>, date_range: &Option, text_search_filter: Option, -) -> Result> { +) -> Result> { let mut query = QueryBuilder::::new("WITH "); add_traces_info_expression(&mut query, date_range, project_id)?; - query.push(", "); - query.push(TRACE_EVENTS_EXPRESSION); query.push( " @@ -441,15 +416,13 @@ pub async fn get_traces( output_cost, cost, success, - COALESCE(trace_events.events, '[]'::jsonb) AS events, top_span_input_preview, top_span_output_preview, top_span_name, top_span_type, top_span_path, status - FROM traces_info - LEFT JOIN trace_events ON trace_events.trace_id = traces_info.id ", + FROM traces_info ", ); if let Some(search) = text_search_filter { add_text_join(&mut query, date_range, &search)?; @@ -466,7 +439,7 @@ pub async fn get_traces( .push_bind(limit as i64); let traces = query - .build_query_as::<'_, TraceWithParentSpanAndEvents>() + .build_query_as::<'_, TraceWithTopSpan>() .fetch_all(pool) .await?; @@ -483,14 +456,11 @@ pub async fn count_traces( ) -> Result { let mut query = QueryBuilder::::new("WITH "); add_traces_info_expression(&mut query, date_range, project_id)?; - query.push(", "); - query.push(TRACE_EVENTS_EXPRESSION); query.push( " SELECT COUNT(DISTINCT(id)) as total_count FROM traces_info - LEFT JOIN trace_events ON trace_events.trace_id = traces_info.id ", ); if let Some(search) = text_search_filter { diff --git a/app-server/src/routes/traces.rs b/app-server/src/routes/traces.rs index e6c1334e..c7b333e8 100644 --- a/app-server/src/routes/traces.rs +++ b/app-server/src/routes/traces.rs @@ -8,7 +8,7 @@ use crate::{ events::EventWithTemplateName, modifiers::{DateRange, Filter, RelativeDateInterval}, spans::Span, - trace::{Session, Trace, TraceWithParentSpanAndEvents}, + trace::{Session, Trace, TraceWithTopSpan}, DB, }, }; @@ -82,7 +82,7 @@ pub async fn get_traces( let (total_count, any_in_project) = count_result.map_err(|e| anyhow::anyhow!("Failed to count traces: {:?}", e))?; - let response = PaginatedResponse:: { + let response = PaginatedResponse:: { total_count, items: traces, any_in_project, From b70881c5701261e5be911aab9b1f4fa7174a7d48 Mon Sep 17 00:00:00 2001 From: skull8888888 Date: Wed, 30 Oct 2024 22:51:22 -0700 Subject: [PATCH 6/6] updated landing (#116) --- frontend/app/api/stars/route.ts | 20 ++++ .../components/landing/landing-header.tsx | 24 +++- frontend/components/landing/landing.tsx | 56 ++++++++- frontend/components/landing/pricing-card.tsx | 2 +- frontend/components/landing/pricing.tsx | 110 ++++++++++-------- frontend/components/ui/code-editor.tsx | 8 +- frontend/components/ui/slider.tsx | 6 +- 7 files changed, 166 insertions(+), 60 deletions(-) create mode 100644 frontend/app/api/stars/route.ts diff --git a/frontend/app/api/stars/route.ts b/frontend/app/api/stars/route.ts new file mode 100644 index 00000000..abb88988 --- /dev/null +++ b/frontend/app/api/stars/route.ts @@ -0,0 +1,20 @@ +export async function GET() { + try { + const response = await fetch('https://api.github.com/repos/lmnr-ai/lmnr', { + headers: { + 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch stars', { cause: await response.text() }); + } + + const data = await response.json(); + return Response.json({ stars: data.stargazers_count }); + + } catch (error) { + console.error('Error fetching stars:', error); + return Response.json({ error: 'Failed to fetch stars' }, { status: 500 }); + } +} diff --git a/frontend/components/landing/landing-header.tsx b/frontend/components/landing/landing-header.tsx index 65c90aa9..158a67be 100644 --- a/frontend/components/landing/landing-header.tsx +++ b/frontend/components/landing/landing-header.tsx @@ -3,7 +3,7 @@ import Link from 'next/link'; import Image from 'next/image'; import logo from '@/assets/logo/logo.svg'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Menu, X, XCircle } from 'lucide-react'; import { Button } from '../ui/button'; import { cn } from '@/lib/utils'; @@ -14,6 +14,21 @@ interface LandingHeaderProps { export default function LandingHeader({ hasSession }: LandingHeaderProps) { const [isMenuOpen, setIsMenuOpen] = useState(false); + const [starCount, setStarCount] = useState(null); + + useEffect(() => { + const fetchStars = async () => { + try { + const response = await fetch('/api/stars'); + const data = await response.json(); + setStarCount(data.stars); + } catch (error) { + console.error('Failed to fetch star count:', error); + } + }; + fetchStars(); + }, []); + const menuItemStyle = 'text-sm md:text-base font-medium px-2 md:px-2 py-2 md:py-1 transition-colors w-full text-left whitespace-nowrap md:rounded-sm hover:bg-secondary'; @@ -45,14 +60,17 @@ export default function LandingHeader({ hasSession }: LandingHeaderProps) { isMenuOpen ? '' : 'hidden' )} > - + Docs Pricing - GitHub + GitHub {starCount && `★ ${starCount}`} + + + Discord @@ -207,6 +216,7 @@ evaluate(
{section.codeExample && (
+